##// END OF EJS Templates
tweak stat walk in forbid_hidden
MinRK -
Show More
@@ -1,529 +1,529 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import functools
22 import functools
23 import hashlib
23 import hashlib
24 import json
24 import json
25 import logging
25 import logging
26 import mimetypes
26 import mimetypes
27 import os
27 import os
28 import stat
28 import stat
29 import sys
29 import sys
30 import threading
30 import threading
31 import traceback
31 import traceback
32
32
33 from tornado import web
33 from tornado import web
34 from tornado import websocket
34 from tornado import websocket
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.external.decorator import decorator
42 from IPython.external.decorator import decorator
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.jsonutil import date_default
44 from IPython.utils.jsonutil import date_default
45
45
46 # UF_HIDDEN is a stat flag not defined in the stat module.
46 # UF_HIDDEN is a stat flag not defined in the stat module.
47 # It is used by BSD to indicate hidden files.
47 # It is used by BSD to indicate hidden files.
48 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
48 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 # parts tornado cares about haven't really changed, so it's OK to continue
55 # parts tornado cares about haven't really changed, so it's OK to continue
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 # version as of Oct 30/2011) the version check fails, see the issue report:
57 # version as of Oct 30/2011) the version check fails, see the issue report:
58
58
59 # https://github.com/facebook/tornado/issues/385
59 # https://github.com/facebook/tornado/issues/385
60
60
61 # This issue has been fixed in Tornado post 2.1.1:
61 # This issue has been fixed in Tornado post 2.1.1:
62
62
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64
64
65 # Here we manually apply the same patch as above so that users of IPython can
65 # Here we manually apply the same patch as above so that users of IPython can
66 # continue to work with an officially released Tornado. We make the
66 # continue to work with an officially released Tornado. We make the
67 # monkeypatch version check as narrow as possible to limit its effects; once
67 # monkeypatch version check as narrow as possible to limit its effects; once
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69
69
70 import tornado
70 import tornado
71
71
72 if tornado.version_info <= (2,1,1):
72 if tornado.version_info <= (2,1,1):
73
73
74 def _execute(self, transforms, *args, **kwargs):
74 def _execute(self, transforms, *args, **kwargs):
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76
76
77 self.open_args = args
77 self.open_args = args
78 self.open_kwargs = kwargs
78 self.open_kwargs = kwargs
79
79
80 # The difference between version 8 and 13 is that in 8 the
80 # The difference between version 8 and 13 is that in 8 the
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 # simply "Origin".
82 # simply "Origin".
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 self.ws_connection = WebSocketProtocol8(self)
84 self.ws_connection = WebSocketProtocol8(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 elif self.request.headers.get("Sec-WebSocket-Version"):
87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 self.stream.write(tornado.escape.utf8(
88 self.stream.write(tornado.escape.utf8(
89 "HTTP/1.1 426 Upgrade Required\r\n"
89 "HTTP/1.1 426 Upgrade Required\r\n"
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 self.stream.close()
91 self.stream.close()
92
92
93 else:
93 else:
94 self.ws_connection = WebSocketProtocol76(self)
94 self.ws_connection = WebSocketProtocol76(self)
95 self.ws_connection.accept_connection()
95 self.ws_connection.accept_connection()
96
96
97 websocket.WebSocketHandler._execute = _execute
97 websocket.WebSocketHandler._execute = _execute
98 del _execute
98 del _execute
99
99
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # Top-level handlers
102 # Top-level handlers
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 class RequestHandler(web.RequestHandler):
105 class RequestHandler(web.RequestHandler):
106 """RequestHandler with default variable setting."""
106 """RequestHandler with default variable setting."""
107
107
108 def render(*args, **kwargs):
108 def render(*args, **kwargs):
109 kwargs.setdefault('message', '')
109 kwargs.setdefault('message', '')
110 return web.RequestHandler.render(*args, **kwargs)
110 return web.RequestHandler.render(*args, **kwargs)
111
111
112 class AuthenticatedHandler(RequestHandler):
112 class AuthenticatedHandler(RequestHandler):
113 """A RequestHandler with an authenticated user."""
113 """A RequestHandler with an authenticated user."""
114
114
115 def clear_login_cookie(self):
115 def clear_login_cookie(self):
116 self.clear_cookie(self.cookie_name)
116 self.clear_cookie(self.cookie_name)
117
117
118 def get_current_user(self):
118 def get_current_user(self):
119 user_id = self.get_secure_cookie(self.cookie_name)
119 user_id = self.get_secure_cookie(self.cookie_name)
120 # For now the user_id should not return empty, but it could eventually
120 # For now the user_id should not return empty, but it could eventually
121 if user_id == '':
121 if user_id == '':
122 user_id = 'anonymous'
122 user_id = 'anonymous'
123 if user_id is None:
123 if user_id is None:
124 # prevent extra Invalid cookie sig warnings:
124 # prevent extra Invalid cookie sig warnings:
125 self.clear_login_cookie()
125 self.clear_login_cookie()
126 if not self.login_available:
126 if not self.login_available:
127 user_id = 'anonymous'
127 user_id = 'anonymous'
128 return user_id
128 return user_id
129
129
130 @property
130 @property
131 def cookie_name(self):
131 def cookie_name(self):
132 default_cookie_name = 'username-{host}'.format(
132 default_cookie_name = 'username-{host}'.format(
133 host=self.request.host,
133 host=self.request.host,
134 ).replace(':', '-')
134 ).replace(':', '-')
135 return self.settings.get('cookie_name', default_cookie_name)
135 return self.settings.get('cookie_name', default_cookie_name)
136
136
137 @property
137 @property
138 def password(self):
138 def password(self):
139 """our password"""
139 """our password"""
140 return self.settings.get('password', '')
140 return self.settings.get('password', '')
141
141
142 @property
142 @property
143 def logged_in(self):
143 def logged_in(self):
144 """Is a user currently logged in?
144 """Is a user currently logged in?
145
145
146 """
146 """
147 user = self.get_current_user()
147 user = self.get_current_user()
148 return (user and not user == 'anonymous')
148 return (user and not user == 'anonymous')
149
149
150 @property
150 @property
151 def login_available(self):
151 def login_available(self):
152 """May a user proceed to log in?
152 """May a user proceed to log in?
153
153
154 This returns True if login capability is available, irrespective of
154 This returns True if login capability is available, irrespective of
155 whether the user is already logged in or not.
155 whether the user is already logged in or not.
156
156
157 """
157 """
158 return bool(self.settings.get('password', ''))
158 return bool(self.settings.get('password', ''))
159
159
160
160
161 class IPythonHandler(AuthenticatedHandler):
161 class IPythonHandler(AuthenticatedHandler):
162 """IPython-specific extensions to authenticated handling
162 """IPython-specific extensions to authenticated handling
163
163
164 Mostly property shortcuts to IPython-specific settings.
164 Mostly property shortcuts to IPython-specific settings.
165 """
165 """
166
166
167 @property
167 @property
168 def config(self):
168 def config(self):
169 return self.settings.get('config', None)
169 return self.settings.get('config', None)
170
170
171 @property
171 @property
172 def log(self):
172 def log(self):
173 """use the IPython log by default, falling back on tornado's logger"""
173 """use the IPython log by default, falling back on tornado's logger"""
174 if Application.initialized():
174 if Application.initialized():
175 return Application.instance().log
175 return Application.instance().log
176 else:
176 else:
177 return app_log
177 return app_log
178
178
179 @property
179 @property
180 def use_less(self):
180 def use_less(self):
181 """Use less instead of css in templates"""
181 """Use less instead of css in templates"""
182 return self.settings.get('use_less', False)
182 return self.settings.get('use_less', False)
183
183
184 #---------------------------------------------------------------
184 #---------------------------------------------------------------
185 # URLs
185 # URLs
186 #---------------------------------------------------------------
186 #---------------------------------------------------------------
187
187
188 @property
188 @property
189 def ws_url(self):
189 def ws_url(self):
190 """websocket url matching the current request
190 """websocket url matching the current request
191
191
192 By default, this is just `''`, indicating that it should match
192 By default, this is just `''`, indicating that it should match
193 the same host, protocol, port, etc.
193 the same host, protocol, port, etc.
194 """
194 """
195 return self.settings.get('websocket_url', '')
195 return self.settings.get('websocket_url', '')
196
196
197 @property
197 @property
198 def mathjax_url(self):
198 def mathjax_url(self):
199 return self.settings.get('mathjax_url', '')
199 return self.settings.get('mathjax_url', '')
200
200
201 @property
201 @property
202 def base_project_url(self):
202 def base_project_url(self):
203 return self.settings.get('base_project_url', '/')
203 return self.settings.get('base_project_url', '/')
204
204
205 @property
205 @property
206 def base_kernel_url(self):
206 def base_kernel_url(self):
207 return self.settings.get('base_kernel_url', '/')
207 return self.settings.get('base_kernel_url', '/')
208
208
209 #---------------------------------------------------------------
209 #---------------------------------------------------------------
210 # Manager objects
210 # Manager objects
211 #---------------------------------------------------------------
211 #---------------------------------------------------------------
212
212
213 @property
213 @property
214 def kernel_manager(self):
214 def kernel_manager(self):
215 return self.settings['kernel_manager']
215 return self.settings['kernel_manager']
216
216
217 @property
217 @property
218 def notebook_manager(self):
218 def notebook_manager(self):
219 return self.settings['notebook_manager']
219 return self.settings['notebook_manager']
220
220
221 @property
221 @property
222 def cluster_manager(self):
222 def cluster_manager(self):
223 return self.settings['cluster_manager']
223 return self.settings['cluster_manager']
224
224
225 @property
225 @property
226 def session_manager(self):
226 def session_manager(self):
227 return self.settings['session_manager']
227 return self.settings['session_manager']
228
228
229 @property
229 @property
230 def project_dir(self):
230 def project_dir(self):
231 return self.notebook_manager.notebook_dir
231 return self.notebook_manager.notebook_dir
232
232
233 #---------------------------------------------------------------
233 #---------------------------------------------------------------
234 # template rendering
234 # template rendering
235 #---------------------------------------------------------------
235 #---------------------------------------------------------------
236
236
237 def get_template(self, name):
237 def get_template(self, name):
238 """Return the jinja template object for a given name"""
238 """Return the jinja template object for a given name"""
239 return self.settings['jinja2_env'].get_template(name)
239 return self.settings['jinja2_env'].get_template(name)
240
240
241 def render_template(self, name, **ns):
241 def render_template(self, name, **ns):
242 ns.update(self.template_namespace)
242 ns.update(self.template_namespace)
243 template = self.get_template(name)
243 template = self.get_template(name)
244 return template.render(**ns)
244 return template.render(**ns)
245
245
246 @property
246 @property
247 def template_namespace(self):
247 def template_namespace(self):
248 return dict(
248 return dict(
249 base_project_url=self.base_project_url,
249 base_project_url=self.base_project_url,
250 base_kernel_url=self.base_kernel_url,
250 base_kernel_url=self.base_kernel_url,
251 logged_in=self.logged_in,
251 logged_in=self.logged_in,
252 login_available=self.login_available,
252 login_available=self.login_available,
253 use_less=self.use_less,
253 use_less=self.use_less,
254 )
254 )
255
255
256 def get_json_body(self):
256 def get_json_body(self):
257 """Return the body of the request as JSON data."""
257 """Return the body of the request as JSON data."""
258 if not self.request.body:
258 if not self.request.body:
259 return None
259 return None
260 # Do we need to call body.decode('utf-8') here?
260 # Do we need to call body.decode('utf-8') here?
261 body = self.request.body.strip().decode(u'utf-8')
261 body = self.request.body.strip().decode(u'utf-8')
262 try:
262 try:
263 model = json.loads(body)
263 model = json.loads(body)
264 except Exception:
264 except Exception:
265 self.log.debug("Bad JSON: %r", body)
265 self.log.debug("Bad JSON: %r", body)
266 self.log.error("Couldn't parse JSON", exc_info=True)
266 self.log.error("Couldn't parse JSON", exc_info=True)
267 raise web.HTTPError(400, u'Invalid JSON in body of request')
267 raise web.HTTPError(400, u'Invalid JSON in body of request')
268 return model
268 return model
269
269
270
270
271 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
271 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
272 """static files should only be accessible when logged in"""
272 """static files should only be accessible when logged in"""
273
273
274 @web.authenticated
274 @web.authenticated
275 def get(self, path):
275 def get(self, path):
276 if os.path.splitext(path)[1] == '.ipynb':
276 if os.path.splitext(path)[1] == '.ipynb':
277 name = os.path.basename(path)
277 name = os.path.basename(path)
278 self.set_header('Content-Type', 'application/json')
278 self.set_header('Content-Type', 'application/json')
279 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
279 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
280
280
281 return web.StaticFileHandler.get(self, path)
281 return web.StaticFileHandler.get(self, path)
282
282
283 def validate_absolute_path(self, root, absolute_path):
283 def validate_absolute_path(self, root, absolute_path):
284 """Validate and return the absolute path.
284 """Validate and return the absolute path.
285
285
286 Requires tornado 3.1
286 Requires tornado 3.1
287
287
288 Adding to tornado's own handling, forbids the serving of hidden files.
288 Adding to tornado's own handling, forbids the serving of hidden files.
289 """
289 """
290 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
290 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
291 abs_root = os.path.abspath(root)
291 abs_root = os.path.abspath(root)
292 self.forbid_hidden(abs_root, abs_path)
292 self.forbid_hidden(abs_root, abs_path)
293 return abs_path
293 return abs_path
294
294
295 def forbid_hidden(self, absolute_root, absolute_path):
295 def forbid_hidden(self, absolute_root, absolute_path):
296 """Raise 403 if a file is hidden or contained in a hidden directory.
296 """Raise 403 if a file is hidden or contained in a hidden directory.
297
297
298 Hidden is determined by either name starting with '.'
298 Hidden is determined by either name starting with '.'
299 or the UF_HIDDEN flag as reported by stat
299 or the UF_HIDDEN flag as reported by stat
300 """
300 """
301 inside_root = absolute_path[len(absolute_root):]
301 inside_root = absolute_path[len(absolute_root):]
302 if any(part.startswith('.') for part in inside_root.split(os.sep)):
302 if any(part.startswith('.') for part in inside_root.split(os.sep)):
303 raise web.HTTPError(403)
303 raise web.HTTPError(403)
304
304
305 # check UF_HIDDEN on any location up to root
305 # check UF_HIDDEN on any location up to root
306 path = absolute_path
306 path = absolute_path
307 while path and path.startswith(absolute_root):
307 while path and path.startswith(absolute_root) and path != absolute_root:
308 st = os.stat(path)
308 st = os.stat(path)
309 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
309 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
310 raise web.HTTPError(403)
310 raise web.HTTPError(403)
311 path, _ = os.path.split(path)
311 path = os.path.dirname(path)
312
312
313 return absolute_path
313 return absolute_path
314
314
315
315
316 def json_errors(method):
316 def json_errors(method):
317 """Decorate methods with this to return GitHub style JSON errors.
317 """Decorate methods with this to return GitHub style JSON errors.
318
318
319 This should be used on any JSON API on any handler method that can raise HTTPErrors.
319 This should be used on any JSON API on any handler method that can raise HTTPErrors.
320
320
321 This will grab the latest HTTPError exception using sys.exc_info
321 This will grab the latest HTTPError exception using sys.exc_info
322 and then:
322 and then:
323
323
324 1. Set the HTTP status code based on the HTTPError
324 1. Set the HTTP status code based on the HTTPError
325 2. Create and return a JSON body with a message field describing
325 2. Create and return a JSON body with a message field describing
326 the error in a human readable form.
326 the error in a human readable form.
327 """
327 """
328 @functools.wraps(method)
328 @functools.wraps(method)
329 def wrapper(self, *args, **kwargs):
329 def wrapper(self, *args, **kwargs):
330 try:
330 try:
331 result = method(self, *args, **kwargs)
331 result = method(self, *args, **kwargs)
332 except web.HTTPError as e:
332 except web.HTTPError as e:
333 status = e.status_code
333 status = e.status_code
334 message = e.log_message
334 message = e.log_message
335 self.set_status(e.status_code)
335 self.set_status(e.status_code)
336 self.finish(json.dumps(dict(message=message)))
336 self.finish(json.dumps(dict(message=message)))
337 except Exception:
337 except Exception:
338 self.log.error("Unhandled error in API request", exc_info=True)
338 self.log.error("Unhandled error in API request", exc_info=True)
339 status = 500
339 status = 500
340 message = "Unknown server error"
340 message = "Unknown server error"
341 t, value, tb = sys.exc_info()
341 t, value, tb = sys.exc_info()
342 self.set_status(status)
342 self.set_status(status)
343 tb_text = ''.join(traceback.format_exception(t, value, tb))
343 tb_text = ''.join(traceback.format_exception(t, value, tb))
344 reply = dict(message=message, traceback=tb_text)
344 reply = dict(message=message, traceback=tb_text)
345 self.finish(json.dumps(reply))
345 self.finish(json.dumps(reply))
346 else:
346 else:
347 return result
347 return result
348 return wrapper
348 return wrapper
349
349
350
350
351
351
352 #-----------------------------------------------------------------------------
352 #-----------------------------------------------------------------------------
353 # File handler
353 # File handler
354 #-----------------------------------------------------------------------------
354 #-----------------------------------------------------------------------------
355
355
356 # to minimize subclass changes:
356 # to minimize subclass changes:
357 HTTPError = web.HTTPError
357 HTTPError = web.HTTPError
358
358
359 class FileFindHandler(web.StaticFileHandler):
359 class FileFindHandler(web.StaticFileHandler):
360 """subclass of StaticFileHandler for serving files from a search path"""
360 """subclass of StaticFileHandler for serving files from a search path"""
361
361
362 _static_paths = {}
362 _static_paths = {}
363 # _lock is needed for tornado < 2.2.0 compat
363 # _lock is needed for tornado < 2.2.0 compat
364 _lock = threading.Lock() # protects _static_hashes
364 _lock = threading.Lock() # protects _static_hashes
365
365
366 def initialize(self, path, default_filename=None):
366 def initialize(self, path, default_filename=None):
367 if isinstance(path, basestring):
367 if isinstance(path, basestring):
368 path = [path]
368 path = [path]
369 self.roots = tuple(
369 self.roots = tuple(
370 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
370 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
371 )
371 )
372 self.default_filename = default_filename
372 self.default_filename = default_filename
373
373
374 @classmethod
374 @classmethod
375 def locate_file(cls, path, roots):
375 def locate_file(cls, path, roots):
376 """locate a file to serve on our static file search path"""
376 """locate a file to serve on our static file search path"""
377 with cls._lock:
377 with cls._lock:
378 if path in cls._static_paths:
378 if path in cls._static_paths:
379 return cls._static_paths[path]
379 return cls._static_paths[path]
380 try:
380 try:
381 abspath = os.path.abspath(filefind(path, roots))
381 abspath = os.path.abspath(filefind(path, roots))
382 except IOError:
382 except IOError:
383 # empty string should always give exists=False
383 # empty string should always give exists=False
384 return ''
384 return ''
385
385
386 # os.path.abspath strips a trailing /
386 # os.path.abspath strips a trailing /
387 # it needs to be temporarily added back for requests to root/
387 # it needs to be temporarily added back for requests to root/
388 if not (abspath + os.sep).startswith(roots):
388 if not (abspath + os.sep).startswith(roots):
389 raise HTTPError(403, "%s is not in root static directory", path)
389 raise HTTPError(403, "%s is not in root static directory", path)
390
390
391 cls._static_paths[path] = abspath
391 cls._static_paths[path] = abspath
392 return abspath
392 return abspath
393
393
394 def get(self, path, include_body=True):
394 def get(self, path, include_body=True):
395 path = self.parse_url_path(path)
395 path = self.parse_url_path(path)
396
396
397 # begin subclass override
397 # begin subclass override
398 abspath = self.locate_file(path, self.roots)
398 abspath = self.locate_file(path, self.roots)
399 # end subclass override
399 # end subclass override
400
400
401 if os.path.isdir(abspath) and self.default_filename is not None:
401 if os.path.isdir(abspath) and self.default_filename is not None:
402 # need to look at the request.path here for when path is empty
402 # need to look at the request.path here for when path is empty
403 # but there is some prefix to the path that was already
403 # but there is some prefix to the path that was already
404 # trimmed by the routing
404 # trimmed by the routing
405 if not self.request.path.endswith("/"):
405 if not self.request.path.endswith("/"):
406 self.redirect(self.request.path + "/")
406 self.redirect(self.request.path + "/")
407 return
407 return
408 abspath = os.path.join(abspath, self.default_filename)
408 abspath = os.path.join(abspath, self.default_filename)
409 if not os.path.exists(abspath):
409 if not os.path.exists(abspath):
410 raise HTTPError(404)
410 raise HTTPError(404)
411 if not os.path.isfile(abspath):
411 if not os.path.isfile(abspath):
412 raise HTTPError(403, "%s is not a file", path)
412 raise HTTPError(403, "%s is not a file", path)
413
413
414 stat_result = os.stat(abspath)
414 stat_result = os.stat(abspath)
415 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
415 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
416
416
417 self.set_header("Last-Modified", modified)
417 self.set_header("Last-Modified", modified)
418
418
419 mime_type, encoding = mimetypes.guess_type(abspath)
419 mime_type, encoding = mimetypes.guess_type(abspath)
420 if mime_type:
420 if mime_type:
421 self.set_header("Content-Type", mime_type)
421 self.set_header("Content-Type", mime_type)
422
422
423 cache_time = self.get_cache_time(path, modified, mime_type)
423 cache_time = self.get_cache_time(path, modified, mime_type)
424
424
425 if cache_time > 0:
425 if cache_time > 0:
426 self.set_header("Expires", datetime.datetime.utcnow() + \
426 self.set_header("Expires", datetime.datetime.utcnow() + \
427 datetime.timedelta(seconds=cache_time))
427 datetime.timedelta(seconds=cache_time))
428 self.set_header("Cache-Control", "max-age=" + str(cache_time))
428 self.set_header("Cache-Control", "max-age=" + str(cache_time))
429 else:
429 else:
430 self.set_header("Cache-Control", "public")
430 self.set_header("Cache-Control", "public")
431
431
432 self.set_extra_headers(path)
432 self.set_extra_headers(path)
433
433
434 # Check the If-Modified-Since, and don't send the result if the
434 # Check the If-Modified-Since, and don't send the result if the
435 # content has not been modified
435 # content has not been modified
436 ims_value = self.request.headers.get("If-Modified-Since")
436 ims_value = self.request.headers.get("If-Modified-Since")
437 if ims_value is not None:
437 if ims_value is not None:
438 date_tuple = email.utils.parsedate(ims_value)
438 date_tuple = email.utils.parsedate(ims_value)
439 if_since = datetime.datetime(*date_tuple[:6])
439 if_since = datetime.datetime(*date_tuple[:6])
440 if if_since >= modified:
440 if if_since >= modified:
441 self.set_status(304)
441 self.set_status(304)
442 return
442 return
443
443
444 with open(abspath, "rb") as file:
444 with open(abspath, "rb") as file:
445 data = file.read()
445 data = file.read()
446 hasher = hashlib.sha1()
446 hasher = hashlib.sha1()
447 hasher.update(data)
447 hasher.update(data)
448 self.set_header("Etag", '"%s"' % hasher.hexdigest())
448 self.set_header("Etag", '"%s"' % hasher.hexdigest())
449 if include_body:
449 if include_body:
450 self.write(data)
450 self.write(data)
451 else:
451 else:
452 assert self.request.method == "HEAD"
452 assert self.request.method == "HEAD"
453 self.set_header("Content-Length", len(data))
453 self.set_header("Content-Length", len(data))
454
454
455 @classmethod
455 @classmethod
456 def get_version(cls, settings, path):
456 def get_version(cls, settings, path):
457 """Generate the version string to be used in static URLs.
457 """Generate the version string to be used in static URLs.
458
458
459 This method may be overridden in subclasses (but note that it
459 This method may be overridden in subclasses (but note that it
460 is a class method rather than a static method). The default
460 is a class method rather than a static method). The default
461 implementation uses a hash of the file's contents.
461 implementation uses a hash of the file's contents.
462
462
463 ``settings`` is the `Application.settings` dictionary and ``path``
463 ``settings`` is the `Application.settings` dictionary and ``path``
464 is the relative location of the requested asset on the filesystem.
464 is the relative location of the requested asset on the filesystem.
465 The returned value should be a string, or ``None`` if no version
465 The returned value should be a string, or ``None`` if no version
466 could be determined.
466 could be determined.
467 """
467 """
468 # begin subclass override:
468 # begin subclass override:
469 static_paths = settings['static_path']
469 static_paths = settings['static_path']
470 if isinstance(static_paths, basestring):
470 if isinstance(static_paths, basestring):
471 static_paths = [static_paths]
471 static_paths = [static_paths]
472 roots = tuple(
472 roots = tuple(
473 os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths
473 os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths
474 )
474 )
475
475
476 try:
476 try:
477 abs_path = filefind(path, roots)
477 abs_path = filefind(path, roots)
478 except IOError:
478 except IOError:
479 app_log.error("Could not find static file %r", path)
479 app_log.error("Could not find static file %r", path)
480 return None
480 return None
481
481
482 # end subclass override
482 # end subclass override
483
483
484 with cls._lock:
484 with cls._lock:
485 hashes = cls._static_hashes
485 hashes = cls._static_hashes
486 if abs_path not in hashes:
486 if abs_path not in hashes:
487 try:
487 try:
488 f = open(abs_path, "rb")
488 f = open(abs_path, "rb")
489 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
489 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
490 f.close()
490 f.close()
491 except Exception:
491 except Exception:
492 app_log.error("Could not open static file %r", path)
492 app_log.error("Could not open static file %r", path)
493 hashes[abs_path] = None
493 hashes[abs_path] = None
494 hsh = hashes.get(abs_path)
494 hsh = hashes.get(abs_path)
495 if hsh:
495 if hsh:
496 return hsh[:5]
496 return hsh[:5]
497 return None
497 return None
498
498
499
499
500 def parse_url_path(self, url_path):
500 def parse_url_path(self, url_path):
501 """Converts a static URL path into a filesystem path.
501 """Converts a static URL path into a filesystem path.
502
502
503 ``url_path`` is the path component of the URL with
503 ``url_path`` is the path component of the URL with
504 ``static_url_prefix`` removed. The return value should be
504 ``static_url_prefix`` removed. The return value should be
505 filesystem path relative to ``static_path``.
505 filesystem path relative to ``static_path``.
506 """
506 """
507 if os.sep != "/":
507 if os.sep != "/":
508 url_path = url_path.replace("/", os.sep)
508 url_path = url_path.replace("/", os.sep)
509 return url_path
509 return url_path
510
510
511 class TrailingSlashHandler(web.RequestHandler):
511 class TrailingSlashHandler(web.RequestHandler):
512 """Simple redirect handler that strips trailing slashes
512 """Simple redirect handler that strips trailing slashes
513
513
514 This should be the first, highest priority handler.
514 This should be the first, highest priority handler.
515 """
515 """
516
516
517 SUPPORTED_METHODS = ['GET']
517 SUPPORTED_METHODS = ['GET']
518
518
519 def get(self):
519 def get(self):
520 self.redirect(self.request.uri.rstrip('/'))
520 self.redirect(self.request.uri.rstrip('/'))
521
521
522 #-----------------------------------------------------------------------------
522 #-----------------------------------------------------------------------------
523 # URL to handler mappings
523 # URL to handler mappings
524 #-----------------------------------------------------------------------------
524 #-----------------------------------------------------------------------------
525
525
526
526
527 default_handlers = [
527 default_handlers = [
528 (r".*/", TrailingSlashHandler)
528 (r".*/", TrailingSlashHandler)
529 ]
529 ]
General Comments 0
You need to be logged in to leave comments. Login now