##// END OF EJS Templates
Merge pull request #7016 from rgbkrk/csp...
Min RK -
r19161:d56bcc10 merge
parent child Browse files
Show More
@@ -0,0 +1,4 b''
1 # URI for the CSP Report. Included here to prevent a cyclic dependency.
2 # csp_report_uri is needed both by the BaseHandler (for setting the report-uri)
3 # and by the CSPReportHandler (which depends on the BaseHandler).
4 csp_report_uri = r"/api/security/csp-report"
@@ -0,0 +1,23 b''
1 """Tornado handlers for security logging."""
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
9 from . import csp_report_uri
10
11 class CSPReportHandler(IPythonHandler):
12 '''Accepts a content security policy violation report'''
13 @web.authenticated
14 @json_errors
15 def post(self):
16 '''Log a content security policy violation report'''
17 csp_report = self.get_json_body()
18 self.log.warn("Content security violation: %s",
19 self.request.body.decode('utf8', 'replace'))
20
21 default_handlers = [
22 (csp_report_uri, CSPReportHandler)
23 ]
@@ -1,507 +1,514 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 try:
23 23 from tornado.log import app_log
24 24 except ImportError:
25 25 app_log = logging.getLogger()
26 26
27 27 import IPython
28 28 from IPython.utils.sysinfo import get_sys_info
29 29
30 30 from IPython.config import Application
31 31 from IPython.utils.path import filefind
32 32 from IPython.utils.py3compat import string_types
33 33 from IPython.html.utils import is_hidden, url_path_join, url_escape
34 34
35 from IPython.html.services.security import csp_report_uri
36
35 37 #-----------------------------------------------------------------------------
36 38 # Top-level handlers
37 39 #-----------------------------------------------------------------------------
38 40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39 41
40 42 sys_info = json.dumps(get_sys_info())
41 43
42 44 class AuthenticatedHandler(web.RequestHandler):
43 45 """A RequestHandler with an authenticated user."""
44 46
45 47 def set_default_headers(self):
46 48 headers = self.settings.get('headers', {})
47 49
48 if "X-Frame-Options" not in headers:
49 headers["X-Frame-Options"] = "SAMEORIGIN"
50 if "Content-Security-Policy" not in headers:
51 headers["Content-Security-Policy"] = (
52 "frame-ancestors 'self'; "
53 # Make sure the report-uri is relative to the base_url
54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
55 )
50 56
57 # Allow for overriding headers
51 58 for header_name,value in headers.items() :
52 59 try:
53 60 self.set_header(header_name, value)
54 except Exception:
61 except Exception as e:
55 62 # tornado raise Exception (not a subclass)
56 63 # if method is unsupported (websocket and Access-Control-Allow-Origin
57 64 # for example, so just ignore)
58 pass
65 self.log.debug(e)
59 66
60 67 def clear_login_cookie(self):
61 68 self.clear_cookie(self.cookie_name)
62 69
63 70 def get_current_user(self):
64 71 user_id = self.get_secure_cookie(self.cookie_name)
65 72 # For now the user_id should not return empty, but it could eventually
66 73 if user_id == '':
67 74 user_id = 'anonymous'
68 75 if user_id is None:
69 76 # prevent extra Invalid cookie sig warnings:
70 77 self.clear_login_cookie()
71 78 if not self.login_available:
72 79 user_id = 'anonymous'
73 80 return user_id
74 81
75 82 @property
76 83 def cookie_name(self):
77 84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
78 85 self.request.host
79 86 ))
80 87 return self.settings.get('cookie_name', default_cookie_name)
81 88
82 89 @property
83 90 def password(self):
84 91 """our password"""
85 92 return self.settings.get('password', '')
86 93
87 94 @property
88 95 def logged_in(self):
89 96 """Is a user currently logged in?
90 97
91 98 """
92 99 user = self.get_current_user()
93 100 return (user and not user == 'anonymous')
94 101
95 102 @property
96 103 def login_available(self):
97 104 """May a user proceed to log in?
98 105
99 106 This returns True if login capability is available, irrespective of
100 107 whether the user is already logged in or not.
101 108
102 109 """
103 110 return bool(self.settings.get('password', ''))
104 111
105 112
106 113 class IPythonHandler(AuthenticatedHandler):
107 114 """IPython-specific extensions to authenticated handling
108 115
109 116 Mostly property shortcuts to IPython-specific settings.
110 117 """
111 118
112 119 @property
113 120 def config(self):
114 121 return self.settings.get('config', None)
115 122
116 123 @property
117 124 def log(self):
118 125 """use the IPython log by default, falling back on tornado's logger"""
119 126 if Application.initialized():
120 127 return Application.instance().log
121 128 else:
122 129 return app_log
123 130
124 131 #---------------------------------------------------------------
125 132 # URLs
126 133 #---------------------------------------------------------------
127 134
128 135 @property
129 136 def version_hash(self):
130 137 """The version hash to use for cache hints for static files"""
131 138 return self.settings.get('version_hash', '')
132 139
133 140 @property
134 141 def mathjax_url(self):
135 142 return self.settings.get('mathjax_url', '')
136 143
137 144 @property
138 145 def base_url(self):
139 146 return self.settings.get('base_url', '/')
140 147
141 148 @property
142 149 def ws_url(self):
143 150 return self.settings.get('websocket_url', '')
144 151
145 152 @property
146 153 def contents_js_source(self):
147 154 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
148 155 'services/contents'))
149 156 return self.settings.get('contents_js_source', 'services/contents')
150 157
151 158 #---------------------------------------------------------------
152 159 # Manager objects
153 160 #---------------------------------------------------------------
154 161
155 162 @property
156 163 def kernel_manager(self):
157 164 return self.settings['kernel_manager']
158 165
159 166 @property
160 167 def contents_manager(self):
161 168 return self.settings['contents_manager']
162 169
163 170 @property
164 171 def cluster_manager(self):
165 172 return self.settings['cluster_manager']
166 173
167 174 @property
168 175 def session_manager(self):
169 176 return self.settings['session_manager']
170 177
171 178 @property
172 179 def terminal_manager(self):
173 180 return self.settings['terminal_manager']
174 181
175 182 @property
176 183 def kernel_spec_manager(self):
177 184 return self.settings['kernel_spec_manager']
178 185
179 186 @property
180 187 def config_manager(self):
181 188 return self.settings['config_manager']
182 189
183 190 #---------------------------------------------------------------
184 191 # CORS
185 192 #---------------------------------------------------------------
186 193
187 194 @property
188 195 def allow_origin(self):
189 196 """Normal Access-Control-Allow-Origin"""
190 197 return self.settings.get('allow_origin', '')
191 198
192 199 @property
193 200 def allow_origin_pat(self):
194 201 """Regular expression version of allow_origin"""
195 202 return self.settings.get('allow_origin_pat', None)
196 203
197 204 @property
198 205 def allow_credentials(self):
199 206 """Whether to set Access-Control-Allow-Credentials"""
200 207 return self.settings.get('allow_credentials', False)
201 208
202 209 def set_default_headers(self):
203 210 """Add CORS headers, if defined"""
204 211 super(IPythonHandler, self).set_default_headers()
205 212 if self.allow_origin:
206 213 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
207 214 elif self.allow_origin_pat:
208 215 origin = self.get_origin()
209 216 if origin and self.allow_origin_pat.match(origin):
210 217 self.set_header("Access-Control-Allow-Origin", origin)
211 218 if self.allow_credentials:
212 219 self.set_header("Access-Control-Allow-Credentials", 'true')
213 220
214 221 def get_origin(self):
215 222 # Handle WebSocket Origin naming convention differences
216 223 # The difference between version 8 and 13 is that in 8 the
217 224 # client sends a "Sec-Websocket-Origin" header and in 13 it's
218 225 # simply "Origin".
219 226 if "Origin" in self.request.headers:
220 227 origin = self.request.headers.get("Origin")
221 228 else:
222 229 origin = self.request.headers.get("Sec-Websocket-Origin", None)
223 230 return origin
224 231
225 232 #---------------------------------------------------------------
226 233 # template rendering
227 234 #---------------------------------------------------------------
228 235
229 236 def get_template(self, name):
230 237 """Return the jinja template object for a given name"""
231 238 return self.settings['jinja2_env'].get_template(name)
232 239
233 240 def render_template(self, name, **ns):
234 241 ns.update(self.template_namespace)
235 242 template = self.get_template(name)
236 243 return template.render(**ns)
237 244
238 245 @property
239 246 def template_namespace(self):
240 247 return dict(
241 248 base_url=self.base_url,
242 249 ws_url=self.ws_url,
243 250 logged_in=self.logged_in,
244 251 login_available=self.login_available,
245 252 static_url=self.static_url,
246 253 sys_info=sys_info,
247 254 contents_js_source=self.contents_js_source,
248 255 version_hash=self.version_hash,
249 256 )
250 257
251 258 def get_json_body(self):
252 259 """Return the body of the request as JSON data."""
253 260 if not self.request.body:
254 261 return None
255 262 # Do we need to call body.decode('utf-8') here?
256 263 body = self.request.body.strip().decode(u'utf-8')
257 264 try:
258 265 model = json.loads(body)
259 266 except Exception:
260 267 self.log.debug("Bad JSON: %r", body)
261 268 self.log.error("Couldn't parse JSON", exc_info=True)
262 269 raise web.HTTPError(400, u'Invalid JSON in body of request')
263 270 return model
264 271
265 272 def write_error(self, status_code, **kwargs):
266 273 """render custom error pages"""
267 274 exc_info = kwargs.get('exc_info')
268 275 message = ''
269 276 status_message = responses.get(status_code, 'Unknown HTTP Error')
270 277 if exc_info:
271 278 exception = exc_info[1]
272 279 # get the custom message, if defined
273 280 try:
274 281 message = exception.log_message % exception.args
275 282 except Exception:
276 283 pass
277 284
278 285 # construct the custom reason, if defined
279 286 reason = getattr(exception, 'reason', '')
280 287 if reason:
281 288 status_message = reason
282 289
283 290 # build template namespace
284 291 ns = dict(
285 292 status_code=status_code,
286 293 status_message=status_message,
287 294 message=message,
288 295 exception=exception,
289 296 )
290 297
291 298 self.set_header('Content-Type', 'text/html')
292 299 # render the template
293 300 try:
294 301 html = self.render_template('%s.html' % status_code, **ns)
295 302 except TemplateNotFound:
296 303 self.log.debug("No template for %d", status_code)
297 304 html = self.render_template('error.html', **ns)
298 305
299 306 self.write(html)
300 307
301 308
302 309
303 310 class Template404(IPythonHandler):
304 311 """Render our 404 template"""
305 312 def prepare(self):
306 313 raise web.HTTPError(404)
307 314
308 315
309 316 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
310 317 """static files should only be accessible when logged in"""
311 318
312 319 @web.authenticated
313 320 def get(self, path):
314 321 if os.path.splitext(path)[1] == '.ipynb':
315 322 name = path.rsplit('/', 1)[-1]
316 323 self.set_header('Content-Type', 'application/json')
317 324 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
318 325
319 326 return web.StaticFileHandler.get(self, path)
320 327
321 328 def set_headers(self):
322 329 super(AuthenticatedFileHandler, self).set_headers()
323 330 # disable browser caching, rely on 304 replies for savings
324 331 if "v" not in self.request.arguments:
325 332 self.add_header("Cache-Control", "no-cache")
326 333
327 334 def compute_etag(self):
328 335 return None
329 336
330 337 def validate_absolute_path(self, root, absolute_path):
331 338 """Validate and return the absolute path.
332 339
333 340 Requires tornado 3.1
334 341
335 342 Adding to tornado's own handling, forbids the serving of hidden files.
336 343 """
337 344 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
338 345 abs_root = os.path.abspath(root)
339 346 if is_hidden(abs_path, abs_root):
340 347 self.log.info("Refusing to serve hidden file, via 404 Error")
341 348 raise web.HTTPError(404)
342 349 return abs_path
343 350
344 351
345 352 def json_errors(method):
346 353 """Decorate methods with this to return GitHub style JSON errors.
347 354
348 355 This should be used on any JSON API on any handler method that can raise HTTPErrors.
349 356
350 357 This will grab the latest HTTPError exception using sys.exc_info
351 358 and then:
352 359
353 360 1. Set the HTTP status code based on the HTTPError
354 361 2. Create and return a JSON body with a message field describing
355 362 the error in a human readable form.
356 363 """
357 364 @functools.wraps(method)
358 365 def wrapper(self, *args, **kwargs):
359 366 try:
360 367 result = method(self, *args, **kwargs)
361 368 except web.HTTPError as e:
362 369 status = e.status_code
363 370 message = e.log_message
364 371 self.log.warn(message)
365 372 self.set_status(e.status_code)
366 373 self.finish(json.dumps(dict(message=message)))
367 374 except Exception:
368 375 self.log.error("Unhandled error in API request", exc_info=True)
369 376 status = 500
370 377 message = "Unknown server error"
371 378 t, value, tb = sys.exc_info()
372 379 self.set_status(status)
373 380 tb_text = ''.join(traceback.format_exception(t, value, tb))
374 381 reply = dict(message=message, traceback=tb_text)
375 382 self.finish(json.dumps(reply))
376 383 else:
377 384 return result
378 385 return wrapper
379 386
380 387
381 388
382 389 #-----------------------------------------------------------------------------
383 390 # File handler
384 391 #-----------------------------------------------------------------------------
385 392
386 393 # to minimize subclass changes:
387 394 HTTPError = web.HTTPError
388 395
389 396 class FileFindHandler(web.StaticFileHandler):
390 397 """subclass of StaticFileHandler for serving files from a search path"""
391 398
392 399 # cache search results, don't search for files more than once
393 400 _static_paths = {}
394 401
395 402 def set_headers(self):
396 403 super(FileFindHandler, self).set_headers()
397 404 # disable browser caching, rely on 304 replies for savings
398 405 if "v" not in self.request.arguments or \
399 406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
400 407 self.add_header("Cache-Control", "no-cache")
401 408
402 409 def initialize(self, path, default_filename=None, no_cache_paths=None):
403 410 self.no_cache_paths = no_cache_paths or []
404 411
405 412 if isinstance(path, string_types):
406 413 path = [path]
407 414
408 415 self.root = tuple(
409 416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
410 417 )
411 418 self.default_filename = default_filename
412 419
413 420 def compute_etag(self):
414 421 return None
415 422
416 423 @classmethod
417 424 def get_absolute_path(cls, roots, path):
418 425 """locate a file to serve on our static file search path"""
419 426 with cls._lock:
420 427 if path in cls._static_paths:
421 428 return cls._static_paths[path]
422 429 try:
423 430 abspath = os.path.abspath(filefind(path, roots))
424 431 except IOError:
425 432 # IOError means not found
426 433 return ''
427 434
428 435 cls._static_paths[path] = abspath
429 436 return abspath
430 437
431 438 def validate_absolute_path(self, root, absolute_path):
432 439 """check if the file should be served (raises 404, 403, etc.)"""
433 440 if absolute_path == '':
434 441 raise web.HTTPError(404)
435 442
436 443 for root in self.root:
437 444 if (absolute_path + os.sep).startswith(root):
438 445 break
439 446
440 447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
441 448
442 449
443 450 class ApiVersionHandler(IPythonHandler):
444 451
445 452 @json_errors
446 453 def get(self):
447 454 # not authenticated, so give as few info as possible
448 455 self.finish(json.dumps({"version":IPython.__version__}))
449 456
450 457
451 458 class TrailingSlashHandler(web.RequestHandler):
452 459 """Simple redirect handler that strips trailing slashes
453 460
454 461 This should be the first, highest priority handler.
455 462 """
456 463
457 464 def get(self):
458 465 self.redirect(self.request.uri.rstrip('/'))
459 466
460 467 post = put = get
461 468
462 469
463 470 class FilesRedirectHandler(IPythonHandler):
464 471 """Handler for redirecting relative URLs to the /files/ handler"""
465 472 def get(self, path=''):
466 473 cm = self.contents_manager
467 474 if cm.dir_exists(path):
468 475 # it's a *directory*, redirect to /tree
469 476 url = url_path_join(self.base_url, 'tree', path)
470 477 else:
471 478 orig_path = path
472 479 # otherwise, redirect to /files
473 480 parts = path.split('/')
474 481
475 482 if not cm.file_exists(path=path) and 'files' in parts:
476 483 # redirect without files/ iff it would 404
477 484 # this preserves pre-2.0-style 'files/' links
478 485 self.log.warn("Deprecated files/ URL: %s", orig_path)
479 486 parts.remove('files')
480 487 path = '/'.join(parts)
481 488
482 489 if not cm.file_exists(path=path):
483 490 raise web.HTTPError(404)
484 491
485 492 url = url_path_join(self.base_url, 'files', path)
486 493 url = url_escape(url)
487 494 self.log.debug("Redirecting %s to %s", self.request.path, url)
488 495 self.redirect(url)
489 496
490 497
491 498 #-----------------------------------------------------------------------------
492 499 # URL pattern fragments for re-use
493 500 #-----------------------------------------------------------------------------
494 501
495 502 # path matches any number of `/foo[/bar...]` or just `/` or ''
496 503 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
497 504 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
498 505
499 506 #-----------------------------------------------------------------------------
500 507 # URL to handler mappings
501 508 #-----------------------------------------------------------------------------
502 509
503 510
504 511 default_handlers = [
505 512 (r".*/", TrailingSlashHandler),
506 513 (r"api", ApiVersionHandler)
507 514 ]
@@ -1,1041 +1,1041 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import datetime
11 11 import errno
12 12 import io
13 13 import json
14 14 import logging
15 15 import os
16 16 import random
17 17 import re
18 18 import select
19 19 import signal
20 20 import socket
21 21 import sys
22 22 import threading
23 23 import time
24 24 import webbrowser
25 25
26 26
27 27 # check for pyzmq 2.1.11
28 28 from IPython.utils.zmqrelated import check_for_zmq
29 29 check_for_zmq('2.1.11', 'IPython.html')
30 30
31 31 from jinja2 import Environment, FileSystemLoader
32 32
33 33 # Install the pyzmq ioloop. This has to be done before anything else from
34 34 # tornado is imported.
35 35 from zmq.eventloop import ioloop
36 36 ioloop.install()
37 37
38 38 # check for tornado 3.1.0
39 39 msg = "The IPython Notebook requires tornado >= 4.0"
40 40 try:
41 41 import tornado
42 42 except ImportError:
43 43 raise ImportError(msg)
44 44 try:
45 45 version_info = tornado.version_info
46 46 except AttributeError:
47 47 raise ImportError(msg + ", but you have < 1.1.0")
48 48 if version_info < (4,0):
49 49 raise ImportError(msg + ", but you have %s" % tornado.version)
50 50
51 51 from tornado import httpserver
52 52 from tornado import web
53 53 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 54
55 55 from IPython.html import (
56 56 DEFAULT_STATIC_FILES_PATH,
57 57 DEFAULT_TEMPLATE_PATH_LIST,
58 58 )
59 59 from .base.handlers import Template404
60 60 from .log import log_request
61 61 from .services.kernels.kernelmanager import MappingKernelManager
62 62 from .services.contents.manager import ContentsManager
63 63 from .services.contents.filemanager import FileContentsManager
64 64 from .services.clusters.clustermanager import ClusterManager
65 65 from .services.sessions.sessionmanager import SessionManager
66 66
67 67 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
68 68
69 69 from IPython.config import Config
70 70 from IPython.config.application import catch_config_error, boolean_flag
71 71 from IPython.core.application import (
72 72 BaseIPythonApplication, base_flags, base_aliases,
73 73 )
74 74 from IPython.core.profiledir import ProfileDir
75 75 from IPython.kernel import KernelManager
76 76 from IPython.kernel.kernelspec import KernelSpecManager
77 77 from IPython.kernel.zmq.session import default_secure, Session
78 78 from IPython.nbformat.sign import NotebookNotary
79 79 from IPython.utils.importstring import import_item
80 80 from IPython.utils import submodule
81 81 from IPython.utils.process import check_pid
82 82 from IPython.utils.traitlets import (
83 83 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
84 84 DottedObjectName, TraitError,
85 85 )
86 86 from IPython.utils import py3compat
87 87 from IPython.utils.path import filefind, get_ipython_dir
88 88 from IPython.utils.sysinfo import get_sys_info
89 89
90 90 from .utils import url_path_join
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Module globals
94 94 #-----------------------------------------------------------------------------
95 95
96 96 _examples = """
97 97 ipython notebook # start the notebook
98 98 ipython notebook --profile=sympy # use the sympy profile
99 99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
100 100 """
101 101
102 102 #-----------------------------------------------------------------------------
103 103 # Helper functions
104 104 #-----------------------------------------------------------------------------
105 105
106 106 def random_ports(port, n):
107 107 """Generate a list of n random ports near the given port.
108 108
109 109 The first 5 ports will be sequential, and the remaining n-5 will be
110 110 randomly selected in the range [port-2*n, port+2*n].
111 111 """
112 112 for i in range(min(5, n)):
113 113 yield port + i
114 114 for i in range(n-5):
115 115 yield max(1, port + random.randint(-2*n, 2*n))
116 116
117 117 def load_handlers(name):
118 118 """Load the (URL pattern, handler) tuples for each component."""
119 119 name = 'IPython.html.' + name
120 120 mod = __import__(name, fromlist=['default_handlers'])
121 121 return mod.default_handlers
122 122
123 123 #-----------------------------------------------------------------------------
124 124 # The Tornado web application
125 125 #-----------------------------------------------------------------------------
126 126
127 127 class NotebookWebApplication(web.Application):
128 128
129 129 def __init__(self, ipython_app, kernel_manager, contents_manager,
130 130 cluster_manager, session_manager, kernel_spec_manager,
131 131 config_manager, log,
132 132 base_url, default_url, settings_overrides, jinja_env_options):
133 133
134 134 settings = self.init_settings(
135 135 ipython_app, kernel_manager, contents_manager, cluster_manager,
136 136 session_manager, kernel_spec_manager, config_manager, log, base_url,
137 137 default_url, settings_overrides, jinja_env_options)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
143 143 cluster_manager, session_manager, kernel_spec_manager,
144 144 config_manager,
145 145 log, base_url, default_url, settings_overrides,
146 146 jinja_env_options=None):
147 147
148 148 _template_path = settings_overrides.get(
149 149 "template_path",
150 150 ipython_app.template_file_path,
151 151 )
152 152 if isinstance(_template_path, str):
153 153 _template_path = (_template_path,)
154 154 template_path = [os.path.expanduser(path) for path in _template_path]
155 155
156 156 jenv_opt = jinja_env_options if jinja_env_options else {}
157 157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
158 158
159 159 sys_info = get_sys_info()
160 160 if sys_info['commit_source'] == 'repository':
161 161 # don't cache (rely on 304) when working from master
162 162 version_hash = ''
163 163 else:
164 164 # reset the cache on server restart
165 165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
166 166
167 167 settings = dict(
168 168 # basics
169 169 log_function=log_request,
170 170 base_url=base_url,
171 171 default_url=default_url,
172 172 template_path=template_path,
173 173 static_path=ipython_app.static_file_path,
174 174 static_handler_class = FileFindHandler,
175 175 static_url_prefix = url_path_join(base_url,'/static/'),
176 176 static_handler_args = {
177 177 # don't cache custom.js
178 178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
179 179 },
180 180 version_hash=version_hash,
181 181
182 182 # authentication
183 183 cookie_secret=ipython_app.cookie_secret,
184 184 login_url=url_path_join(base_url,'/login'),
185 185 password=ipython_app.password,
186 186
187 187 # managers
188 188 kernel_manager=kernel_manager,
189 189 contents_manager=contents_manager,
190 190 cluster_manager=cluster_manager,
191 191 session_manager=session_manager,
192 192 kernel_spec_manager=kernel_spec_manager,
193 193 config_manager=config_manager,
194 194
195 195 # IPython stuff
196 196 nbextensions_path = ipython_app.nbextensions_path,
197 197 websocket_url=ipython_app.websocket_url,
198 198 mathjax_url=ipython_app.mathjax_url,
199 199 config=ipython_app.config,
200 200 jinja2_env=env,
201 201 terminals_available=False, # Set later if terminals are available
202 202 )
203 203
204 204 # allow custom overrides for the tornado web app.
205 205 settings.update(settings_overrides)
206 206 return settings
207 207
208 208 def init_handlers(self, settings):
209 209 """Load the (URL pattern, handler) tuples for each component."""
210 210
211 211 # Order matters. The first handler to match the URL will handle the request.
212 212 handlers = []
213 213 handlers.extend(load_handlers('tree.handlers'))
214 214 handlers.extend(load_handlers('auth.login'))
215 215 handlers.extend(load_handlers('auth.logout'))
216 216 handlers.extend(load_handlers('files.handlers'))
217 217 handlers.extend(load_handlers('notebook.handlers'))
218 218 handlers.extend(load_handlers('nbconvert.handlers'))
219 219 handlers.extend(load_handlers('kernelspecs.handlers'))
220 220 handlers.extend(load_handlers('edit.handlers'))
221 221 handlers.extend(load_handlers('services.config.handlers'))
222 222 handlers.extend(load_handlers('services.kernels.handlers'))
223 223 handlers.extend(load_handlers('services.contents.handlers'))
224 224 handlers.extend(load_handlers('services.clusters.handlers'))
225 225 handlers.extend(load_handlers('services.sessions.handlers'))
226 226 handlers.extend(load_handlers('services.nbconvert.handlers'))
227 227 handlers.extend(load_handlers('services.kernelspecs.handlers'))
228
228 handlers.extend(load_handlers('services.security.handlers'))
229 229 handlers.append(
230 230 (r"/nbextensions/(.*)", FileFindHandler, {
231 231 'path': settings['nbextensions_path'],
232 232 'no_cache_paths': ['/'], # don't cache anything in nbextensions
233 233 }),
234 234 )
235 235 # register base handlers last
236 236 handlers.extend(load_handlers('base.handlers'))
237 237 # set the URL that will be redirected from `/`
238 238 handlers.append(
239 239 (r'/?', web.RedirectHandler, {
240 240 'url' : url_path_join(settings['base_url'], settings['default_url']),
241 241 'permanent': False, # want 302, not 301
242 242 })
243 243 )
244 244 # prepend base_url onto the patterns that we match
245 245 new_handlers = []
246 246 for handler in handlers:
247 247 pattern = url_path_join(settings['base_url'], handler[0])
248 248 new_handler = tuple([pattern] + list(handler[1:]))
249 249 new_handlers.append(new_handler)
250 250 # add 404 on the end, which will catch everything that falls through
251 251 new_handlers.append((r'(.*)', Template404))
252 252 return new_handlers
253 253
254 254
255 255 class NbserverListApp(BaseIPythonApplication):
256 256
257 257 description="List currently running notebook servers in this profile."
258 258
259 259 flags = dict(
260 260 json=({'NbserverListApp': {'json': True}},
261 261 "Produce machine-readable JSON output."),
262 262 )
263 263
264 264 json = Bool(False, config=True,
265 265 help="If True, each line of output will be a JSON object with the "
266 266 "details from the server info file.")
267 267
268 268 def start(self):
269 269 if not self.json:
270 270 print("Currently running servers:")
271 271 for serverinfo in list_running_servers(self.profile):
272 272 if self.json:
273 273 print(json.dumps(serverinfo))
274 274 else:
275 275 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
276 276
277 277 #-----------------------------------------------------------------------------
278 278 # Aliases and Flags
279 279 #-----------------------------------------------------------------------------
280 280
281 281 flags = dict(base_flags)
282 282 flags['no-browser']=(
283 283 {'NotebookApp' : {'open_browser' : False}},
284 284 "Don't open the notebook in a browser after startup."
285 285 )
286 286 flags['pylab']=(
287 287 {'NotebookApp' : {'pylab' : 'warn'}},
288 288 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
289 289 )
290 290 flags['no-mathjax']=(
291 291 {'NotebookApp' : {'enable_mathjax' : False}},
292 292 """Disable MathJax
293 293
294 294 MathJax is the javascript library IPython uses to render math/LaTeX. It is
295 295 very large, so you may want to disable it if you have a slow internet
296 296 connection, or for offline use of the notebook.
297 297
298 298 When disabled, equations etc. will appear as their untransformed TeX source.
299 299 """
300 300 )
301 301
302 302 # Add notebook manager flags
303 303 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
304 304 'DEPRECATED, IGNORED',
305 305 'DEPRECATED, IGNORED'))
306 306
307 307 aliases = dict(base_aliases)
308 308
309 309 aliases.update({
310 310 'ip': 'NotebookApp.ip',
311 311 'port': 'NotebookApp.port',
312 312 'port-retries': 'NotebookApp.port_retries',
313 313 'transport': 'KernelManager.transport',
314 314 'keyfile': 'NotebookApp.keyfile',
315 315 'certfile': 'NotebookApp.certfile',
316 316 'notebook-dir': 'NotebookApp.notebook_dir',
317 317 'browser': 'NotebookApp.browser',
318 318 'pylab': 'NotebookApp.pylab',
319 319 })
320 320
321 321 #-----------------------------------------------------------------------------
322 322 # NotebookApp
323 323 #-----------------------------------------------------------------------------
324 324
325 325 class NotebookApp(BaseIPythonApplication):
326 326
327 327 name = 'ipython-notebook'
328 328
329 329 description = """
330 330 The IPython HTML Notebook.
331 331
332 332 This launches a Tornado based HTML Notebook Server that serves up an
333 333 HTML5/Javascript Notebook client.
334 334 """
335 335 examples = _examples
336 336 aliases = aliases
337 337 flags = flags
338 338
339 339 classes = [
340 340 KernelManager, ProfileDir, Session, MappingKernelManager,
341 341 ContentsManager, FileContentsManager, NotebookNotary,
342 342 ]
343 343 flags = Dict(flags)
344 344 aliases = Dict(aliases)
345 345
346 346 subcommands = dict(
347 347 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
348 348 )
349 349
350 350 ipython_kernel_argv = List(Unicode)
351 351
352 352 _log_formatter_cls = LogFormatter
353 353
354 354 def _log_level_default(self):
355 355 return logging.INFO
356 356
357 357 def _log_datefmt_default(self):
358 358 """Exclude date from default date format"""
359 359 return "%H:%M:%S"
360 360
361 361 def _log_format_default(self):
362 362 """override default log format to include time"""
363 363 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
364 364
365 365 # create requested profiles by default, if they don't exist:
366 366 auto_create = Bool(True)
367 367
368 368 # file to be opened in the notebook server
369 369 file_to_run = Unicode('', config=True)
370 370
371 371 # Network related information
372 372
373 373 allow_origin = Unicode('', config=True,
374 374 help="""Set the Access-Control-Allow-Origin header
375 375
376 376 Use '*' to allow any origin to access your server.
377 377
378 378 Takes precedence over allow_origin_pat.
379 379 """
380 380 )
381 381
382 382 allow_origin_pat = Unicode('', config=True,
383 383 help="""Use a regular expression for the Access-Control-Allow-Origin header
384 384
385 385 Requests from an origin matching the expression will get replies with:
386 386
387 387 Access-Control-Allow-Origin: origin
388 388
389 389 where `origin` is the origin of the request.
390 390
391 391 Ignored if allow_origin is set.
392 392 """
393 393 )
394 394
395 395 allow_credentials = Bool(False, config=True,
396 396 help="Set the Access-Control-Allow-Credentials: true header"
397 397 )
398 398
399 399 default_url = Unicode('/tree', config=True,
400 400 help="The default URL to redirect to from `/`"
401 401 )
402 402
403 403 ip = Unicode('localhost', config=True,
404 404 help="The IP address the notebook server will listen on."
405 405 )
406 406
407 407 def _ip_changed(self, name, old, new):
408 408 if new == u'*': self.ip = u''
409 409
410 410 port = Integer(8888, config=True,
411 411 help="The port the notebook server will listen on."
412 412 )
413 413 port_retries = Integer(50, config=True,
414 414 help="The number of additional ports to try if the specified port is not available."
415 415 )
416 416
417 417 certfile = Unicode(u'', config=True,
418 418 help="""The full path to an SSL/TLS certificate file."""
419 419 )
420 420
421 421 keyfile = Unicode(u'', config=True,
422 422 help="""The full path to a private key file for usage with SSL/TLS."""
423 423 )
424 424
425 425 cookie_secret_file = Unicode(config=True,
426 426 help="""The file where the cookie secret is stored."""
427 427 )
428 428 def _cookie_secret_file_default(self):
429 429 if self.profile_dir is None:
430 430 return ''
431 431 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
432 432
433 433 cookie_secret = Bytes(b'', config=True,
434 434 help="""The random bytes used to secure cookies.
435 435 By default this is a new random number every time you start the Notebook.
436 436 Set it to a value in a config file to enable logins to persist across server sessions.
437 437
438 438 Note: Cookie secrets should be kept private, do not share config files with
439 439 cookie_secret stored in plaintext (you can read the value from a file).
440 440 """
441 441 )
442 442 def _cookie_secret_default(self):
443 443 if os.path.exists(self.cookie_secret_file):
444 444 with io.open(self.cookie_secret_file, 'rb') as f:
445 445 return f.read()
446 446 else:
447 447 secret = base64.encodestring(os.urandom(1024))
448 448 self._write_cookie_secret_file(secret)
449 449 return secret
450 450
451 451 def _write_cookie_secret_file(self, secret):
452 452 """write my secret to my secret_file"""
453 453 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
454 454 with io.open(self.cookie_secret_file, 'wb') as f:
455 455 f.write(secret)
456 456 try:
457 457 os.chmod(self.cookie_secret_file, 0o600)
458 458 except OSError:
459 459 self.log.warn(
460 460 "Could not set permissions on %s",
461 461 self.cookie_secret_file
462 462 )
463 463
464 464 password = Unicode(u'', config=True,
465 465 help="""Hashed password to use for web authentication.
466 466
467 467 To generate, type in a python/IPython shell:
468 468
469 469 from IPython.lib import passwd; passwd()
470 470
471 471 The string should be of the form type:salt:hashed-password.
472 472 """
473 473 )
474 474
475 475 open_browser = Bool(True, config=True,
476 476 help="""Whether to open in a browser after starting.
477 477 The specific browser used is platform dependent and
478 478 determined by the python standard library `webbrowser`
479 479 module, unless it is overridden using the --browser
480 480 (NotebookApp.browser) configuration option.
481 481 """)
482 482
483 483 browser = Unicode(u'', config=True,
484 484 help="""Specify what command to use to invoke a web
485 485 browser when opening the notebook. If not specified, the
486 486 default browser will be determined by the `webbrowser`
487 487 standard library module, which allows setting of the
488 488 BROWSER environment variable to override it.
489 489 """)
490 490
491 491 webapp_settings = Dict(config=True,
492 492 help="DEPRECATED, use tornado_settings"
493 493 )
494 494 def _webapp_settings_changed(self, name, old, new):
495 495 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
496 496 self.tornado_settings = new
497 497
498 498 tornado_settings = Dict(config=True,
499 499 help="Supply overrides for the tornado.web.Application that the "
500 500 "IPython notebook uses.")
501 501
502 502 jinja_environment_options = Dict(config=True,
503 503 help="Supply extra arguments that will be passed to Jinja environment.")
504 504
505 505
506 506 enable_mathjax = Bool(True, config=True,
507 507 help="""Whether to enable MathJax for typesetting math/TeX
508 508
509 509 MathJax is the javascript library IPython uses to render math/LaTeX. It is
510 510 very large, so you may want to disable it if you have a slow internet
511 511 connection, or for offline use of the notebook.
512 512
513 513 When disabled, equations etc. will appear as their untransformed TeX source.
514 514 """
515 515 )
516 516 def _enable_mathjax_changed(self, name, old, new):
517 517 """set mathjax url to empty if mathjax is disabled"""
518 518 if not new:
519 519 self.mathjax_url = u''
520 520
521 521 base_url = Unicode('/', config=True,
522 522 help='''The base URL for the notebook server.
523 523
524 524 Leading and trailing slashes can be omitted,
525 525 and will automatically be added.
526 526 ''')
527 527 def _base_url_changed(self, name, old, new):
528 528 if not new.startswith('/'):
529 529 self.base_url = '/'+new
530 530 elif not new.endswith('/'):
531 531 self.base_url = new+'/'
532 532
533 533 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
534 534 def _base_project_url_changed(self, name, old, new):
535 535 self.log.warn("base_project_url is deprecated, use base_url")
536 536 self.base_url = new
537 537
538 538 extra_static_paths = List(Unicode, config=True,
539 539 help="""Extra paths to search for serving static files.
540 540
541 541 This allows adding javascript/css to be available from the notebook server machine,
542 542 or overriding individual files in the IPython"""
543 543 )
544 544 def _extra_static_paths_default(self):
545 545 return [os.path.join(self.profile_dir.location, 'static')]
546 546
547 547 @property
548 548 def static_file_path(self):
549 549 """return extra paths + the default location"""
550 550 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
551 551
552 552 extra_template_paths = List(Unicode, config=True,
553 553 help="""Extra paths to search for serving jinja templates.
554 554
555 555 Can be used to override templates from IPython.html.templates."""
556 556 )
557 557 def _extra_template_paths_default(self):
558 558 return []
559 559
560 560 @property
561 561 def template_file_path(self):
562 562 """return extra paths + the default locations"""
563 563 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
564 564
565 565 nbextensions_path = List(Unicode, config=True,
566 566 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
567 567 )
568 568 def _nbextensions_path_default(self):
569 569 return [os.path.join(get_ipython_dir(), 'nbextensions')]
570 570
571 571 websocket_url = Unicode("", config=True,
572 572 help="""The base URL for websockets,
573 573 if it differs from the HTTP server (hint: it almost certainly doesn't).
574 574
575 575 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
576 576 """
577 577 )
578 578 mathjax_url = Unicode("", config=True,
579 579 help="""The url for MathJax.js."""
580 580 )
581 581 def _mathjax_url_default(self):
582 582 if not self.enable_mathjax:
583 583 return u''
584 584 static_url_prefix = self.tornado_settings.get("static_url_prefix",
585 585 url_path_join(self.base_url, "static")
586 586 )
587 587
588 588 # try local mathjax, either in nbextensions/mathjax or static/mathjax
589 589 for (url_prefix, search_path) in [
590 590 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
591 591 (static_url_prefix, self.static_file_path),
592 592 ]:
593 593 self.log.debug("searching for local mathjax in %s", search_path)
594 594 try:
595 595 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
596 596 except IOError:
597 597 continue
598 598 else:
599 599 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
600 600 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
601 601 return url
602 602
603 603 # no local mathjax, serve from CDN
604 604 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
605 605 self.log.info("Using MathJax from CDN: %s", url)
606 606 return url
607 607
608 608 def _mathjax_url_changed(self, name, old, new):
609 609 if new and not self.enable_mathjax:
610 610 # enable_mathjax=False overrides mathjax_url
611 611 self.mathjax_url = u''
612 612 else:
613 613 self.log.info("Using MathJax: %s", new)
614 614
615 615 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
616 616 config=True,
617 617 help='The notebook manager class to use.'
618 618 )
619 619 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
620 620 config=True,
621 621 help='The kernel manager class to use.'
622 622 )
623 623 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
624 624 config=True,
625 625 help='The session manager class to use.'
626 626 )
627 627 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
628 628 config=True,
629 629 help='The cluster manager class to use.'
630 630 )
631 631
632 632 config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
633 633 config = True,
634 634 help='The config manager class to use'
635 635 )
636 636
637 637 kernel_spec_manager = Instance(KernelSpecManager)
638 638
639 639 def _kernel_spec_manager_default(self):
640 640 return KernelSpecManager(ipython_dir=self.ipython_dir)
641 641
642 642
643 643 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
644 644 config=True,
645 645 help="""
646 646 The kernel spec manager class to use. Should be a subclass
647 647 of `IPython.kernel.kernelspec.KernelSpecManager`.
648 648
649 649 The Api of KernelSpecManager is provisional and might change
650 650 without warning between this version of IPython and the next stable one.
651 651 """)
652 652
653 653 trust_xheaders = Bool(False, config=True,
654 654 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
655 655 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
656 656 )
657 657
658 658 info_file = Unicode()
659 659
660 660 def _info_file_default(self):
661 661 info_file = "nbserver-%s.json"%os.getpid()
662 662 return os.path.join(self.profile_dir.security_dir, info_file)
663 663
664 664 pylab = Unicode('disabled', config=True,
665 665 help="""
666 666 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
667 667 """
668 668 )
669 669 def _pylab_changed(self, name, old, new):
670 670 """when --pylab is specified, display a warning and exit"""
671 671 if new != 'warn':
672 672 backend = ' %s' % new
673 673 else:
674 674 backend = ''
675 675 self.log.error("Support for specifying --pylab on the command line has been removed.")
676 676 self.log.error(
677 677 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
678 678 )
679 679 self.exit(1)
680 680
681 681 notebook_dir = Unicode(config=True,
682 682 help="The directory to use for notebooks and kernels."
683 683 )
684 684
685 685 def _notebook_dir_default(self):
686 686 if self.file_to_run:
687 687 return os.path.dirname(os.path.abspath(self.file_to_run))
688 688 else:
689 689 return py3compat.getcwd()
690 690
691 691 def _notebook_dir_changed(self, name, old, new):
692 692 """Do a bit of validation of the notebook dir."""
693 693 if not os.path.isabs(new):
694 694 # If we receive a non-absolute path, make it absolute.
695 695 self.notebook_dir = os.path.abspath(new)
696 696 return
697 697 if not os.path.isdir(new):
698 698 raise TraitError("No such notebook dir: %r" % new)
699 699
700 700 # setting App.notebook_dir implies setting notebook and kernel dirs as well
701 701 self.config.FileContentsManager.root_dir = new
702 702 self.config.MappingKernelManager.root_dir = new
703 703
704 704
705 705 def parse_command_line(self, argv=None):
706 706 super(NotebookApp, self).parse_command_line(argv)
707 707
708 708 if self.extra_args:
709 709 arg0 = self.extra_args[0]
710 710 f = os.path.abspath(arg0)
711 711 self.argv.remove(arg0)
712 712 if not os.path.exists(f):
713 713 self.log.critical("No such file or directory: %s", f)
714 714 self.exit(1)
715 715
716 716 # Use config here, to ensure that it takes higher priority than
717 717 # anything that comes from the profile.
718 718 c = Config()
719 719 if os.path.isdir(f):
720 720 c.NotebookApp.notebook_dir = f
721 721 elif os.path.isfile(f):
722 722 c.NotebookApp.file_to_run = f
723 723 self.update_config(c)
724 724
725 725 def init_kernel_argv(self):
726 726 """add the profile-dir to arguments to be passed to IPython kernels"""
727 727 # FIXME: remove special treatment of IPython kernels
728 728 # Kernel should get *absolute* path to profile directory
729 729 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
730 730
731 731 def init_configurables(self):
732 732 # force Session default to be secure
733 733 default_secure(self.config)
734 734 kls = import_item(self.kernel_spec_manager_class)
735 735 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
736 736
737 737 kls = import_item(self.kernel_manager_class)
738 738 self.kernel_manager = kls(
739 739 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
740 740 connection_dir = self.profile_dir.security_dir,
741 741 )
742 742 kls = import_item(self.contents_manager_class)
743 743 self.contents_manager = kls(parent=self, log=self.log)
744 744 kls = import_item(self.session_manager_class)
745 745 self.session_manager = kls(parent=self, log=self.log,
746 746 kernel_manager=self.kernel_manager,
747 747 contents_manager=self.contents_manager)
748 748 kls = import_item(self.cluster_manager_class)
749 749 self.cluster_manager = kls(parent=self, log=self.log)
750 750 self.cluster_manager.update_profiles()
751 751
752 752 kls = import_item(self.config_manager_class)
753 753 self.config_manager = kls(parent=self, log=self.log,
754 754 profile_dir=self.profile_dir.location)
755 755
756 756 def init_logging(self):
757 757 # This prevents double log messages because tornado use a root logger that
758 758 # self.log is a child of. The logging module dipatches log messages to a log
759 759 # and all of its ancenstors until propagate is set to False.
760 760 self.log.propagate = False
761 761
762 762 for log in app_log, access_log, gen_log:
763 763 # consistent log output name (NotebookApp instead of tornado.access, etc.)
764 764 log.name = self.log.name
765 765 # hook up tornado 3's loggers to our app handlers
766 766 logger = logging.getLogger('tornado')
767 767 logger.propagate = True
768 768 logger.parent = self.log
769 769 logger.setLevel(self.log.level)
770 770
771 771 def init_webapp(self):
772 772 """initialize tornado webapp and httpserver"""
773 773 self.tornado_settings['allow_origin'] = self.allow_origin
774 774 if self.allow_origin_pat:
775 775 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
776 776 self.tornado_settings['allow_credentials'] = self.allow_credentials
777 777
778 778 self.web_app = NotebookWebApplication(
779 779 self, self.kernel_manager, self.contents_manager,
780 780 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
781 781 self.config_manager,
782 782 self.log, self.base_url, self.default_url, self.tornado_settings,
783 783 self.jinja_environment_options
784 784 )
785 785 if self.certfile:
786 786 ssl_options = dict(certfile=self.certfile)
787 787 if self.keyfile:
788 788 ssl_options['keyfile'] = self.keyfile
789 789 else:
790 790 ssl_options = None
791 791 self.web_app.password = self.password
792 792 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
793 793 xheaders=self.trust_xheaders)
794 794 if not self.ip:
795 795 warning = "WARNING: The notebook server is listening on all IP addresses"
796 796 if ssl_options is None:
797 797 self.log.critical(warning + " and not using encryption. This "
798 798 "is not recommended.")
799 799 if not self.password:
800 800 self.log.critical(warning + " and not using authentication. "
801 801 "This is highly insecure and not recommended.")
802 802 success = None
803 803 for port in random_ports(self.port, self.port_retries+1):
804 804 try:
805 805 self.http_server.listen(port, self.ip)
806 806 except socket.error as e:
807 807 if e.errno == errno.EADDRINUSE:
808 808 self.log.info('The port %i is already in use, trying another random port.' % port)
809 809 continue
810 810 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
811 811 self.log.warn("Permission to listen on port %i denied" % port)
812 812 continue
813 813 else:
814 814 raise
815 815 else:
816 816 self.port = port
817 817 success = True
818 818 break
819 819 if not success:
820 820 self.log.critical('ERROR: the notebook server could not be started because '
821 821 'no available port could be found.')
822 822 self.exit(1)
823 823
824 824 @property
825 825 def display_url(self):
826 826 ip = self.ip if self.ip else '[all ip addresses on your system]'
827 827 return self._url(ip)
828 828
829 829 @property
830 830 def connection_url(self):
831 831 ip = self.ip if self.ip else 'localhost'
832 832 return self._url(ip)
833 833
834 834 def _url(self, ip):
835 835 proto = 'https' if self.certfile else 'http'
836 836 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
837 837
838 838 def init_terminals(self):
839 839 try:
840 840 from .terminal import initialize
841 841 initialize(self.web_app)
842 842 self.web_app.settings['terminals_available'] = True
843 843 except ImportError as e:
844 844 self.log.info("Terminals not available (error was %s)", e)
845 845
846 846 def init_signal(self):
847 847 if not sys.platform.startswith('win'):
848 848 signal.signal(signal.SIGINT, self._handle_sigint)
849 849 signal.signal(signal.SIGTERM, self._signal_stop)
850 850 if hasattr(signal, 'SIGUSR1'):
851 851 # Windows doesn't support SIGUSR1
852 852 signal.signal(signal.SIGUSR1, self._signal_info)
853 853 if hasattr(signal, 'SIGINFO'):
854 854 # only on BSD-based systems
855 855 signal.signal(signal.SIGINFO, self._signal_info)
856 856
857 857 def _handle_sigint(self, sig, frame):
858 858 """SIGINT handler spawns confirmation dialog"""
859 859 # register more forceful signal handler for ^C^C case
860 860 signal.signal(signal.SIGINT, self._signal_stop)
861 861 # request confirmation dialog in bg thread, to avoid
862 862 # blocking the App
863 863 thread = threading.Thread(target=self._confirm_exit)
864 864 thread.daemon = True
865 865 thread.start()
866 866
867 867 def _restore_sigint_handler(self):
868 868 """callback for restoring original SIGINT handler"""
869 869 signal.signal(signal.SIGINT, self._handle_sigint)
870 870
871 871 def _confirm_exit(self):
872 872 """confirm shutdown on ^C
873 873
874 874 A second ^C, or answering 'y' within 5s will cause shutdown,
875 875 otherwise original SIGINT handler will be restored.
876 876
877 877 This doesn't work on Windows.
878 878 """
879 879 info = self.log.info
880 880 info('interrupted')
881 881 print(self.notebook_info())
882 882 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
883 883 sys.stdout.flush()
884 884 r,w,x = select.select([sys.stdin], [], [], 5)
885 885 if r:
886 886 line = sys.stdin.readline()
887 887 if line.lower().startswith('y') and 'n' not in line.lower():
888 888 self.log.critical("Shutdown confirmed")
889 889 ioloop.IOLoop.instance().stop()
890 890 return
891 891 else:
892 892 print("No answer for 5s:", end=' ')
893 893 print("resuming operation...")
894 894 # no answer, or answer is no:
895 895 # set it back to original SIGINT handler
896 896 # use IOLoop.add_callback because signal.signal must be called
897 897 # from main thread
898 898 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
899 899
900 900 def _signal_stop(self, sig, frame):
901 901 self.log.critical("received signal %s, stopping", sig)
902 902 ioloop.IOLoop.instance().stop()
903 903
904 904 def _signal_info(self, sig, frame):
905 905 print(self.notebook_info())
906 906
907 907 def init_components(self):
908 908 """Check the components submodule, and warn if it's unclean"""
909 909 status = submodule.check_submodule_status()
910 910 if status == 'missing':
911 911 self.log.warn("components submodule missing, running `git submodule update`")
912 912 submodule.update_submodules(submodule.ipython_parent())
913 913 elif status == 'unclean':
914 914 self.log.warn("components submodule unclean, you may see 404s on static/components")
915 915 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
916 916
917 917 @catch_config_error
918 918 def initialize(self, argv=None):
919 919 super(NotebookApp, self).initialize(argv)
920 920 self.init_logging()
921 921 self.init_kernel_argv()
922 922 self.init_configurables()
923 923 self.init_components()
924 924 self.init_webapp()
925 925 self.init_terminals()
926 926 self.init_signal()
927 927
928 928 def cleanup_kernels(self):
929 929 """Shutdown all kernels.
930 930
931 931 The kernels will shutdown themselves when this process no longer exists,
932 932 but explicit shutdown allows the KernelManagers to cleanup the connection files.
933 933 """
934 934 self.log.info('Shutting down kernels')
935 935 self.kernel_manager.shutdown_all()
936 936
937 937 def notebook_info(self):
938 938 "Return the current working directory and the server url information"
939 939 info = self.contents_manager.info_string() + "\n"
940 940 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
941 941 return info + "The IPython Notebook is running at: %s" % self.display_url
942 942
943 943 def server_info(self):
944 944 """Return a JSONable dict of information about this server."""
945 945 return {'url': self.connection_url,
946 946 'hostname': self.ip if self.ip else 'localhost',
947 947 'port': self.port,
948 948 'secure': bool(self.certfile),
949 949 'base_url': self.base_url,
950 950 'notebook_dir': os.path.abspath(self.notebook_dir),
951 951 'pid': os.getpid()
952 952 }
953 953
954 954 def write_server_info_file(self):
955 955 """Write the result of server_info() to the JSON file info_file."""
956 956 with open(self.info_file, 'w') as f:
957 957 json.dump(self.server_info(), f, indent=2)
958 958
959 959 def remove_server_info_file(self):
960 960 """Remove the nbserver-<pid>.json file created for this server.
961 961
962 962 Ignores the error raised when the file has already been removed.
963 963 """
964 964 try:
965 965 os.unlink(self.info_file)
966 966 except OSError as e:
967 967 if e.errno != errno.ENOENT:
968 968 raise
969 969
970 970 def start(self):
971 971 """ Start the IPython Notebook server app, after initialization
972 972
973 973 This method takes no arguments so all configuration and initialization
974 974 must be done prior to calling this method."""
975 975 if self.subapp is not None:
976 976 return self.subapp.start()
977 977
978 978 info = self.log.info
979 979 for line in self.notebook_info().split("\n"):
980 980 info(line)
981 981 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
982 982
983 983 self.write_server_info_file()
984 984
985 985 if self.open_browser or self.file_to_run:
986 986 try:
987 987 browser = webbrowser.get(self.browser or None)
988 988 except webbrowser.Error as e:
989 989 self.log.warn('No web browser found: %s.' % e)
990 990 browser = None
991 991
992 992 if self.file_to_run:
993 993 if not os.path.exists(self.file_to_run):
994 994 self.log.critical("%s does not exist" % self.file_to_run)
995 995 self.exit(1)
996 996
997 997 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
998 998 uri = url_path_join('notebooks', *relpath.split(os.sep))
999 999 else:
1000 1000 uri = 'tree'
1001 1001 if browser:
1002 1002 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1003 1003 new=2)
1004 1004 threading.Thread(target=b).start()
1005 1005 try:
1006 1006 ioloop.IOLoop.instance().start()
1007 1007 except KeyboardInterrupt:
1008 1008 info("Interrupted...")
1009 1009 finally:
1010 1010 self.cleanup_kernels()
1011 1011 self.remove_server_info_file()
1012 1012
1013 1013
1014 1014 def list_running_servers(profile='default'):
1015 1015 """Iterate over the server info files of running notebook servers.
1016 1016
1017 1017 Given a profile name, find nbserver-* files in the security directory of
1018 1018 that profile, and yield dicts of their information, each one pertaining to
1019 1019 a currently running notebook server instance.
1020 1020 """
1021 1021 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1022 1022 for file in os.listdir(pd.security_dir):
1023 1023 if file.startswith('nbserver-'):
1024 1024 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1025 1025 info = json.load(f)
1026 1026
1027 1027 # Simple check whether that process is really still running
1028 1028 if check_pid(info['pid']):
1029 1029 yield info
1030 1030 else:
1031 1031 # If the process has died, try to delete its info file
1032 1032 try:
1033 1033 os.unlink(file)
1034 1034 except OSError:
1035 1035 pass # TODO: This should warn or log or something
1036 1036 #-----------------------------------------------------------------------------
1037 1037 # Main entry point
1038 1038 #-----------------------------------------------------------------------------
1039 1039
1040 1040 launch_new_instance = NotebookApp.launch_instance
1041 1041
@@ -1,133 +1,139 b''
1 1 """Test the kernels service API."""
2 2
3 3 import json
4 4 import requests
5 5
6 6 from IPython.html.utils import url_path_join
7 7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
8 8
9 9 class KernelAPI(object):
10 10 """Wrapper for kernel REST API requests"""
11 11 def __init__(self, base_url):
12 12 self.base_url = base_url
13 13
14 14 def _req(self, verb, path, body=None):
15 15 response = requests.request(verb,
16 16 url_path_join(self.base_url, 'api/kernels', path), data=body)
17 17
18 18 if 400 <= response.status_code < 600:
19 19 try:
20 20 response.reason = response.json()['message']
21 21 except:
22 22 pass
23 23 response.raise_for_status()
24 24
25 25 return response
26 26
27 27 def list(self):
28 28 return self._req('GET', '')
29 29
30 30 def get(self, id):
31 31 return self._req('GET', id)
32 32
33 33 def start(self, name='python'):
34 34 body = json.dumps({'name': name})
35 35 return self._req('POST', '', body)
36 36
37 37 def shutdown(self, id):
38 38 return self._req('DELETE', id)
39 39
40 40 def interrupt(self, id):
41 41 return self._req('POST', url_path_join(id, 'interrupt'))
42 42
43 43 def restart(self, id):
44 44 return self._req('POST', url_path_join(id, 'restart'))
45 45
46 46 class KernelAPITest(NotebookTestBase):
47 47 """Test the kernels web service API"""
48 48 def setUp(self):
49 49 self.kern_api = KernelAPI(self.base_url())
50 50
51 51 def tearDown(self):
52 52 for k in self.kern_api.list().json():
53 53 self.kern_api.shutdown(k['id'])
54 54
55 55 def test__no_kernels(self):
56 56 """Make sure there are no kernels running at the start"""
57 57 kernels = self.kern_api.list().json()
58 58 self.assertEqual(kernels, [])
59 59
60 60 def test_default_kernel(self):
61 61 # POST request
62 62 r = self.kern_api._req('POST', '')
63 63 kern1 = r.json()
64 64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 65 self.assertEqual(r.status_code, 201)
66 66 self.assertIsInstance(kern1, dict)
67 67
68 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
68 self.assertEqual(r.headers['Content-Security-Policy'], (
69 "frame-ancestors 'self'; "
70 "report-uri /api/security/csp-report;"
71 ))
69 72
70 73 def test_main_kernel_handler(self):
71 74 # POST request
72 75 r = self.kern_api.start()
73 76 kern1 = r.json()
74 77 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
75 78 self.assertEqual(r.status_code, 201)
76 79 self.assertIsInstance(kern1, dict)
77 80
78 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
81 self.assertEqual(r.headers['Content-Security-Policy'], (
82 "frame-ancestors 'self'; "
83 "report-uri /api/security/csp-report;"
84 ))
79 85
80 86 # GET request
81 87 r = self.kern_api.list()
82 88 self.assertEqual(r.status_code, 200)
83 89 assert isinstance(r.json(), list)
84 90 self.assertEqual(r.json()[0]['id'], kern1['id'])
85 91 self.assertEqual(r.json()[0]['name'], kern1['name'])
86 92
87 93 # create another kernel and check that they both are added to the
88 94 # list of kernels from a GET request
89 95 kern2 = self.kern_api.start().json()
90 96 assert isinstance(kern2, dict)
91 97 r = self.kern_api.list()
92 98 kernels = r.json()
93 99 self.assertEqual(r.status_code, 200)
94 100 assert isinstance(kernels, list)
95 101 self.assertEqual(len(kernels), 2)
96 102
97 103 # Interrupt a kernel
98 104 r = self.kern_api.interrupt(kern2['id'])
99 105 self.assertEqual(r.status_code, 204)
100 106
101 107 # Restart a kernel
102 108 r = self.kern_api.restart(kern2['id'])
103 109 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
104 110 rekern = r.json()
105 111 self.assertEqual(rekern['id'], kern2['id'])
106 112 self.assertEqual(rekern['name'], kern2['name'])
107 113
108 114 def test_kernel_handler(self):
109 115 # GET kernel with given id
110 116 kid = self.kern_api.start().json()['id']
111 117 r = self.kern_api.get(kid)
112 118 kern1 = r.json()
113 119 self.assertEqual(r.status_code, 200)
114 120 assert isinstance(kern1, dict)
115 121 self.assertIn('id', kern1)
116 122 self.assertEqual(kern1['id'], kid)
117 123
118 124 # Request a bad kernel id and check that a JSON
119 125 # message is returned!
120 126 bad_id = '111-111-111-111-111'
121 127 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
122 128 self.kern_api.get(bad_id)
123 129
124 130 # DELETE kernel with id
125 131 r = self.kern_api.shutdown(kid)
126 132 self.assertEqual(r.status_code, 204)
127 133 kernels = self.kern_api.list().json()
128 134 self.assertEqual(kernels, [])
129 135
130 136 # Request to delete a non-existent kernel id
131 137 bad_id = '111-111-111-111-111'
132 138 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
133 139 self.kern_api.shutdown(bad_id)
@@ -1,195 +1,221 b''
1 1 =====================
2 2 Development version
3 3 =====================
4 4
5 5 This document describes in-flight development work.
6 6
7 7 .. warning::
8 8
9 9 Please do not edit this file by hand (doing so will likely cause merge
10 10 conflicts for other Pull Requests). Instead, create a new file in the
11 11 `docs/source/whatsnew/pr` folder
12 12
13 13 Using different kernels
14 14 -----------------------
15 15
16 16 .. image:: ../_images/kernel_selector_screenshot.png
17 17 :alt: Screenshot of notebook kernel selection dropdown menu
18 18 :align: center
19 19
20 20 You can now choose a kernel for a notebook within the user interface, rather
21 21 than starting up a separate notebook server for each kernel you want to use. The
22 22 syntax highlighting adapts to match the language you're working in.
23 23
24 24 Information about the kernel is stored in the notebook file, so when you open a
25 25 notebook, it will automatically start the correct kernel.
26 26
27 27 It is also easier to use the Qt console and the terminal console with other
28 28 kernels, using the --kernel flag::
29 29
30 30 ipython qtconsole --kernel bash
31 31 ipython console --kernel bash
32 32
33 33 # To list available kernels
34 34 ipython kernelspec list
35 35
36 36 Kernel authors should see :ref:`kernelspecs` for how to register their kernels
37 37 with IPython so that these mechanisms work.
38 38
39 39 Typing unicode identifiers
40 40 --------------------------
41 41
42 42 .. image:: /_images/unicode_completion.png
43 43
44 44 Complex expressions can be much cleaner when written with a wider choice of
45 45 characters. Python 3 allows unicode identifiers, and IPython 3 makes it easier
46 46 to type those, using a feature from Julia. Type a backslash followed by a LaTeX
47 47 style short name, such as ``\alpha``. Press tab, and it will turn into Ξ±.
48 48
49 49 Other new features
50 50 ------------------
51 51
52 52 * :class:`~.TextWidget` and :class:`~.TextareaWidget` objects now include a
53 53 ``placeholder`` attribute, for displaying placeholder text before the
54 54 user has typed anything.
55 55
56 56 * The :magic:`load` magic can now find the source for objects in the user namespace.
57 57 To enable searching the namespace, use the ``-n`` option.
58 58
59 59 .. sourcecode:: ipython
60 60
61 61 In [1]: %load -n my_module.some_function
62 62
63 63 * :class:`~.DirectView` objects have a new :meth:`~.DirectView.use_cloudpickle`
64 64 method, which works like ``view.use_dill()``, but causes the ``cloudpickle``
65 65 module from PiCloud's `cloud`__ library to be used rather than dill or the
66 66 builtin pickle module.
67 67
68 68 __ https://pypi.python.org/pypi/cloud
69 69
70 70 * Added a .ipynb exporter to nbconvert. It can be used by passing `--to notebook`
71 71 as a commandline argument to nbconvert.
72 72
73 73 * New nbconvert preprocessor called :class:`~.ClearOutputPreprocessor`. This
74 74 clears the output from IPython notebooks.
75 75
76 76 * New preprocessor for nbconvert that executes all the code cells in a notebook.
77 77 To run a notebook and save its output in a new notebook::
78 78
79 79 ipython nbconvert InputNotebook --ExecutePreprocessor.enabled=True --to notebook --output Executed
80 80
81 81 * Consecutive stream (stdout/stderr) output is merged into a single output
82 82 in the notebook document.
83 83 Previously, all output messages were preserved as separate output fields in the JSON.
84 84 Now, the same merge is applied to the stored output as the displayed output,
85 85 improving document load time for notebooks with many small outputs.
86 86
87 87 * ``NotebookApp.webapp_settings`` is deprecated and replaced with
88 88 the more informatively named ``NotebookApp.tornado_settings``.
89 89
90 90 * Using :magic:`timeit` prints warnings if there is atleast a 4x difference in timings
91 91 between the slowest and fastest runs, since this might meant that the multiple
92 92 runs are not independent of one another.
93 93
94 94 * It's now possible to provide mechanisms to integrate IPython with other event
95 95 loops, in addition to the ones we already support. This lets you run GUI code
96 96 in IPython with an interactive prompt, and to embed the IPython
97 97 kernel in GUI applications. See :doc:`/config/eventloops` for details. As part
98 98 of this, the direct ``enable_*`` and ``disable_*`` functions for various GUIs
99 99 in :mod:`IPython.lib.inputhook` have been deprecated in favour of
100 100 :meth:`~.InputHookManager.enable_gui` and :meth:`~.InputHookManager.disable_gui`.
101 101
102 102 * A ``ScrollManager`` was added to the notebook. The ``ScrollManager`` controls how the notebook document is scrolled using keyboard. Users can inherit from the ``ScrollManager`` or ``TargetScrollManager`` to customize how their notebook scrolls. The default ``ScrollManager`` is the ``SlideScrollManager``, which tries to scroll to the nearest slide or sub-slide cell.
103 103
104 104 * The function :func:`~IPython.html.widgets.interaction.interact_manual` has been
105 105 added which behaves similarly to :func:`~IPython.html.widgets.interaction.interact`,
106 106 but adds a button to explicitly run the interacted-with function, rather than
107 107 doing it automatically for every change of the parameter widgets. This should
108 108 be useful for long-running functions.
109 109
110 110 * The ``%cython`` magic is now part of the Cython module. Use `%load_ext Cython` with a version of Cython >= 0.21 to have access to the magic now.
111 111
112 112 * The Notebook application now offers integrated terminals on Unix platforms,
113 113 intended for when it is used on a remote server. To enable these, install
114 114 the ``terminado`` Python package.
115 115
116 116 * Setting the default highlighting language for nbconvert with the config option
117 117 ``NbConvertBase.default_language`` is deprecated. Nbconvert now respects
118 118 metadata stored in the :ref:`kernel spec <kernelspecs>`.
119 119
120 120 * IPython can now be configured systemwide, with files in :file:`/etc/ipython`
121 121 or :file:`/usr/local/etc/ipython` on Unix systems,
122 122 or :file:`{%PROGRAMDATA%}\\ipython` on Windows.
123 123
124 124 .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT.
125 125
126 126
127 127 Backwards incompatible changes
128 128 ------------------------------
129 129
130 130 * :func:`IPython.core.oinspect.getsource` call specification has changed:
131 131
132 132 * `oname` keyword argument has been added for property source formatting
133 133 * `is_binary` keyword argument has been dropped, passing ``True`` had
134 134 previously short-circuited the function to return ``None`` unconditionally
135 135
136 136 * Removed the octavemagic extension: it is now available as ``oct2py.ipython``.
137 137
138 138 * Creating PDFs with LaTeX no longer uses a post processor.
139 139 Use `nbconvert --to pdf` instead of `nbconvert --to latex --post pdf`.
140 140
141 141 * Used https://github.com/jdfreder/bootstrap2to3 to migrate the Notebook to Bootstrap 3.
142 142
143 143 Additional changes:
144 144
145 145 - Set `.tab-content .row` `0px;` left and right margin (bootstrap default is `-15px;`)
146 146 - Removed `height: @btn_mini_height;` from `.list_header>div, .list_item>div` in `tree.less`
147 147 - Set `#header` div `margin-bottom: 0px;`
148 148 - Set `#menus` to `float: left;`
149 149 - Set `#maintoolbar .navbar-text` to `float: none;`
150 150 - Added no-padding convienence class.
151 151 - Set border of #maintoolbar to 0px
152 152
153 153 * Accessing the `container` DOM object when displaying javascript has been
154 154 deprecated in IPython 2.0 in favor of accessing `element`. Starting with
155 155 IPython 3.0 trying to access `container` will raise an error in browser
156 156 javascript console.
157 157
158 158 * ``IPython.utils.py3compat.open`` was removed: :func:`io.open` provides all
159 159 the same functionality.
160 160
161 161 * The NotebookManager and ``/api/notebooks`` service has been replaced by
162 162 a more generic ContentsManager and ``/api/contents`` service,
163 163 which supports all kinds of files.
164 164 * The Dashboard now lists all files, not just notebooks and directories.
165 165 * The ``--script`` hook for saving notebooks to Python scripts is removed,
166 166 use :samp:`ipython nbconvert --to python {notebook}` instead.
167 167
168 168 * The ``rmagic`` extension is deprecated, as it is now part of rpy2. See
169 169 :mod:`rpy2.ipython.rmagic`.
170 170
171 171 * :meth:`~.KernelManager.start_kernel` and :meth:`~.KernelManager.format_kernel_cmd`
172 172 no longer accept a ``executable`` parameter. Use the kernelspec machinery instead.
173 173
174 174 * The widget classes have been renamed from `*Widget` to `*`. The old names are
175 175 still functional, but are deprecated. i.e. `IntSliderWidget` has been renamed
176 176 to `IntSlider`.
177 177 * The ContainerWidget was renamed to Box and no longer defaults as a flexible
178 178 box in the web browser. A new FlexBox widget was added, which allows you to
179 179 use the flexible box model.
180 180
181 181 .. DO NOT EDIT THIS LINE BEFORE RELEASE. INCOMPAT INSERTION POINT.
182 182
183 IFrame embedding
184 ````````````````
183 Content Security Policy
184 ```````````````````````
185 185
186 The IPython Notebook and its APIs by default will only be allowed to be
187 embedded in an iframe on the same origin.
186 The Content Security Policy is a web standard for adding a layer of security to
187 detect and mitigate certain classes of attacks, including Cross Site Scripting
188 (XSS) and data injection attacks. This was introduced into the notebook to
189 ensure that the IPython Notebook and its APIs (by default) can only be embedded
190 in an iframe on the same origin.
188 191
189 To override this, set ``headers[X-Frame-Options]`` to one of
192 Override ``headers['Content-Security-Policy']`` within your notebook
193 configuration to extend for alternate domains and security settings.::
190 194
191 * DENY
192 * SAMEORIGIN
193 * ALLOW-FROM uri
195 c.NotebookApp.tornado_settings = {
196 'headers': {
197 'Content-Security-Policy': "frame-ancestors 'self'"
198 }
199 }
194 200
195 See `Mozilla's guide to X-Frame-Options <https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options>`_ for more examples.
201 Example policies::
202
203 Content-Security-Policy: default-src 'self' https://*.jupyter.org
204
205 Matches embeddings on any subdomain of jupyter.org, so long as they are served
206 over SSL.
207
208 There is a `report-uri <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives#report-uri>`_ endpoint available for logging CSP violations, located at
209 ``/api/security/csp-report``. To use it, set ``report-uri`` as part of the CSP::
210
211 c.NotebookApp.tornado_settings = {
212 'headers': {
213 'Content-Security-Policy': "frame-ancestors 'self'; report-uri /api/security/csp-report"
214 }
215 }
216
217 It simply provides the CSP report as a warning in IPython's logs. The default
218 CSP sets this report-uri relative to the ``base_url`` (not shown above).
219
220 For a more thorough and accurate guide on Content Security Policies, check out
221 `MDN's Using Content Security Policy <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_Content_Security_Policy>`_ for more examples.
General Comments 0
You need to be logged in to leave comments. Login now