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