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