##// END OF EJS Templates
Merge pull request #4435 from minrk/tornado-tweaks...
Min RK -
r13335:256a3890 merge
parent child Browse files
Show More
@@ -1,363 +1,363 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 functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import stat
24 import stat
25 import sys
25 import sys
26 import traceback
26 import traceback
27
27
28 from tornado import web
28 from tornado import web
29
29
30 try:
30 try:
31 from tornado.log import app_log
31 from tornado.log import app_log
32 except ImportError:
32 except ImportError:
33 app_log = logging.getLogger()
33 app_log = logging.getLogger()
34
34
35 from IPython.config import Application
35 from IPython.config import Application
36 from IPython.utils.path import filefind
36 from IPython.utils.path import filefind
37
37
38 # UF_HIDDEN is a stat flag not defined in the stat module.
38 # UF_HIDDEN is a stat flag not defined in the stat module.
39 # It is used by BSD to indicate hidden files.
39 # It is used by BSD to indicate hidden files.
40 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
40 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Top-level handlers
43 # Top-level handlers
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 class RequestHandler(web.RequestHandler):
46 class RequestHandler(web.RequestHandler):
47 """RequestHandler with default variable setting."""
47 """RequestHandler with default variable setting."""
48
48
49 def render(*args, **kwargs):
49 def render(*args, **kwargs):
50 kwargs.setdefault('message', '')
50 kwargs.setdefault('message', '')
51 return web.RequestHandler.render(*args, **kwargs)
51 return web.RequestHandler.render(*args, **kwargs)
52
52
53 class AuthenticatedHandler(RequestHandler):
53 class AuthenticatedHandler(RequestHandler):
54 """A RequestHandler with an authenticated user."""
54 """A RequestHandler with an authenticated user."""
55
55
56 def clear_login_cookie(self):
56 def clear_login_cookie(self):
57 self.clear_cookie(self.cookie_name)
57 self.clear_cookie(self.cookie_name)
58
58
59 def get_current_user(self):
59 def get_current_user(self):
60 user_id = self.get_secure_cookie(self.cookie_name)
60 user_id = self.get_secure_cookie(self.cookie_name)
61 # For now the user_id should not return empty, but it could eventually
61 # For now the user_id should not return empty, but it could eventually
62 if user_id == '':
62 if user_id == '':
63 user_id = 'anonymous'
63 user_id = 'anonymous'
64 if user_id is None:
64 if user_id is None:
65 # prevent extra Invalid cookie sig warnings:
65 # prevent extra Invalid cookie sig warnings:
66 self.clear_login_cookie()
66 self.clear_login_cookie()
67 if not self.login_available:
67 if not self.login_available:
68 user_id = 'anonymous'
68 user_id = 'anonymous'
69 return user_id
69 return user_id
70
70
71 @property
71 @property
72 def cookie_name(self):
72 def cookie_name(self):
73 default_cookie_name = 'username-{host}'.format(
73 default_cookie_name = 'username-{host}'.format(
74 host=self.request.host,
74 host=self.request.host,
75 ).replace(':', '-')
75 ).replace(':', '-')
76 return self.settings.get('cookie_name', default_cookie_name)
76 return self.settings.get('cookie_name', default_cookie_name)
77
77
78 @property
78 @property
79 def password(self):
79 def password(self):
80 """our password"""
80 """our password"""
81 return self.settings.get('password', '')
81 return self.settings.get('password', '')
82
82
83 @property
83 @property
84 def logged_in(self):
84 def logged_in(self):
85 """Is a user currently logged in?
85 """Is a user currently logged in?
86
86
87 """
87 """
88 user = self.get_current_user()
88 user = self.get_current_user()
89 return (user and not user == 'anonymous')
89 return (user and not user == 'anonymous')
90
90
91 @property
91 @property
92 def login_available(self):
92 def login_available(self):
93 """May a user proceed to log in?
93 """May a user proceed to log in?
94
94
95 This returns True if login capability is available, irrespective of
95 This returns True if login capability is available, irrespective of
96 whether the user is already logged in or not.
96 whether the user is already logged in or not.
97
97
98 """
98 """
99 return bool(self.settings.get('password', ''))
99 return bool(self.settings.get('password', ''))
100
100
101
101
102 class IPythonHandler(AuthenticatedHandler):
102 class IPythonHandler(AuthenticatedHandler):
103 """IPython-specific extensions to authenticated handling
103 """IPython-specific extensions to authenticated handling
104
104
105 Mostly property shortcuts to IPython-specific settings.
105 Mostly property shortcuts to IPython-specific settings.
106 """
106 """
107
107
108 @property
108 @property
109 def config(self):
109 def config(self):
110 return self.settings.get('config', None)
110 return self.settings.get('config', None)
111
111
112 @property
112 @property
113 def log(self):
113 def log(self):
114 """use the IPython log by default, falling back on tornado's logger"""
114 """use the IPython log by default, falling back on tornado's logger"""
115 if Application.initialized():
115 if Application.initialized():
116 return Application.instance().log
116 return Application.instance().log
117 else:
117 else:
118 return app_log
118 return app_log
119
119
120 @property
120 @property
121 def use_less(self):
121 def use_less(self):
122 """Use less instead of css in templates"""
122 """Use less instead of css in templates"""
123 return self.settings.get('use_less', False)
123 return self.settings.get('use_less', False)
124
124
125 #---------------------------------------------------------------
125 #---------------------------------------------------------------
126 # URLs
126 # URLs
127 #---------------------------------------------------------------
127 #---------------------------------------------------------------
128
128
129 @property
129 @property
130 def ws_url(self):
130 def ws_url(self):
131 """websocket url matching the current request
131 """websocket url matching the current request
132
132
133 By default, this is just `''`, indicating that it should match
133 By default, this is just `''`, indicating that it should match
134 the same host, protocol, port, etc.
134 the same host, protocol, port, etc.
135 """
135 """
136 return self.settings.get('websocket_url', '')
136 return self.settings.get('websocket_url', '')
137
137
138 @property
138 @property
139 def mathjax_url(self):
139 def mathjax_url(self):
140 return self.settings.get('mathjax_url', '')
140 return self.settings.get('mathjax_url', '')
141
141
142 @property
142 @property
143 def base_project_url(self):
143 def base_project_url(self):
144 return self.settings.get('base_project_url', '/')
144 return self.settings.get('base_project_url', '/')
145
145
146 @property
146 @property
147 def base_kernel_url(self):
147 def base_kernel_url(self):
148 return self.settings.get('base_kernel_url', '/')
148 return self.settings.get('base_kernel_url', '/')
149
149
150 #---------------------------------------------------------------
150 #---------------------------------------------------------------
151 # Manager objects
151 # Manager objects
152 #---------------------------------------------------------------
152 #---------------------------------------------------------------
153
153
154 @property
154 @property
155 def kernel_manager(self):
155 def kernel_manager(self):
156 return self.settings['kernel_manager']
156 return self.settings['kernel_manager']
157
157
158 @property
158 @property
159 def notebook_manager(self):
159 def notebook_manager(self):
160 return self.settings['notebook_manager']
160 return self.settings['notebook_manager']
161
161
162 @property
162 @property
163 def cluster_manager(self):
163 def cluster_manager(self):
164 return self.settings['cluster_manager']
164 return self.settings['cluster_manager']
165
165
166 @property
166 @property
167 def session_manager(self):
167 def session_manager(self):
168 return self.settings['session_manager']
168 return self.settings['session_manager']
169
169
170 @property
170 @property
171 def project_dir(self):
171 def project_dir(self):
172 return self.notebook_manager.notebook_dir
172 return self.notebook_manager.notebook_dir
173
173
174 #---------------------------------------------------------------
174 #---------------------------------------------------------------
175 # template rendering
175 # template rendering
176 #---------------------------------------------------------------
176 #---------------------------------------------------------------
177
177
178 def get_template(self, name):
178 def get_template(self, name):
179 """Return the jinja template object for a given name"""
179 """Return the jinja template object for a given name"""
180 return self.settings['jinja2_env'].get_template(name)
180 return self.settings['jinja2_env'].get_template(name)
181
181
182 def render_template(self, name, **ns):
182 def render_template(self, name, **ns):
183 ns.update(self.template_namespace)
183 ns.update(self.template_namespace)
184 template = self.get_template(name)
184 template = self.get_template(name)
185 return template.render(**ns)
185 return template.render(**ns)
186
186
187 @property
187 @property
188 def template_namespace(self):
188 def template_namespace(self):
189 return dict(
189 return dict(
190 base_project_url=self.base_project_url,
190 base_project_url=self.base_project_url,
191 base_kernel_url=self.base_kernel_url,
191 base_kernel_url=self.base_kernel_url,
192 logged_in=self.logged_in,
192 logged_in=self.logged_in,
193 login_available=self.login_available,
193 login_available=self.login_available,
194 use_less=self.use_less,
194 use_less=self.use_less,
195 )
195 )
196
196
197 def get_json_body(self):
197 def get_json_body(self):
198 """Return the body of the request as JSON data."""
198 """Return the body of the request as JSON data."""
199 if not self.request.body:
199 if not self.request.body:
200 return None
200 return None
201 # Do we need to call body.decode('utf-8') here?
201 # Do we need to call body.decode('utf-8') here?
202 body = self.request.body.strip().decode(u'utf-8')
202 body = self.request.body.strip().decode(u'utf-8')
203 try:
203 try:
204 model = json.loads(body)
204 model = json.loads(body)
205 except Exception:
205 except Exception:
206 self.log.debug("Bad JSON: %r", body)
206 self.log.debug("Bad JSON: %r", body)
207 self.log.error("Couldn't parse JSON", exc_info=True)
207 self.log.error("Couldn't parse JSON", exc_info=True)
208 raise web.HTTPError(400, u'Invalid JSON in body of request')
208 raise web.HTTPError(400, u'Invalid JSON in body of request')
209 return model
209 return model
210
210
211
211
212 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
212 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
213 """static files should only be accessible when logged in"""
213 """static files should only be accessible when logged in"""
214
214
215 @web.authenticated
215 @web.authenticated
216 def get(self, path):
216 def get(self, path):
217 if os.path.splitext(path)[1] == '.ipynb':
217 if os.path.splitext(path)[1] == '.ipynb':
218 name = os.path.basename(path)
218 name = os.path.basename(path)
219 self.set_header('Content-Type', 'application/json')
219 self.set_header('Content-Type', 'application/json')
220 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
220 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
221
221
222 return web.StaticFileHandler.get(self, path)
222 return web.StaticFileHandler.get(self, path)
223
223
224 def compute_etag(self):
224 def compute_etag(self):
225 return None
225 return None
226
226
227 def validate_absolute_path(self, root, absolute_path):
227 def validate_absolute_path(self, root, absolute_path):
228 """Validate and return the absolute path.
228 """Validate and return the absolute path.
229
229
230 Requires tornado 3.1
230 Requires tornado 3.1
231
231
232 Adding to tornado's own handling, forbids the serving of hidden files.
232 Adding to tornado's own handling, forbids the serving of hidden files.
233 """
233 """
234 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
234 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
235 abs_root = os.path.abspath(root)
235 abs_root = os.path.abspath(root)
236 self.forbid_hidden(abs_root, abs_path)
236 self.forbid_hidden(abs_root, abs_path)
237 return abs_path
237 return abs_path
238
238
239 def forbid_hidden(self, absolute_root, absolute_path):
239 def forbid_hidden(self, absolute_root, absolute_path):
240 """Raise 403 if a file is hidden or contained in a hidden directory.
240 """Raise 403 if a file is hidden or contained in a hidden directory.
241
241
242 Hidden is determined by either name starting with '.'
242 Hidden is determined by either name starting with '.'
243 or the UF_HIDDEN flag as reported by stat
243 or the UF_HIDDEN flag as reported by stat
244 """
244 """
245 inside_root = absolute_path[len(absolute_root):]
245 inside_root = absolute_path[len(absolute_root):]
246 if any(part.startswith('.') for part in inside_root.split(os.sep)):
246 if any(part.startswith('.') for part in inside_root.split(os.sep)):
247 raise web.HTTPError(403)
247 raise web.HTTPError(403)
248
248
249 # check UF_HIDDEN on any location up to root
249 # check UF_HIDDEN on any location up to root
250 path = absolute_path
250 path = absolute_path
251 while path and path.startswith(absolute_root) and path != absolute_root:
251 while path and path.startswith(absolute_root) and path != absolute_root:
252 st = os.stat(path)
252 st = os.stat(path)
253 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
253 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
254 raise web.HTTPError(403)
254 raise web.HTTPError(403)
255 path = os.path.dirname(path)
255 path = os.path.dirname(path)
256
256
257 return absolute_path
257 return absolute_path
258
258
259
259
260 def json_errors(method):
260 def json_errors(method):
261 """Decorate methods with this to return GitHub style JSON errors.
261 """Decorate methods with this to return GitHub style JSON errors.
262
262
263 This should be used on any JSON API on any handler method that can raise HTTPErrors.
263 This should be used on any JSON API on any handler method that can raise HTTPErrors.
264
264
265 This will grab the latest HTTPError exception using sys.exc_info
265 This will grab the latest HTTPError exception using sys.exc_info
266 and then:
266 and then:
267
267
268 1. Set the HTTP status code based on the HTTPError
268 1. Set the HTTP status code based on the HTTPError
269 2. Create and return a JSON body with a message field describing
269 2. Create and return a JSON body with a message field describing
270 the error in a human readable form.
270 the error in a human readable form.
271 """
271 """
272 @functools.wraps(method)
272 @functools.wraps(method)
273 def wrapper(self, *args, **kwargs):
273 def wrapper(self, *args, **kwargs):
274 try:
274 try:
275 result = method(self, *args, **kwargs)
275 result = method(self, *args, **kwargs)
276 except web.HTTPError as e:
276 except web.HTTPError as e:
277 status = e.status_code
277 status = e.status_code
278 message = e.log_message
278 message = e.log_message
279 self.set_status(e.status_code)
279 self.set_status(e.status_code)
280 self.finish(json.dumps(dict(message=message)))
280 self.finish(json.dumps(dict(message=message)))
281 except Exception:
281 except Exception:
282 self.log.error("Unhandled error in API request", exc_info=True)
282 self.log.error("Unhandled error in API request", exc_info=True)
283 status = 500
283 status = 500
284 message = "Unknown server error"
284 message = "Unknown server error"
285 t, value, tb = sys.exc_info()
285 t, value, tb = sys.exc_info()
286 self.set_status(status)
286 self.set_status(status)
287 tb_text = ''.join(traceback.format_exception(t, value, tb))
287 tb_text = ''.join(traceback.format_exception(t, value, tb))
288 reply = dict(message=message, traceback=tb_text)
288 reply = dict(message=message, traceback=tb_text)
289 self.finish(json.dumps(reply))
289 self.finish(json.dumps(reply))
290 else:
290 else:
291 return result
291 return result
292 return wrapper
292 return wrapper
293
293
294
294
295
295
296 #-----------------------------------------------------------------------------
296 #-----------------------------------------------------------------------------
297 # File handler
297 # File handler
298 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
299
299
300 # to minimize subclass changes:
300 # to minimize subclass changes:
301 HTTPError = web.HTTPError
301 HTTPError = web.HTTPError
302
302
303 class FileFindHandler(web.StaticFileHandler):
303 class FileFindHandler(web.StaticFileHandler):
304 """subclass of StaticFileHandler for serving files from a search path"""
304 """subclass of StaticFileHandler for serving files from a search path"""
305
305
306 # cache search results, don't search for files more than once
306 # cache search results, don't search for files more than once
307 _static_paths = {}
307 _static_paths = {}
308
308
309 def initialize(self, path, default_filename=None):
309 def initialize(self, path, default_filename=None):
310 if isinstance(path, basestring):
310 if isinstance(path, basestring):
311 path = [path]
311 path = [path]
312
312
313 self.root = tuple(
313 self.root = tuple(
314 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
314 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
315 )
315 )
316 self.default_filename = default_filename
316 self.default_filename = default_filename
317
317
318 def compute_etag(self):
318 def compute_etag(self):
319 return None
319 return None
320
320
321 @classmethod
321 @classmethod
322 def get_absolute_path(cls, roots, path):
322 def get_absolute_path(cls, roots, path):
323 """locate a file to serve on our static file search path"""
323 """locate a file to serve on our static file search path"""
324 with cls._lock:
324 with cls._lock:
325 if path in cls._static_paths:
325 if path in cls._static_paths:
326 return cls._static_paths[path]
326 return cls._static_paths[path]
327 try:
327 try:
328 abspath = os.path.abspath(filefind(path, roots))
328 abspath = os.path.abspath(filefind(path, roots))
329 except IOError:
329 except IOError:
330 # empty string should always give exists=False
330 # IOError means not found
331 return ''
331 raise web.HTTPError(404)
332
332
333 cls._static_paths[path] = abspath
333 cls._static_paths[path] = abspath
334 return abspath
334 return abspath
335
335
336 def validate_absolute_path(self, root, absolute_path):
336 def validate_absolute_path(self, root, absolute_path):
337 """check if the file should be served (raises 404, 403, etc.)"""
337 """check if the file should be served (raises 404, 403, etc.)"""
338 for root in self.root:
338 for root in self.root:
339 if (absolute_path + os.sep).startswith(root):
339 if (absolute_path + os.sep).startswith(root):
340 break
340 break
341
341
342 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
342 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
343
343
344
344
345 class TrailingSlashHandler(web.RequestHandler):
345 class TrailingSlashHandler(web.RequestHandler):
346 """Simple redirect handler that strips trailing slashes
346 """Simple redirect handler that strips trailing slashes
347
347
348 This should be the first, highest priority handler.
348 This should be the first, highest priority handler.
349 """
349 """
350
350
351 SUPPORTED_METHODS = ['GET']
351 SUPPORTED_METHODS = ['GET']
352
352
353 def get(self):
353 def get(self):
354 self.redirect(self.request.uri.rstrip('/'))
354 self.redirect(self.request.uri.rstrip('/'))
355
355
356 #-----------------------------------------------------------------------------
356 #-----------------------------------------------------------------------------
357 # URL to handler mappings
357 # URL to handler mappings
358 #-----------------------------------------------------------------------------
358 #-----------------------------------------------------------------------------
359
359
360
360
361 default_handlers = [
361 default_handlers = [
362 (r".*/", TrailingSlashHandler)
362 (r".*/", TrailingSlashHandler)
363 ]
363 ]
General Comments 0
You need to be logged in to leave comments. Login now