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