##// END OF EJS Templates
use gen.Return for Python 2
Min RK -
Show More
@@ -1,508 +1,509 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 from tornado import gen
22 from tornado import gen
23 from tornado.log import app_log
23 from tornado.log import app_log
24
24
25
25
26 import IPython
26 import IPython
27 from IPython.utils.sysinfo import get_sys_info
27 from IPython.utils.sysinfo import get_sys_info
28
28
29 from IPython.config import Application
29 from IPython.config import Application
30 from IPython.utils.path import filefind
30 from IPython.utils.path import filefind
31 from IPython.utils.py3compat import string_types
31 from IPython.utils.py3compat import string_types
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
33
33
34 from IPython.html.services.security import csp_report_uri
34 from IPython.html.services.security import csp_report_uri
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Top-level handlers
37 # Top-level handlers
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40
40
41 sys_info = json.dumps(get_sys_info())
41 sys_info = json.dumps(get_sys_info())
42
42
43 class AuthenticatedHandler(web.RequestHandler):
43 class AuthenticatedHandler(web.RequestHandler):
44 """A RequestHandler with an authenticated user."""
44 """A RequestHandler with an authenticated user."""
45
45
46 def set_default_headers(self):
46 def set_default_headers(self):
47 headers = self.settings.get('headers', {})
47 headers = self.settings.get('headers', {})
48
48
49 if "Content-Security-Policy" not in headers:
49 if "Content-Security-Policy" not in headers:
50 headers["Content-Security-Policy"] = (
50 headers["Content-Security-Policy"] = (
51 "frame-ancestors 'self'; "
51 "frame-ancestors 'self'; "
52 # Make sure the report-uri is relative to the base_url
52 # Make sure the report-uri is relative to the base_url
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 )
54 )
55
55
56 # Allow for overriding headers
56 # Allow for overriding headers
57 for header_name,value in headers.items() :
57 for header_name,value in headers.items() :
58 try:
58 try:
59 self.set_header(header_name, value)
59 self.set_header(header_name, value)
60 except Exception as e:
60 except Exception as e:
61 # tornado raise Exception (not a subclass)
61 # tornado raise Exception (not a subclass)
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # for example, so just ignore)
63 # for example, so just ignore)
64 self.log.debug(e)
64 self.log.debug(e)
65
65
66 def clear_login_cookie(self):
66 def clear_login_cookie(self):
67 self.clear_cookie(self.cookie_name)
67 self.clear_cookie(self.cookie_name)
68
68
69 def get_current_user(self):
69 def get_current_user(self):
70 if self.login_handler is None:
70 if self.login_handler is None:
71 return 'anonymous'
71 return 'anonymous'
72 return self.login_handler.get_user(self)
72 return self.login_handler.get_user(self)
73
73
74 @property
74 @property
75 def cookie_name(self):
75 def cookie_name(self):
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 self.request.host
77 self.request.host
78 ))
78 ))
79 return self.settings.get('cookie_name', default_cookie_name)
79 return self.settings.get('cookie_name', default_cookie_name)
80
80
81 @property
81 @property
82 def logged_in(self):
82 def logged_in(self):
83 """Is a user currently logged in?"""
83 """Is a user currently logged in?"""
84 user = self.get_current_user()
84 user = self.get_current_user()
85 return (user and not user == 'anonymous')
85 return (user and not user == 'anonymous')
86
86
87 @property
87 @property
88 def login_handler(self):
88 def login_handler(self):
89 """Return the login handler for this application, if any."""
89 """Return the login handler for this application, if any."""
90 return self.settings.get('login_handler_class', None)
90 return self.settings.get('login_handler_class', None)
91
91
92 @property
92 @property
93 def login_available(self):
93 def login_available(self):
94 """May a user proceed to log in?
94 """May a user proceed to log in?
95
95
96 This returns True if login capability is available, irrespective of
96 This returns True if login capability is available, irrespective of
97 whether the user is already logged in or not.
97 whether the user is already logged in or not.
98
98
99 """
99 """
100 if self.login_handler is None:
100 if self.login_handler is None:
101 return False
101 return False
102 return bool(self.login_handler.login_available(self.settings))
102 return bool(self.login_handler.login_available(self.settings))
103
103
104
104
105 class IPythonHandler(AuthenticatedHandler):
105 class IPythonHandler(AuthenticatedHandler):
106 """IPython-specific extensions to authenticated handling
106 """IPython-specific extensions to authenticated handling
107
107
108 Mostly property shortcuts to IPython-specific settings.
108 Mostly property shortcuts to IPython-specific settings.
109 """
109 """
110
110
111 @property
111 @property
112 def config(self):
112 def config(self):
113 return self.settings.get('config', None)
113 return self.settings.get('config', None)
114
114
115 @property
115 @property
116 def log(self):
116 def log(self):
117 """use the IPython log by default, falling back on tornado's logger"""
117 """use the IPython log by default, falling back on tornado's logger"""
118 if Application.initialized():
118 if Application.initialized():
119 return Application.instance().log
119 return Application.instance().log
120 else:
120 else:
121 return app_log
121 return app_log
122
122
123 #---------------------------------------------------------------
123 #---------------------------------------------------------------
124 # URLs
124 # URLs
125 #---------------------------------------------------------------
125 #---------------------------------------------------------------
126
126
127 @property
127 @property
128 def version_hash(self):
128 def version_hash(self):
129 """The version hash to use for cache hints for static files"""
129 """The version hash to use for cache hints for static files"""
130 return self.settings.get('version_hash', '')
130 return self.settings.get('version_hash', '')
131
131
132 @property
132 @property
133 def mathjax_url(self):
133 def mathjax_url(self):
134 return self.settings.get('mathjax_url', '')
134 return self.settings.get('mathjax_url', '')
135
135
136 @property
136 @property
137 def base_url(self):
137 def base_url(self):
138 return self.settings.get('base_url', '/')
138 return self.settings.get('base_url', '/')
139
139
140 @property
140 @property
141 def ws_url(self):
141 def ws_url(self):
142 return self.settings.get('websocket_url', '')
142 return self.settings.get('websocket_url', '')
143
143
144 @property
144 @property
145 def contents_js_source(self):
145 def contents_js_source(self):
146 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
146 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
147 'services/contents'))
147 'services/contents'))
148 return self.settings.get('contents_js_source', 'services/contents')
148 return self.settings.get('contents_js_source', 'services/contents')
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 contents_manager(self):
159 def contents_manager(self):
160 return self.settings['contents_manager']
160 return self.settings['contents_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 terminal_manager(self):
171 def terminal_manager(self):
172 return self.settings['terminal_manager']
172 return self.settings['terminal_manager']
173
173
174 @property
174 @property
175 def kernel_spec_manager(self):
175 def kernel_spec_manager(self):
176 return self.settings['kernel_spec_manager']
176 return self.settings['kernel_spec_manager']
177
177
178 @property
178 @property
179 def config_manager(self):
179 def config_manager(self):
180 return self.settings['config_manager']
180 return self.settings['config_manager']
181
181
182 #---------------------------------------------------------------
182 #---------------------------------------------------------------
183 # CORS
183 # CORS
184 #---------------------------------------------------------------
184 #---------------------------------------------------------------
185
185
186 @property
186 @property
187 def allow_origin(self):
187 def allow_origin(self):
188 """Normal Access-Control-Allow-Origin"""
188 """Normal Access-Control-Allow-Origin"""
189 return self.settings.get('allow_origin', '')
189 return self.settings.get('allow_origin', '')
190
190
191 @property
191 @property
192 def allow_origin_pat(self):
192 def allow_origin_pat(self):
193 """Regular expression version of allow_origin"""
193 """Regular expression version of allow_origin"""
194 return self.settings.get('allow_origin_pat', None)
194 return self.settings.get('allow_origin_pat', None)
195
195
196 @property
196 @property
197 def allow_credentials(self):
197 def allow_credentials(self):
198 """Whether to set Access-Control-Allow-Credentials"""
198 """Whether to set Access-Control-Allow-Credentials"""
199 return self.settings.get('allow_credentials', False)
199 return self.settings.get('allow_credentials', False)
200
200
201 def set_default_headers(self):
201 def set_default_headers(self):
202 """Add CORS headers, if defined"""
202 """Add CORS headers, if defined"""
203 super(IPythonHandler, self).set_default_headers()
203 super(IPythonHandler, self).set_default_headers()
204 if self.allow_origin:
204 if self.allow_origin:
205 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
205 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
206 elif self.allow_origin_pat:
206 elif self.allow_origin_pat:
207 origin = self.get_origin()
207 origin = self.get_origin()
208 if origin and self.allow_origin_pat.match(origin):
208 if origin and self.allow_origin_pat.match(origin):
209 self.set_header("Access-Control-Allow-Origin", origin)
209 self.set_header("Access-Control-Allow-Origin", origin)
210 if self.allow_credentials:
210 if self.allow_credentials:
211 self.set_header("Access-Control-Allow-Credentials", 'true')
211 self.set_header("Access-Control-Allow-Credentials", 'true')
212
212
213 def get_origin(self):
213 def get_origin(self):
214 # Handle WebSocket Origin naming convention differences
214 # Handle WebSocket Origin naming convention differences
215 # The difference between version 8 and 13 is that in 8 the
215 # The difference between version 8 and 13 is that in 8 the
216 # client sends a "Sec-Websocket-Origin" header and in 13 it's
216 # client sends a "Sec-Websocket-Origin" header and in 13 it's
217 # simply "Origin".
217 # simply "Origin".
218 if "Origin" in self.request.headers:
218 if "Origin" in self.request.headers:
219 origin = self.request.headers.get("Origin")
219 origin = self.request.headers.get("Origin")
220 else:
220 else:
221 origin = self.request.headers.get("Sec-Websocket-Origin", None)
221 origin = self.request.headers.get("Sec-Websocket-Origin", None)
222 return origin
222 return origin
223
223
224 #---------------------------------------------------------------
224 #---------------------------------------------------------------
225 # template rendering
225 # template rendering
226 #---------------------------------------------------------------
226 #---------------------------------------------------------------
227
227
228 def get_template(self, name):
228 def get_template(self, name):
229 """Return the jinja template object for a given name"""
229 """Return the jinja template object for a given name"""
230 return self.settings['jinja2_env'].get_template(name)
230 return self.settings['jinja2_env'].get_template(name)
231
231
232 def render_template(self, name, **ns):
232 def render_template(self, name, **ns):
233 ns.update(self.template_namespace)
233 ns.update(self.template_namespace)
234 template = self.get_template(name)
234 template = self.get_template(name)
235 return template.render(**ns)
235 return template.render(**ns)
236
236
237 @property
237 @property
238 def template_namespace(self):
238 def template_namespace(self):
239 return dict(
239 return dict(
240 base_url=self.base_url,
240 base_url=self.base_url,
241 ws_url=self.ws_url,
241 ws_url=self.ws_url,
242 logged_in=self.logged_in,
242 logged_in=self.logged_in,
243 login_available=self.login_available,
243 login_available=self.login_available,
244 static_url=self.static_url,
244 static_url=self.static_url,
245 sys_info=sys_info,
245 sys_info=sys_info,
246 contents_js_source=self.contents_js_source,
246 contents_js_source=self.contents_js_source,
247 version_hash=self.version_hash,
247 version_hash=self.version_hash,
248 )
248 )
249
249
250 def get_json_body(self):
250 def get_json_body(self):
251 """Return the body of the request as JSON data."""
251 """Return the body of the request as JSON data."""
252 if not self.request.body:
252 if not self.request.body:
253 return None
253 return None
254 # Do we need to call body.decode('utf-8') here?
254 # Do we need to call body.decode('utf-8') here?
255 body = self.request.body.strip().decode(u'utf-8')
255 body = self.request.body.strip().decode(u'utf-8')
256 try:
256 try:
257 model = json.loads(body)
257 model = json.loads(body)
258 except Exception:
258 except Exception:
259 self.log.debug("Bad JSON: %r", body)
259 self.log.debug("Bad JSON: %r", body)
260 self.log.error("Couldn't parse JSON", exc_info=True)
260 self.log.error("Couldn't parse JSON", exc_info=True)
261 raise web.HTTPError(400, u'Invalid JSON in body of request')
261 raise web.HTTPError(400, u'Invalid JSON in body of request')
262 return model
262 return model
263
263
264 def write_error(self, status_code, **kwargs):
264 def write_error(self, status_code, **kwargs):
265 """render custom error pages"""
265 """render custom error pages"""
266 exc_info = kwargs.get('exc_info')
266 exc_info = kwargs.get('exc_info')
267 message = ''
267 message = ''
268 status_message = responses.get(status_code, 'Unknown HTTP Error')
268 status_message = responses.get(status_code, 'Unknown HTTP Error')
269 if exc_info:
269 if exc_info:
270 exception = exc_info[1]
270 exception = exc_info[1]
271 # get the custom message, if defined
271 # get the custom message, if defined
272 try:
272 try:
273 message = exception.log_message % exception.args
273 message = exception.log_message % exception.args
274 except Exception:
274 except Exception:
275 pass
275 pass
276
276
277 # construct the custom reason, if defined
277 # construct the custom reason, if defined
278 reason = getattr(exception, 'reason', '')
278 reason = getattr(exception, 'reason', '')
279 if reason:
279 if reason:
280 status_message = reason
280 status_message = reason
281
281
282 # build template namespace
282 # build template namespace
283 ns = dict(
283 ns = dict(
284 status_code=status_code,
284 status_code=status_code,
285 status_message=status_message,
285 status_message=status_message,
286 message=message,
286 message=message,
287 exception=exception,
287 exception=exception,
288 )
288 )
289
289
290 self.set_header('Content-Type', 'text/html')
290 self.set_header('Content-Type', 'text/html')
291 # render the template
291 # render the template
292 try:
292 try:
293 html = self.render_template('%s.html' % status_code, **ns)
293 html = self.render_template('%s.html' % status_code, **ns)
294 except TemplateNotFound:
294 except TemplateNotFound:
295 self.log.debug("No template for %d", status_code)
295 self.log.debug("No template for %d", status_code)
296 html = self.render_template('error.html', **ns)
296 html = self.render_template('error.html', **ns)
297
297
298 self.write(html)
298 self.write(html)
299
299
300
300
301
301
302 class Template404(IPythonHandler):
302 class Template404(IPythonHandler):
303 """Render our 404 template"""
303 """Render our 404 template"""
304 def prepare(self):
304 def prepare(self):
305 raise web.HTTPError(404)
305 raise web.HTTPError(404)
306
306
307
307
308 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
308 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
309 """static files should only be accessible when logged in"""
309 """static files should only be accessible when logged in"""
310
310
311 @web.authenticated
311 @web.authenticated
312 def get(self, path):
312 def get(self, path):
313 if os.path.splitext(path)[1] == '.ipynb':
313 if os.path.splitext(path)[1] == '.ipynb':
314 name = path.rsplit('/', 1)[-1]
314 name = path.rsplit('/', 1)[-1]
315 self.set_header('Content-Type', 'application/json')
315 self.set_header('Content-Type', 'application/json')
316 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
316 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
317
317
318 return web.StaticFileHandler.get(self, path)
318 return web.StaticFileHandler.get(self, path)
319
319
320 def set_headers(self):
320 def set_headers(self):
321 super(AuthenticatedFileHandler, self).set_headers()
321 super(AuthenticatedFileHandler, self).set_headers()
322 # disable browser caching, rely on 304 replies for savings
322 # disable browser caching, rely on 304 replies for savings
323 if "v" not in self.request.arguments:
323 if "v" not in self.request.arguments:
324 self.add_header("Cache-Control", "no-cache")
324 self.add_header("Cache-Control", "no-cache")
325
325
326 def compute_etag(self):
326 def compute_etag(self):
327 return None
327 return None
328
328
329 def validate_absolute_path(self, root, absolute_path):
329 def validate_absolute_path(self, root, absolute_path):
330 """Validate and return the absolute path.
330 """Validate and return the absolute path.
331
331
332 Requires tornado 3.1
332 Requires tornado 3.1
333
333
334 Adding to tornado's own handling, forbids the serving of hidden files.
334 Adding to tornado's own handling, forbids the serving of hidden files.
335 """
335 """
336 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
336 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
337 abs_root = os.path.abspath(root)
337 abs_root = os.path.abspath(root)
338 if is_hidden(abs_path, abs_root):
338 if is_hidden(abs_path, abs_root):
339 self.log.info("Refusing to serve hidden file, via 404 Error")
339 self.log.info("Refusing to serve hidden file, via 404 Error")
340 raise web.HTTPError(404)
340 raise web.HTTPError(404)
341 return abs_path
341 return abs_path
342
342
343
343
344 def json_errors(method):
344 def json_errors(method):
345 """Decorate methods with this to return GitHub style JSON errors.
345 """Decorate methods with this to return GitHub style JSON errors.
346
346
347 This should be used on any JSON API on any handler method that can raise HTTPErrors.
347 This should be used on any JSON API on any handler method that can raise HTTPErrors.
348
348
349 This will grab the latest HTTPError exception using sys.exc_info
349 This will grab the latest HTTPError exception using sys.exc_info
350 and then:
350 and then:
351
351
352 1. Set the HTTP status code based on the HTTPError
352 1. Set the HTTP status code based on the HTTPError
353 2. Create and return a JSON body with a message field describing
353 2. Create and return a JSON body with a message field describing
354 the error in a human readable form.
354 the error in a human readable form.
355 """
355 """
356 @functools.wraps(method)
356 @functools.wraps(method)
357 @gen.coroutine
357 @gen.coroutine
358 def wrapper(self, *args, **kwargs):
358 def wrapper(self, *args, **kwargs):
359 try:
359 try:
360 result = yield gen.maybe_future(method(self, *args, **kwargs))
360 result = yield gen.maybe_future(method(self, *args, **kwargs))
361 except web.HTTPError as e:
361 except web.HTTPError as e:
362 status = e.status_code
362 status = e.status_code
363 message = e.log_message
363 message = e.log_message
364 self.log.warn(message)
364 self.log.warn(message)
365 self.set_status(e.status_code)
365 self.set_status(e.status_code)
366 reply = dict(message=message, reason=e.reason)
366 reply = dict(message=message, reason=e.reason)
367 self.finish(json.dumps(reply))
367 self.finish(json.dumps(reply))
368 except Exception:
368 except Exception:
369 self.log.error("Unhandled error in API request", exc_info=True)
369 self.log.error("Unhandled error in API request", exc_info=True)
370 status = 500
370 status = 500
371 message = "Unknown server error"
371 message = "Unknown server error"
372 t, value, tb = sys.exc_info()
372 t, value, tb = sys.exc_info()
373 self.set_status(status)
373 self.set_status(status)
374 tb_text = ''.join(traceback.format_exception(t, value, tb))
374 tb_text = ''.join(traceback.format_exception(t, value, tb))
375 reply = dict(message=message, reason=None, traceback=tb_text)
375 reply = dict(message=message, reason=None, traceback=tb_text)
376 self.finish(json.dumps(reply))
376 self.finish(json.dumps(reply))
377 else:
377 else:
378 return result
378 # FIXME: can use regular return in generators in py3
379 raise gen.Return(result)
379 return wrapper
380 return wrapper
380
381
381
382
382
383
383 #-----------------------------------------------------------------------------
384 #-----------------------------------------------------------------------------
384 # File handler
385 # File handler
385 #-----------------------------------------------------------------------------
386 #-----------------------------------------------------------------------------
386
387
387 # to minimize subclass changes:
388 # to minimize subclass changes:
388 HTTPError = web.HTTPError
389 HTTPError = web.HTTPError
389
390
390 class FileFindHandler(web.StaticFileHandler):
391 class FileFindHandler(web.StaticFileHandler):
391 """subclass of StaticFileHandler for serving files from a search path"""
392 """subclass of StaticFileHandler for serving files from a search path"""
392
393
393 # cache search results, don't search for files more than once
394 # cache search results, don't search for files more than once
394 _static_paths = {}
395 _static_paths = {}
395
396
396 def set_headers(self):
397 def set_headers(self):
397 super(FileFindHandler, self).set_headers()
398 super(FileFindHandler, self).set_headers()
398 # disable browser caching, rely on 304 replies for savings
399 # disable browser caching, rely on 304 replies for savings
399 if "v" not in self.request.arguments or \
400 if "v" not in self.request.arguments or \
400 any(self.request.path.startswith(path) for path in self.no_cache_paths):
401 any(self.request.path.startswith(path) for path in self.no_cache_paths):
401 self.add_header("Cache-Control", "no-cache")
402 self.add_header("Cache-Control", "no-cache")
402
403
403 def initialize(self, path, default_filename=None, no_cache_paths=None):
404 def initialize(self, path, default_filename=None, no_cache_paths=None):
404 self.no_cache_paths = no_cache_paths or []
405 self.no_cache_paths = no_cache_paths or []
405
406
406 if isinstance(path, string_types):
407 if isinstance(path, string_types):
407 path = [path]
408 path = [path]
408
409
409 self.root = tuple(
410 self.root = tuple(
410 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
411 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
411 )
412 )
412 self.default_filename = default_filename
413 self.default_filename = default_filename
413
414
414 def compute_etag(self):
415 def compute_etag(self):
415 return None
416 return None
416
417
417 @classmethod
418 @classmethod
418 def get_absolute_path(cls, roots, path):
419 def get_absolute_path(cls, roots, path):
419 """locate a file to serve on our static file search path"""
420 """locate a file to serve on our static file search path"""
420 with cls._lock:
421 with cls._lock:
421 if path in cls._static_paths:
422 if path in cls._static_paths:
422 return cls._static_paths[path]
423 return cls._static_paths[path]
423 try:
424 try:
424 abspath = os.path.abspath(filefind(path, roots))
425 abspath = os.path.abspath(filefind(path, roots))
425 except IOError:
426 except IOError:
426 # IOError means not found
427 # IOError means not found
427 return ''
428 return ''
428
429
429 cls._static_paths[path] = abspath
430 cls._static_paths[path] = abspath
430 return abspath
431 return abspath
431
432
432 def validate_absolute_path(self, root, absolute_path):
433 def validate_absolute_path(self, root, absolute_path):
433 """check if the file should be served (raises 404, 403, etc.)"""
434 """check if the file should be served (raises 404, 403, etc.)"""
434 if absolute_path == '':
435 if absolute_path == '':
435 raise web.HTTPError(404)
436 raise web.HTTPError(404)
436
437
437 for root in self.root:
438 for root in self.root:
438 if (absolute_path + os.sep).startswith(root):
439 if (absolute_path + os.sep).startswith(root):
439 break
440 break
440
441
441 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
442 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
442
443
443
444
444 class ApiVersionHandler(IPythonHandler):
445 class ApiVersionHandler(IPythonHandler):
445
446
446 @json_errors
447 @json_errors
447 def get(self):
448 def get(self):
448 # not authenticated, so give as few info as possible
449 # not authenticated, so give as few info as possible
449 self.finish(json.dumps({"version":IPython.__version__}))
450 self.finish(json.dumps({"version":IPython.__version__}))
450
451
451
452
452 class TrailingSlashHandler(web.RequestHandler):
453 class TrailingSlashHandler(web.RequestHandler):
453 """Simple redirect handler that strips trailing slashes
454 """Simple redirect handler that strips trailing slashes
454
455
455 This should be the first, highest priority handler.
456 This should be the first, highest priority handler.
456 """
457 """
457
458
458 def get(self):
459 def get(self):
459 self.redirect(self.request.uri.rstrip('/'))
460 self.redirect(self.request.uri.rstrip('/'))
460
461
461 post = put = get
462 post = put = get
462
463
463
464
464 class FilesRedirectHandler(IPythonHandler):
465 class FilesRedirectHandler(IPythonHandler):
465 """Handler for redirecting relative URLs to the /files/ handler"""
466 """Handler for redirecting relative URLs to the /files/ handler"""
466 def get(self, path=''):
467 def get(self, path=''):
467 cm = self.contents_manager
468 cm = self.contents_manager
468 if cm.dir_exists(path):
469 if cm.dir_exists(path):
469 # it's a *directory*, redirect to /tree
470 # it's a *directory*, redirect to /tree
470 url = url_path_join(self.base_url, 'tree', path)
471 url = url_path_join(self.base_url, 'tree', path)
471 else:
472 else:
472 orig_path = path
473 orig_path = path
473 # otherwise, redirect to /files
474 # otherwise, redirect to /files
474 parts = path.split('/')
475 parts = path.split('/')
475
476
476 if not cm.file_exists(path=path) and 'files' in parts:
477 if not cm.file_exists(path=path) and 'files' in parts:
477 # redirect without files/ iff it would 404
478 # redirect without files/ iff it would 404
478 # this preserves pre-2.0-style 'files/' links
479 # this preserves pre-2.0-style 'files/' links
479 self.log.warn("Deprecated files/ URL: %s", orig_path)
480 self.log.warn("Deprecated files/ URL: %s", orig_path)
480 parts.remove('files')
481 parts.remove('files')
481 path = '/'.join(parts)
482 path = '/'.join(parts)
482
483
483 if not cm.file_exists(path=path):
484 if not cm.file_exists(path=path):
484 raise web.HTTPError(404)
485 raise web.HTTPError(404)
485
486
486 url = url_path_join(self.base_url, 'files', path)
487 url = url_path_join(self.base_url, 'files', path)
487 url = url_escape(url)
488 url = url_escape(url)
488 self.log.debug("Redirecting %s to %s", self.request.path, url)
489 self.log.debug("Redirecting %s to %s", self.request.path, url)
489 self.redirect(url)
490 self.redirect(url)
490
491
491
492
492 #-----------------------------------------------------------------------------
493 #-----------------------------------------------------------------------------
493 # URL pattern fragments for re-use
494 # URL pattern fragments for re-use
494 #-----------------------------------------------------------------------------
495 #-----------------------------------------------------------------------------
495
496
496 # path matches any number of `/foo[/bar...]` or just `/` or ''
497 # path matches any number of `/foo[/bar...]` or just `/` or ''
497 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
498 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
498 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
499 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
499
500
500 #-----------------------------------------------------------------------------
501 #-----------------------------------------------------------------------------
501 # URL to handler mappings
502 # URL to handler mappings
502 #-----------------------------------------------------------------------------
503 #-----------------------------------------------------------------------------
503
504
504
505
505 default_handlers = [
506 default_handlers = [
506 (r".*/", TrailingSlashHandler),
507 (r".*/", TrailingSlashHandler),
507 (r"api", ApiVersionHandler)
508 (r"api", ApiVersionHandler)
508 ]
509 ]
General Comments 0
You need to be logged in to leave comments. Login now