##// END OF EJS Templates
empty default_url when outside IPython
Min RK -
Show More
@@ -1,522 +1,522 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 default_url(self):
141 def default_url(self):
142 return self.settings.get('default_url', '/')
142 return self.settings.get('default_url', '')
143
143
144 @property
144 @property
145 def ws_url(self):
145 def ws_url(self):
146 return self.settings.get('websocket_url', '')
146 return self.settings.get('websocket_url', '')
147
147
148 @property
148 @property
149 def contents_js_source(self):
149 def contents_js_source(self):
150 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
150 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
151 'services/contents'))
151 'services/contents'))
152 return self.settings.get('contents_js_source', 'services/contents')
152 return self.settings.get('contents_js_source', 'services/contents')
153
153
154 #---------------------------------------------------------------
154 #---------------------------------------------------------------
155 # Manager objects
155 # Manager objects
156 #---------------------------------------------------------------
156 #---------------------------------------------------------------
157
157
158 @property
158 @property
159 def kernel_manager(self):
159 def kernel_manager(self):
160 return self.settings['kernel_manager']
160 return self.settings['kernel_manager']
161
161
162 @property
162 @property
163 def contents_manager(self):
163 def contents_manager(self):
164 return self.settings['contents_manager']
164 return self.settings['contents_manager']
165
165
166 @property
166 @property
167 def cluster_manager(self):
167 def cluster_manager(self):
168 return self.settings['cluster_manager']
168 return self.settings['cluster_manager']
169
169
170 @property
170 @property
171 def session_manager(self):
171 def session_manager(self):
172 return self.settings['session_manager']
172 return self.settings['session_manager']
173
173
174 @property
174 @property
175 def terminal_manager(self):
175 def terminal_manager(self):
176 return self.settings['terminal_manager']
176 return self.settings['terminal_manager']
177
177
178 @property
178 @property
179 def kernel_spec_manager(self):
179 def kernel_spec_manager(self):
180 return self.settings['kernel_spec_manager']
180 return self.settings['kernel_spec_manager']
181
181
182 @property
182 @property
183 def config_manager(self):
183 def config_manager(self):
184 return self.settings['config_manager']
184 return self.settings['config_manager']
185
185
186 #---------------------------------------------------------------
186 #---------------------------------------------------------------
187 # CORS
187 # CORS
188 #---------------------------------------------------------------
188 #---------------------------------------------------------------
189
189
190 @property
190 @property
191 def allow_origin(self):
191 def allow_origin(self):
192 """Normal Access-Control-Allow-Origin"""
192 """Normal Access-Control-Allow-Origin"""
193 return self.settings.get('allow_origin', '')
193 return self.settings.get('allow_origin', '')
194
194
195 @property
195 @property
196 def allow_origin_pat(self):
196 def allow_origin_pat(self):
197 """Regular expression version of allow_origin"""
197 """Regular expression version of allow_origin"""
198 return self.settings.get('allow_origin_pat', None)
198 return self.settings.get('allow_origin_pat', None)
199
199
200 @property
200 @property
201 def allow_credentials(self):
201 def allow_credentials(self):
202 """Whether to set Access-Control-Allow-Credentials"""
202 """Whether to set Access-Control-Allow-Credentials"""
203 return self.settings.get('allow_credentials', False)
203 return self.settings.get('allow_credentials', False)
204
204
205 def set_default_headers(self):
205 def set_default_headers(self):
206 """Add CORS headers, if defined"""
206 """Add CORS headers, if defined"""
207 super(IPythonHandler, self).set_default_headers()
207 super(IPythonHandler, self).set_default_headers()
208 if self.allow_origin:
208 if self.allow_origin:
209 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
209 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
210 elif self.allow_origin_pat:
210 elif self.allow_origin_pat:
211 origin = self.get_origin()
211 origin = self.get_origin()
212 if origin and self.allow_origin_pat.match(origin):
212 if origin and self.allow_origin_pat.match(origin):
213 self.set_header("Access-Control-Allow-Origin", origin)
213 self.set_header("Access-Control-Allow-Origin", origin)
214 if self.allow_credentials:
214 if self.allow_credentials:
215 self.set_header("Access-Control-Allow-Credentials", 'true')
215 self.set_header("Access-Control-Allow-Credentials", 'true')
216
216
217 def get_origin(self):
217 def get_origin(self):
218 # Handle WebSocket Origin naming convention differences
218 # Handle WebSocket Origin naming convention differences
219 # The difference between version 8 and 13 is that in 8 the
219 # The difference between version 8 and 13 is that in 8 the
220 # client sends a "Sec-Websocket-Origin" header and in 13 it's
220 # client sends a "Sec-Websocket-Origin" header and in 13 it's
221 # simply "Origin".
221 # simply "Origin".
222 if "Origin" in self.request.headers:
222 if "Origin" in self.request.headers:
223 origin = self.request.headers.get("Origin")
223 origin = self.request.headers.get("Origin")
224 else:
224 else:
225 origin = self.request.headers.get("Sec-Websocket-Origin", None)
225 origin = self.request.headers.get("Sec-Websocket-Origin", None)
226 return origin
226 return origin
227
227
228 #---------------------------------------------------------------
228 #---------------------------------------------------------------
229 # template rendering
229 # template rendering
230 #---------------------------------------------------------------
230 #---------------------------------------------------------------
231
231
232 def get_template(self, name):
232 def get_template(self, name):
233 """Return the jinja template object for a given name"""
233 """Return the jinja template object for a given name"""
234 return self.settings['jinja2_env'].get_template(name)
234 return self.settings['jinja2_env'].get_template(name)
235
235
236 def render_template(self, name, **ns):
236 def render_template(self, name, **ns):
237 ns.update(self.template_namespace)
237 ns.update(self.template_namespace)
238 template = self.get_template(name)
238 template = self.get_template(name)
239 return template.render(**ns)
239 return template.render(**ns)
240
240
241 @property
241 @property
242 def template_namespace(self):
242 def template_namespace(self):
243 return dict(
243 return dict(
244 base_url=self.base_url,
244 base_url=self.base_url,
245 default_url=self.default_url,
245 default_url=self.default_url,
246 ws_url=self.ws_url,
246 ws_url=self.ws_url,
247 logged_in=self.logged_in,
247 logged_in=self.logged_in,
248 login_available=self.login_available,
248 login_available=self.login_available,
249 static_url=self.static_url,
249 static_url=self.static_url,
250 sys_info=sys_info,
250 sys_info=sys_info,
251 contents_js_source=self.contents_js_source,
251 contents_js_source=self.contents_js_source,
252 version_hash=self.version_hash,
252 version_hash=self.version_hash,
253 )
253 )
254
254
255 def get_json_body(self):
255 def get_json_body(self):
256 """Return the body of the request as JSON data."""
256 """Return the body of the request as JSON data."""
257 if not self.request.body:
257 if not self.request.body:
258 return None
258 return None
259 # Do we need to call body.decode('utf-8') here?
259 # Do we need to call body.decode('utf-8') here?
260 body = self.request.body.strip().decode(u'utf-8')
260 body = self.request.body.strip().decode(u'utf-8')
261 try:
261 try:
262 model = json.loads(body)
262 model = json.loads(body)
263 except Exception:
263 except Exception:
264 self.log.debug("Bad JSON: %r", body)
264 self.log.debug("Bad JSON: %r", body)
265 self.log.error("Couldn't parse JSON", exc_info=True)
265 self.log.error("Couldn't parse JSON", exc_info=True)
266 raise web.HTTPError(400, u'Invalid JSON in body of request')
266 raise web.HTTPError(400, u'Invalid JSON in body of request')
267 return model
267 return model
268
268
269 def write_error(self, status_code, **kwargs):
269 def write_error(self, status_code, **kwargs):
270 """render custom error pages"""
270 """render custom error pages"""
271 exc_info = kwargs.get('exc_info')
271 exc_info = kwargs.get('exc_info')
272 message = ''
272 message = ''
273 status_message = responses.get(status_code, 'Unknown HTTP Error')
273 status_message = responses.get(status_code, 'Unknown HTTP Error')
274 if exc_info:
274 if exc_info:
275 exception = exc_info[1]
275 exception = exc_info[1]
276 # get the custom message, if defined
276 # get the custom message, if defined
277 try:
277 try:
278 message = exception.log_message % exception.args
278 message = exception.log_message % exception.args
279 except Exception:
279 except Exception:
280 pass
280 pass
281
281
282 # construct the custom reason, if defined
282 # construct the custom reason, if defined
283 reason = getattr(exception, 'reason', '')
283 reason = getattr(exception, 'reason', '')
284 if reason:
284 if reason:
285 status_message = reason
285 status_message = reason
286
286
287 # build template namespace
287 # build template namespace
288 ns = dict(
288 ns = dict(
289 status_code=status_code,
289 status_code=status_code,
290 status_message=status_message,
290 status_message=status_message,
291 message=message,
291 message=message,
292 exception=exception,
292 exception=exception,
293 )
293 )
294
294
295 self.set_header('Content-Type', 'text/html')
295 self.set_header('Content-Type', 'text/html')
296 # render the template
296 # render the template
297 try:
297 try:
298 html = self.render_template('%s.html' % status_code, **ns)
298 html = self.render_template('%s.html' % status_code, **ns)
299 except TemplateNotFound:
299 except TemplateNotFound:
300 self.log.debug("No template for %d", status_code)
300 self.log.debug("No template for %d", status_code)
301 html = self.render_template('error.html', **ns)
301 html = self.render_template('error.html', **ns)
302
302
303 self.write(html)
303 self.write(html)
304
304
305
305
306
306
307 class Template404(IPythonHandler):
307 class Template404(IPythonHandler):
308 """Render our 404 template"""
308 """Render our 404 template"""
309 def prepare(self):
309 def prepare(self):
310 raise web.HTTPError(404)
310 raise web.HTTPError(404)
311
311
312
312
313 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
313 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
314 """static files should only be accessible when logged in"""
314 """static files should only be accessible when logged in"""
315
315
316 @web.authenticated
316 @web.authenticated
317 def get(self, path):
317 def get(self, path):
318 if os.path.splitext(path)[1] == '.ipynb':
318 if os.path.splitext(path)[1] == '.ipynb':
319 name = path.rsplit('/', 1)[-1]
319 name = path.rsplit('/', 1)[-1]
320 self.set_header('Content-Type', 'application/json')
320 self.set_header('Content-Type', 'application/json')
321 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
321 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
322
322
323 return web.StaticFileHandler.get(self, path)
323 return web.StaticFileHandler.get(self, path)
324
324
325 def set_headers(self):
325 def set_headers(self):
326 super(AuthenticatedFileHandler, self).set_headers()
326 super(AuthenticatedFileHandler, self).set_headers()
327 # disable browser caching, rely on 304 replies for savings
327 # disable browser caching, rely on 304 replies for savings
328 if "v" not in self.request.arguments:
328 if "v" not in self.request.arguments:
329 self.add_header("Cache-Control", "no-cache")
329 self.add_header("Cache-Control", "no-cache")
330
330
331 def compute_etag(self):
331 def compute_etag(self):
332 return None
332 return None
333
333
334 def validate_absolute_path(self, root, absolute_path):
334 def validate_absolute_path(self, root, absolute_path):
335 """Validate and return the absolute path.
335 """Validate and return the absolute path.
336
336
337 Requires tornado 3.1
337 Requires tornado 3.1
338
338
339 Adding to tornado's own handling, forbids the serving of hidden files.
339 Adding to tornado's own handling, forbids the serving of hidden files.
340 """
340 """
341 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
341 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
342 abs_root = os.path.abspath(root)
342 abs_root = os.path.abspath(root)
343 if is_hidden(abs_path, abs_root):
343 if is_hidden(abs_path, abs_root):
344 self.log.info("Refusing to serve hidden file, via 404 Error")
344 self.log.info("Refusing to serve hidden file, via 404 Error")
345 raise web.HTTPError(404)
345 raise web.HTTPError(404)
346 return abs_path
346 return abs_path
347
347
348
348
349 def json_errors(method):
349 def json_errors(method):
350 """Decorate methods with this to return GitHub style JSON errors.
350 """Decorate methods with this to return GitHub style JSON errors.
351
351
352 This should be used on any JSON API on any handler method that can raise HTTPErrors.
352 This should be used on any JSON API on any handler method that can raise HTTPErrors.
353
353
354 This will grab the latest HTTPError exception using sys.exc_info
354 This will grab the latest HTTPError exception using sys.exc_info
355 and then:
355 and then:
356
356
357 1. Set the HTTP status code based on the HTTPError
357 1. Set the HTTP status code based on the HTTPError
358 2. Create and return a JSON body with a message field describing
358 2. Create and return a JSON body with a message field describing
359 the error in a human readable form.
359 the error in a human readable form.
360 """
360 """
361 @functools.wraps(method)
361 @functools.wraps(method)
362 @gen.coroutine
362 @gen.coroutine
363 def wrapper(self, *args, **kwargs):
363 def wrapper(self, *args, **kwargs):
364 try:
364 try:
365 result = yield gen.maybe_future(method(self, *args, **kwargs))
365 result = yield gen.maybe_future(method(self, *args, **kwargs))
366 except web.HTTPError as e:
366 except web.HTTPError as e:
367 status = e.status_code
367 status = e.status_code
368 message = e.log_message
368 message = e.log_message
369 self.log.warn(message)
369 self.log.warn(message)
370 self.set_status(e.status_code)
370 self.set_status(e.status_code)
371 reply = dict(message=message, reason=e.reason)
371 reply = dict(message=message, reason=e.reason)
372 self.finish(json.dumps(reply))
372 self.finish(json.dumps(reply))
373 except Exception:
373 except Exception:
374 self.log.error("Unhandled error in API request", exc_info=True)
374 self.log.error("Unhandled error in API request", exc_info=True)
375 status = 500
375 status = 500
376 message = "Unknown server error"
376 message = "Unknown server error"
377 t, value, tb = sys.exc_info()
377 t, value, tb = sys.exc_info()
378 self.set_status(status)
378 self.set_status(status)
379 tb_text = ''.join(traceback.format_exception(t, value, tb))
379 tb_text = ''.join(traceback.format_exception(t, value, tb))
380 reply = dict(message=message, reason=None, traceback=tb_text)
380 reply = dict(message=message, reason=None, traceback=tb_text)
381 self.finish(json.dumps(reply))
381 self.finish(json.dumps(reply))
382 else:
382 else:
383 # FIXME: can use regular return in generators in py3
383 # FIXME: can use regular return in generators in py3
384 raise gen.Return(result)
384 raise gen.Return(result)
385 return wrapper
385 return wrapper
386
386
387
387
388
388
389 #-----------------------------------------------------------------------------
389 #-----------------------------------------------------------------------------
390 # File handler
390 # File handler
391 #-----------------------------------------------------------------------------
391 #-----------------------------------------------------------------------------
392
392
393 # to minimize subclass changes:
393 # to minimize subclass changes:
394 HTTPError = web.HTTPError
394 HTTPError = web.HTTPError
395
395
396 class FileFindHandler(web.StaticFileHandler):
396 class FileFindHandler(web.StaticFileHandler):
397 """subclass of StaticFileHandler for serving files from a search path"""
397 """subclass of StaticFileHandler for serving files from a search path"""
398
398
399 # cache search results, don't search for files more than once
399 # cache search results, don't search for files more than once
400 _static_paths = {}
400 _static_paths = {}
401
401
402 def set_headers(self):
402 def set_headers(self):
403 super(FileFindHandler, self).set_headers()
403 super(FileFindHandler, self).set_headers()
404 # disable browser caching, rely on 304 replies for savings
404 # disable browser caching, rely on 304 replies for savings
405 if "v" not in self.request.arguments or \
405 if "v" not in self.request.arguments or \
406 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):
407 self.add_header("Cache-Control", "no-cache")
407 self.add_header("Cache-Control", "no-cache")
408
408
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
410 self.no_cache_paths = no_cache_paths or []
410 self.no_cache_paths = no_cache_paths or []
411
411
412 if isinstance(path, string_types):
412 if isinstance(path, string_types):
413 path = [path]
413 path = [path]
414
414
415 self.root = tuple(
415 self.root = tuple(
416 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
417 )
417 )
418 self.default_filename = default_filename
418 self.default_filename = default_filename
419
419
420 def compute_etag(self):
420 def compute_etag(self):
421 return None
421 return None
422
422
423 @classmethod
423 @classmethod
424 def get_absolute_path(cls, roots, path):
424 def get_absolute_path(cls, roots, path):
425 """locate a file to serve on our static file search path"""
425 """locate a file to serve on our static file search path"""
426 with cls._lock:
426 with cls._lock:
427 if path in cls._static_paths:
427 if path in cls._static_paths:
428 return cls._static_paths[path]
428 return cls._static_paths[path]
429 try:
429 try:
430 abspath = os.path.abspath(filefind(path, roots))
430 abspath = os.path.abspath(filefind(path, roots))
431 except IOError:
431 except IOError:
432 # IOError means not found
432 # IOError means not found
433 return ''
433 return ''
434
434
435 cls._static_paths[path] = abspath
435 cls._static_paths[path] = abspath
436 return abspath
436 return abspath
437
437
438 def validate_absolute_path(self, root, absolute_path):
438 def validate_absolute_path(self, root, absolute_path):
439 """check if the file should be served (raises 404, 403, etc.)"""
439 """check if the file should be served (raises 404, 403, etc.)"""
440 if absolute_path == '':
440 if absolute_path == '':
441 raise web.HTTPError(404)
441 raise web.HTTPError(404)
442
442
443 for root in self.root:
443 for root in self.root:
444 if (absolute_path + os.sep).startswith(root):
444 if (absolute_path + os.sep).startswith(root):
445 break
445 break
446
446
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
448
448
449
449
450 class ApiVersionHandler(IPythonHandler):
450 class ApiVersionHandler(IPythonHandler):
451
451
452 @json_errors
452 @json_errors
453 def get(self):
453 def get(self):
454 # not authenticated, so give as few info as possible
454 # not authenticated, so give as few info as possible
455 self.finish(json.dumps({"version":IPython.__version__}))
455 self.finish(json.dumps({"version":IPython.__version__}))
456
456
457
457
458 class TrailingSlashHandler(web.RequestHandler):
458 class TrailingSlashHandler(web.RequestHandler):
459 """Simple redirect handler that strips trailing slashes
459 """Simple redirect handler that strips trailing slashes
460
460
461 This should be the first, highest priority handler.
461 This should be the first, highest priority handler.
462 """
462 """
463
463
464 def get(self):
464 def get(self):
465 self.redirect(self.request.uri.rstrip('/'))
465 self.redirect(self.request.uri.rstrip('/'))
466
466
467 post = put = get
467 post = put = get
468
468
469
469
470 class FilesRedirectHandler(IPythonHandler):
470 class FilesRedirectHandler(IPythonHandler):
471 """Handler for redirecting relative URLs to the /files/ handler"""
471 """Handler for redirecting relative URLs to the /files/ handler"""
472
472
473 @staticmethod
473 @staticmethod
474 def redirect_to_files(self, path):
474 def redirect_to_files(self, path):
475 """make redirect logic a reusable static method
475 """make redirect logic a reusable static method
476
476
477 so it can be called from other handlers.
477 so it can be called from other handlers.
478 """
478 """
479 cm = self.contents_manager
479 cm = self.contents_manager
480 if cm.dir_exists(path):
480 if cm.dir_exists(path):
481 # it's a *directory*, redirect to /tree
481 # it's a *directory*, redirect to /tree
482 url = url_path_join(self.base_url, 'tree', path)
482 url = url_path_join(self.base_url, 'tree', path)
483 else:
483 else:
484 orig_path = path
484 orig_path = path
485 # otherwise, redirect to /files
485 # otherwise, redirect to /files
486 parts = path.split('/')
486 parts = path.split('/')
487
487
488 if not cm.file_exists(path=path) and 'files' in parts:
488 if not cm.file_exists(path=path) and 'files' in parts:
489 # redirect without files/ iff it would 404
489 # redirect without files/ iff it would 404
490 # this preserves pre-2.0-style 'files/' links
490 # this preserves pre-2.0-style 'files/' links
491 self.log.warn("Deprecated files/ URL: %s", orig_path)
491 self.log.warn("Deprecated files/ URL: %s", orig_path)
492 parts.remove('files')
492 parts.remove('files')
493 path = '/'.join(parts)
493 path = '/'.join(parts)
494
494
495 if not cm.file_exists(path=path):
495 if not cm.file_exists(path=path):
496 raise web.HTTPError(404)
496 raise web.HTTPError(404)
497
497
498 url = url_path_join(self.base_url, 'files', path)
498 url = url_path_join(self.base_url, 'files', path)
499 url = url_escape(url)
499 url = url_escape(url)
500 self.log.debug("Redirecting %s to %s", self.request.path, url)
500 self.log.debug("Redirecting %s to %s", self.request.path, url)
501 self.redirect(url)
501 self.redirect(url)
502
502
503 def get(self, path=''):
503 def get(self, path=''):
504 return self.redirect_to_files(self, path)
504 return self.redirect_to_files(self, path)
505
505
506
506
507 #-----------------------------------------------------------------------------
507 #-----------------------------------------------------------------------------
508 # URL pattern fragments for re-use
508 # URL pattern fragments for re-use
509 #-----------------------------------------------------------------------------
509 #-----------------------------------------------------------------------------
510
510
511 # path matches any number of `/foo[/bar...]` or just `/` or ''
511 # path matches any number of `/foo[/bar...]` or just `/` or ''
512 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
512 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
513
513
514 #-----------------------------------------------------------------------------
514 #-----------------------------------------------------------------------------
515 # URL to handler mappings
515 # URL to handler mappings
516 #-----------------------------------------------------------------------------
516 #-----------------------------------------------------------------------------
517
517
518
518
519 default_handlers = [
519 default_handlers = [
520 (r".*/", TrailingSlashHandler),
520 (r".*/", TrailingSlashHandler),
521 (r"api", ApiVersionHandler)
521 (r"api", ApiVersionHandler)
522 ]
522 ]
General Comments 0
You need to be logged in to leave comments. Login now