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