##// END OF EJS Templates
Added authentication configuration for the notebook app.
Phil Elson -
Show More
@@ -1,62 +1,86 b''
1 1 """Tornado handlers logging into the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 * Phil Elson
6 7 """
7 8
8 9 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2014 The IPython Development Team
10 11 #
11 12 # Distributed under the terms of the BSD License. The full license is in
12 13 # the file COPYING, distributed as part of this software.
13 14 #-----------------------------------------------------------------------------
14 15
15 16 #-----------------------------------------------------------------------------
16 17 # Imports
17 18 #-----------------------------------------------------------------------------
18 19
19 20 import uuid
20 21
21 22 from tornado.escape import url_escape
23 from tornado import web
22 24
25 from IPython.config.configurable import Configurable
23 26 from IPython.lib.security import passwd_check
24 27
25 28 from ..base.handlers import IPythonHandler
26 29
27 30 #-----------------------------------------------------------------------------
28 31 # Handler
29 32 #-----------------------------------------------------------------------------
30 33
31 34 class LoginHandler(IPythonHandler):
32
35 """ The basic IPythonWebApplication login handler which authenticates with a
36 hashed password from the configuration.
37
38 """
33 39 def _render(self, message=None):
34 40 self.write(self.render_template('login.html',
35 41 next=url_escape(self.get_argument('next', default=self.base_url)),
36 42 message=message,
37 43 ))
38 44
39 45 def get(self):
40 46 if self.current_user:
41 47 self.redirect(self.get_argument('next', default=self.base_url))
42 48 else:
43 49 self._render()
44 50
45 51 def post(self):
46 pwd = self.get_argument('password', default=u'')
47 if self.login_available:
48 if passwd_check(self.password, pwd):
52 hashed_password = self.password_from_configuration(self.application)
53 typed_password = self.get_argument('password', default=u'')
54 if self.login_available(self.application):
55 if passwd_check(hashed_password, typed_password):
49 56 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
50 57 else:
51 58 self._render(message={'error': 'Invalid password'})
52 59 return
53 60
54 61 self.redirect(self.get_argument('next', default=self.base_url))
55
56
57 #-----------------------------------------------------------------------------
58 # URL to handler mappings
59 #-----------------------------------------------------------------------------
60
61
62 default_handlers = [(r"/login", LoginHandler)]
62
63 @classmethod
64 def validate_notebook_app_security(cls, notebook_app, ssl_options=None):
65 if not notebook_app.ip:
66 warning = "WARNING: The notebook server is listening on all IP addresses"
67 if ssl_options is None:
68 notebook_app.log.critical(warning + " and not using encryption. This "
69 "is not recommended.")
70 if not self.password_from_configuration(notebook_app):
71 notebook_app.log.critical(warning + " and not using authentication. "
72 "This is highly insecure and not recommended.")
73
74 @staticmethod
75 def password_from_configuration(webapp):
76 """ Return the hashed password from the given NotebookWebApplication's configuration.
77
78 If there is no configured password, None will be returned.
79
80 """
81 return webapp.settings['config']['NotebookApp'].get('password', None)
82
83 @classmethod
84 def login_available(cls, webapp):
85 """Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
86 return bool(cls.password_from_configuration(webapp))
@@ -1,514 +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 35 from IPython.html.services.security import csp_report_uri
36 36
37 37 #-----------------------------------------------------------------------------
38 38 # Top-level handlers
39 39 #-----------------------------------------------------------------------------
40 40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
41 41
42 42 sys_info = json.dumps(get_sys_info())
43 43
44 44 class AuthenticatedHandler(web.RequestHandler):
45 45 """A RequestHandler with an authenticated user."""
46 46
47 47 def set_default_headers(self):
48 48 headers = self.settings.get('headers', {})
49 49
50 50 if "Content-Security-Policy" not in headers:
51 51 headers["Content-Security-Policy"] = (
52 52 "frame-ancestors 'self'; "
53 53 # Make sure the report-uri is relative to the base_url
54 54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
55 55 )
56 56
57 57 # Allow for overriding headers
58 58 for header_name,value in headers.items() :
59 59 try:
60 60 self.set_header(header_name, value)
61 61 except Exception as e:
62 62 # tornado raise Exception (not a subclass)
63 63 # if method is unsupported (websocket and Access-Control-Allow-Origin
64 64 # for example, so just ignore)
65 65 self.log.debug(e)
66 66
67 67 def clear_login_cookie(self):
68 68 self.clear_cookie(self.cookie_name)
69 69
70 70 def get_current_user(self):
71 71 user_id = self.get_secure_cookie(self.cookie_name)
72 72 # For now the user_id should not return empty, but it could eventually
73 73 if user_id == '':
74 74 user_id = 'anonymous'
75 75 if user_id is None:
76 76 # prevent extra Invalid cookie sig warnings:
77 77 self.clear_login_cookie()
78 78 if not self.login_available:
79 79 user_id = 'anonymous'
80 80 return user_id
81 81
82 82 @property
83 83 def cookie_name(self):
84 84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
85 85 self.request.host
86 86 ))
87 87 return self.settings.get('cookie_name', default_cookie_name)
88 88
89 89 @property
90 def password(self):
91 """our password"""
92 return self.settings.get('password', '')
93
94 @property
95 90 def logged_in(self):
96 91 """Is a user currently logged in?
97 92
98 93 """
99 94 user = self.get_current_user()
100 95 return (user and not user == 'anonymous')
101 96
102 97 @property
98 def _login_handler(self):
99 """Return the login handler for this application."""
100 return self.settings['login_handler_class']
101
102 @property
103 103 def login_available(self):
104 104 """May a user proceed to log in?
105 105
106 106 This returns True if login capability is available, irrespective of
107 107 whether the user is already logged in or not.
108 108
109 109 """
110 return bool(self.settings.get('password', ''))
110 return bool(self._login_handler.login_available(self.application))
111 111
112 112
113 113 class IPythonHandler(AuthenticatedHandler):
114 114 """IPython-specific extensions to authenticated handling
115 115
116 116 Mostly property shortcuts to IPython-specific settings.
117 117 """
118 118
119 119 @property
120 120 def config(self):
121 121 return self.settings.get('config', None)
122 122
123 123 @property
124 124 def log(self):
125 125 """use the IPython log by default, falling back on tornado's logger"""
126 126 if Application.initialized():
127 127 return Application.instance().log
128 128 else:
129 129 return app_log
130 130
131 131 #---------------------------------------------------------------
132 132 # URLs
133 133 #---------------------------------------------------------------
134 134
135 135 @property
136 136 def version_hash(self):
137 137 """The version hash to use for cache hints for static files"""
138 138 return self.settings.get('version_hash', '')
139 139
140 140 @property
141 141 def mathjax_url(self):
142 142 return self.settings.get('mathjax_url', '')
143 143
144 144 @property
145 145 def base_url(self):
146 146 return self.settings.get('base_url', '/')
147 147
148 148 @property
149 149 def ws_url(self):
150 150 return self.settings.get('websocket_url', '')
151 151
152 152 @property
153 153 def contents_js_source(self):
154 154 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
155 155 'services/contents'))
156 156 return self.settings.get('contents_js_source', 'services/contents')
157 157
158 158 #---------------------------------------------------------------
159 159 # Manager objects
160 160 #---------------------------------------------------------------
161 161
162 162 @property
163 163 def kernel_manager(self):
164 164 return self.settings['kernel_manager']
165 165
166 166 @property
167 167 def contents_manager(self):
168 168 return self.settings['contents_manager']
169 169
170 170 @property
171 171 def cluster_manager(self):
172 172 return self.settings['cluster_manager']
173 173
174 174 @property
175 175 def session_manager(self):
176 176 return self.settings['session_manager']
177 177
178 178 @property
179 179 def terminal_manager(self):
180 180 return self.settings['terminal_manager']
181 181
182 182 @property
183 183 def kernel_spec_manager(self):
184 184 return self.settings['kernel_spec_manager']
185 185
186 186 @property
187 187 def config_manager(self):
188 188 return self.settings['config_manager']
189 189
190 190 #---------------------------------------------------------------
191 191 # CORS
192 192 #---------------------------------------------------------------
193 193
194 194 @property
195 195 def allow_origin(self):
196 196 """Normal Access-Control-Allow-Origin"""
197 197 return self.settings.get('allow_origin', '')
198 198
199 199 @property
200 200 def allow_origin_pat(self):
201 201 """Regular expression version of allow_origin"""
202 202 return self.settings.get('allow_origin_pat', None)
203 203
204 204 @property
205 205 def allow_credentials(self):
206 206 """Whether to set Access-Control-Allow-Credentials"""
207 207 return self.settings.get('allow_credentials', False)
208 208
209 209 def set_default_headers(self):
210 210 """Add CORS headers, if defined"""
211 211 super(IPythonHandler, self).set_default_headers()
212 212 if self.allow_origin:
213 213 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
214 214 elif self.allow_origin_pat:
215 215 origin = self.get_origin()
216 216 if origin and self.allow_origin_pat.match(origin):
217 217 self.set_header("Access-Control-Allow-Origin", origin)
218 218 if self.allow_credentials:
219 219 self.set_header("Access-Control-Allow-Credentials", 'true')
220 220
221 221 def get_origin(self):
222 222 # Handle WebSocket Origin naming convention differences
223 223 # The difference between version 8 and 13 is that in 8 the
224 224 # client sends a "Sec-Websocket-Origin" header and in 13 it's
225 225 # simply "Origin".
226 226 if "Origin" in self.request.headers:
227 227 origin = self.request.headers.get("Origin")
228 228 else:
229 229 origin = self.request.headers.get("Sec-Websocket-Origin", None)
230 230 return origin
231 231
232 232 #---------------------------------------------------------------
233 233 # template rendering
234 234 #---------------------------------------------------------------
235 235
236 236 def get_template(self, name):
237 237 """Return the jinja template object for a given name"""
238 238 return self.settings['jinja2_env'].get_template(name)
239 239
240 240 def render_template(self, name, **ns):
241 241 ns.update(self.template_namespace)
242 242 template = self.get_template(name)
243 243 return template.render(**ns)
244 244
245 245 @property
246 246 def template_namespace(self):
247 247 return dict(
248 248 base_url=self.base_url,
249 249 ws_url=self.ws_url,
250 250 logged_in=self.logged_in,
251 251 login_available=self.login_available,
252 252 static_url=self.static_url,
253 253 sys_info=sys_info,
254 254 contents_js_source=self.contents_js_source,
255 255 version_hash=self.version_hash,
256 256 )
257 257
258 258 def get_json_body(self):
259 259 """Return the body of the request as JSON data."""
260 260 if not self.request.body:
261 261 return None
262 262 # Do we need to call body.decode('utf-8') here?
263 263 body = self.request.body.strip().decode(u'utf-8')
264 264 try:
265 265 model = json.loads(body)
266 266 except Exception:
267 267 self.log.debug("Bad JSON: %r", body)
268 268 self.log.error("Couldn't parse JSON", exc_info=True)
269 269 raise web.HTTPError(400, u'Invalid JSON in body of request')
270 270 return model
271 271
272 272 def write_error(self, status_code, **kwargs):
273 273 """render custom error pages"""
274 274 exc_info = kwargs.get('exc_info')
275 275 message = ''
276 276 status_message = responses.get(status_code, 'Unknown HTTP Error')
277 277 if exc_info:
278 278 exception = exc_info[1]
279 279 # get the custom message, if defined
280 280 try:
281 281 message = exception.log_message % exception.args
282 282 except Exception:
283 283 pass
284 284
285 285 # construct the custom reason, if defined
286 286 reason = getattr(exception, 'reason', '')
287 287 if reason:
288 288 status_message = reason
289 289
290 290 # build template namespace
291 291 ns = dict(
292 292 status_code=status_code,
293 293 status_message=status_message,
294 294 message=message,
295 295 exception=exception,
296 296 )
297 297
298 298 self.set_header('Content-Type', 'text/html')
299 299 # render the template
300 300 try:
301 301 html = self.render_template('%s.html' % status_code, **ns)
302 302 except TemplateNotFound:
303 303 self.log.debug("No template for %d", status_code)
304 304 html = self.render_template('error.html', **ns)
305 305
306 306 self.write(html)
307 307
308 308
309 309
310 310 class Template404(IPythonHandler):
311 311 """Render our 404 template"""
312 312 def prepare(self):
313 313 raise web.HTTPError(404)
314 314
315 315
316 316 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
317 317 """static files should only be accessible when logged in"""
318 318
319 319 @web.authenticated
320 320 def get(self, path):
321 321 if os.path.splitext(path)[1] == '.ipynb':
322 322 name = path.rsplit('/', 1)[-1]
323 323 self.set_header('Content-Type', 'application/json')
324 324 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
325 325
326 326 return web.StaticFileHandler.get(self, path)
327 327
328 328 def set_headers(self):
329 329 super(AuthenticatedFileHandler, self).set_headers()
330 330 # disable browser caching, rely on 304 replies for savings
331 331 if "v" not in self.request.arguments:
332 332 self.add_header("Cache-Control", "no-cache")
333 333
334 334 def compute_etag(self):
335 335 return None
336 336
337 337 def validate_absolute_path(self, root, absolute_path):
338 338 """Validate and return the absolute path.
339 339
340 340 Requires tornado 3.1
341 341
342 342 Adding to tornado's own handling, forbids the serving of hidden files.
343 343 """
344 344 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
345 345 abs_root = os.path.abspath(root)
346 346 if is_hidden(abs_path, abs_root):
347 347 self.log.info("Refusing to serve hidden file, via 404 Error")
348 348 raise web.HTTPError(404)
349 349 return abs_path
350 350
351 351
352 352 def json_errors(method):
353 353 """Decorate methods with this to return GitHub style JSON errors.
354 354
355 355 This should be used on any JSON API on any handler method that can raise HTTPErrors.
356 356
357 357 This will grab the latest HTTPError exception using sys.exc_info
358 358 and then:
359 359
360 360 1. Set the HTTP status code based on the HTTPError
361 361 2. Create and return a JSON body with a message field describing
362 362 the error in a human readable form.
363 363 """
364 364 @functools.wraps(method)
365 365 def wrapper(self, *args, **kwargs):
366 366 try:
367 367 result = method(self, *args, **kwargs)
368 368 except web.HTTPError as e:
369 369 status = e.status_code
370 370 message = e.log_message
371 371 self.log.warn(message)
372 372 self.set_status(e.status_code)
373 373 self.finish(json.dumps(dict(message=message)))
374 374 except Exception:
375 375 self.log.error("Unhandled error in API request", exc_info=True)
376 376 status = 500
377 377 message = "Unknown server error"
378 378 t, value, tb = sys.exc_info()
379 379 self.set_status(status)
380 380 tb_text = ''.join(traceback.format_exception(t, value, tb))
381 381 reply = dict(message=message, traceback=tb_text)
382 382 self.finish(json.dumps(reply))
383 383 else:
384 384 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 def get(self, path=''):
473 473 cm = self.contents_manager
474 474 if cm.dir_exists(path):
475 475 # it's a *directory*, redirect to /tree
476 476 url = url_path_join(self.base_url, 'tree', path)
477 477 else:
478 478 orig_path = path
479 479 # otherwise, redirect to /files
480 480 parts = path.split('/')
481 481
482 482 if not cm.file_exists(path=path) and 'files' in parts:
483 483 # redirect without files/ iff it would 404
484 484 # this preserves pre-2.0-style 'files/' links
485 485 self.log.warn("Deprecated files/ URL: %s", orig_path)
486 486 parts.remove('files')
487 487 path = '/'.join(parts)
488 488
489 489 if not cm.file_exists(path=path):
490 490 raise web.HTTPError(404)
491 491
492 492 url = url_path_join(self.base_url, 'files', path)
493 493 url = url_escape(url)
494 494 self.log.debug("Redirecting %s to %s", self.request.path, url)
495 495 self.redirect(url)
496 496
497 497
498 498 #-----------------------------------------------------------------------------
499 499 # URL pattern fragments for re-use
500 500 #-----------------------------------------------------------------------------
501 501
502 502 # path matches any number of `/foo[/bar...]` or just `/` or ''
503 503 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
504 504 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
505 505
506 506 #-----------------------------------------------------------------------------
507 507 # URL to handler mappings
508 508 #-----------------------------------------------------------------------------
509 509
510 510
511 511 default_handlers = [
512 512 (r".*/", TrailingSlashHandler),
513 513 (r"api", ApiVersionHandler)
514 514 ]
@@ -1,1042 +1,1038 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 login_handler_class=ipython_app.login_handler_class,
185 186 password=ipython_app.password,
186
187
187 188 # managers
188 189 kernel_manager=kernel_manager,
189 190 contents_manager=contents_manager,
190 191 cluster_manager=cluster_manager,
191 192 session_manager=session_manager,
192 193 kernel_spec_manager=kernel_spec_manager,
193 194 config_manager=config_manager,
194 195
195 196 # IPython stuff
196 nbextensions_path = ipython_app.nbextensions_path,
197 nbextensions_path=ipython_app.nbextensions_path,
197 198 websocket_url=ipython_app.websocket_url,
198 199 mathjax_url=ipython_app.mathjax_url,
199 200 config=ipython_app.config,
200 201 jinja2_env=env,
201 202 terminals_available=False, # Set later if terminals are available
202 203 )
203 204
204 205 # allow custom overrides for the tornado web app.
205 206 settings.update(settings_overrides)
206 207 return settings
207 208
208 209 def init_handlers(self, settings):
209 210 """Load the (URL pattern, handler) tuples for each component."""
210 211
211 212 # Order matters. The first handler to match the URL will handle the request.
212 213 handlers = []
213 214 handlers.extend(load_handlers('tree.handlers'))
214 handlers.extend(load_handlers('auth.login'))
215 handlers.extend([(r"/login", settings['login_handler_class'])])
215 216 handlers.extend(load_handlers('auth.logout'))
216 217 handlers.extend(load_handlers('files.handlers'))
217 218 handlers.extend(load_handlers('notebook.handlers'))
218 219 handlers.extend(load_handlers('nbconvert.handlers'))
219 220 handlers.extend(load_handlers('kernelspecs.handlers'))
220 221 handlers.extend(load_handlers('edit.handlers'))
221 222 handlers.extend(load_handlers('services.config.handlers'))
222 223 handlers.extend(load_handlers('services.kernels.handlers'))
223 224 handlers.extend(load_handlers('services.contents.handlers'))
224 225 handlers.extend(load_handlers('services.clusters.handlers'))
225 226 handlers.extend(load_handlers('services.sessions.handlers'))
226 227 handlers.extend(load_handlers('services.nbconvert.handlers'))
227 228 handlers.extend(load_handlers('services.kernelspecs.handlers'))
228 229 handlers.extend(load_handlers('services.security.handlers'))
229 230 handlers.append(
230 231 (r"/nbextensions/(.*)", FileFindHandler, {
231 232 'path': settings['nbextensions_path'],
232 233 'no_cache_paths': ['/'], # don't cache anything in nbextensions
233 234 }),
234 235 )
235 236 # register base handlers last
236 237 handlers.extend(load_handlers('base.handlers'))
237 238 # set the URL that will be redirected from `/`
238 239 handlers.append(
239 240 (r'/?', web.RedirectHandler, {
240 241 'url' : url_path_join(settings['base_url'], settings['default_url']),
241 242 'permanent': False, # want 302, not 301
242 243 })
243 244 )
244 245 # prepend base_url onto the patterns that we match
245 246 new_handlers = []
246 247 for handler in handlers:
247 248 pattern = url_path_join(settings['base_url'], handler[0])
248 249 new_handler = tuple([pattern] + list(handler[1:]))
249 250 new_handlers.append(new_handler)
250 251 # add 404 on the end, which will catch everything that falls through
251 252 new_handlers.append((r'(.*)', Template404))
252 253 return new_handlers
253 254
254 255
255 256 class NbserverListApp(BaseIPythonApplication):
256 257
257 258 description="List currently running notebook servers in this profile."
258 259
259 260 flags = dict(
260 261 json=({'NbserverListApp': {'json': True}},
261 262 "Produce machine-readable JSON output."),
262 263 )
263 264
264 265 json = Bool(False, config=True,
265 266 help="If True, each line of output will be a JSON object with the "
266 267 "details from the server info file.")
267 268
268 269 def start(self):
269 270 if not self.json:
270 271 print("Currently running servers:")
271 272 for serverinfo in list_running_servers(self.profile):
272 273 if self.json:
273 274 print(json.dumps(serverinfo))
274 275 else:
275 276 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
276 277
277 278 #-----------------------------------------------------------------------------
278 279 # Aliases and Flags
279 280 #-----------------------------------------------------------------------------
280 281
281 282 flags = dict(base_flags)
282 283 flags['no-browser']=(
283 284 {'NotebookApp' : {'open_browser' : False}},
284 285 "Don't open the notebook in a browser after startup."
285 286 )
286 287 flags['pylab']=(
287 288 {'NotebookApp' : {'pylab' : 'warn'}},
288 289 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
289 290 )
290 291 flags['no-mathjax']=(
291 292 {'NotebookApp' : {'enable_mathjax' : False}},
292 293 """Disable MathJax
293 294
294 295 MathJax is the javascript library IPython uses to render math/LaTeX. It is
295 296 very large, so you may want to disable it if you have a slow internet
296 297 connection, or for offline use of the notebook.
297 298
298 299 When disabled, equations etc. will appear as their untransformed TeX source.
299 300 """
300 301 )
301 302
302 303 # Add notebook manager flags
303 304 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
304 305 'DEPRECATED, IGNORED',
305 306 'DEPRECATED, IGNORED'))
306 307
307 308 aliases = dict(base_aliases)
308 309
309 310 aliases.update({
310 311 'ip': 'NotebookApp.ip',
311 312 'port': 'NotebookApp.port',
312 313 'port-retries': 'NotebookApp.port_retries',
313 314 'transport': 'KernelManager.transport',
314 315 'keyfile': 'NotebookApp.keyfile',
315 316 'certfile': 'NotebookApp.certfile',
316 317 'notebook-dir': 'NotebookApp.notebook_dir',
317 318 'browser': 'NotebookApp.browser',
318 319 'pylab': 'NotebookApp.pylab',
319 320 })
320 321
321 322 #-----------------------------------------------------------------------------
322 323 # NotebookApp
323 324 #-----------------------------------------------------------------------------
324 325
325 326 class NotebookApp(BaseIPythonApplication):
326 327
327 328 name = 'ipython-notebook'
328 329
329 330 description = """
330 331 The IPython HTML Notebook.
331 332
332 333 This launches a Tornado based HTML Notebook Server that serves up an
333 334 HTML5/Javascript Notebook client.
334 335 """
335 336 examples = _examples
336 337 aliases = aliases
337 338 flags = flags
338 339
339 340 classes = [
340 341 KernelManager, ProfileDir, Session, MappingKernelManager,
341 342 ContentsManager, FileContentsManager, NotebookNotary,
342 343 ]
343 344 flags = Dict(flags)
344 345 aliases = Dict(aliases)
345 346
346 347 subcommands = dict(
347 348 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
348 349 )
349 350
350 351 ipython_kernel_argv = List(Unicode)
351 352
352 353 _log_formatter_cls = LogFormatter
353 354
354 355 def _log_level_default(self):
355 356 return logging.INFO
356 357
357 358 def _log_datefmt_default(self):
358 359 """Exclude date from default date format"""
359 360 return "%H:%M:%S"
360 361
361 362 def _log_format_default(self):
362 363 """override default log format to include time"""
363 364 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
364 365
365 366 # create requested profiles by default, if they don't exist:
366 367 auto_create = Bool(True)
367 368
368 369 # file to be opened in the notebook server
369 370 file_to_run = Unicode('', config=True)
370 371
371 372 # Network related information
372 373
373 374 allow_origin = Unicode('', config=True,
374 375 help="""Set the Access-Control-Allow-Origin header
375 376
376 377 Use '*' to allow any origin to access your server.
377 378
378 379 Takes precedence over allow_origin_pat.
379 380 """
380 381 )
381 382
382 383 allow_origin_pat = Unicode('', config=True,
383 384 help="""Use a regular expression for the Access-Control-Allow-Origin header
384 385
385 386 Requests from an origin matching the expression will get replies with:
386 387
387 388 Access-Control-Allow-Origin: origin
388 389
389 390 where `origin` is the origin of the request.
390 391
391 392 Ignored if allow_origin is set.
392 393 """
393 394 )
394 395
395 396 allow_credentials = Bool(False, config=True,
396 397 help="Set the Access-Control-Allow-Credentials: true header"
397 398 )
398 399
399 400 default_url = Unicode('/tree', config=True,
400 401 help="The default URL to redirect to from `/`"
401 402 )
402 403
403 404 ip = Unicode('localhost', config=True,
404 405 help="The IP address the notebook server will listen on."
405 406 )
406 407
407 408 def _ip_changed(self, name, old, new):
408 409 if new == u'*': self.ip = u''
409 410
410 411 port = Integer(8888, config=True,
411 412 help="The port the notebook server will listen on."
412 413 )
413 414 port_retries = Integer(50, config=True,
414 415 help="The number of additional ports to try if the specified port is not available."
415 416 )
416 417
417 418 certfile = Unicode(u'', config=True,
418 419 help="""The full path to an SSL/TLS certificate file."""
419 420 )
420 421
421 422 keyfile = Unicode(u'', config=True,
422 423 help="""The full path to a private key file for usage with SSL/TLS."""
423 424 )
424 425
425 426 cookie_secret_file = Unicode(config=True,
426 427 help="""The file where the cookie secret is stored."""
427 428 )
428 429 def _cookie_secret_file_default(self):
429 430 if self.profile_dir is None:
430 431 return ''
431 432 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
432 433
433 434 cookie_secret = Bytes(b'', config=True,
434 435 help="""The random bytes used to secure cookies.
435 436 By default this is a new random number every time you start the Notebook.
436 437 Set it to a value in a config file to enable logins to persist across server sessions.
437 438
438 439 Note: Cookie secrets should be kept private, do not share config files with
439 440 cookie_secret stored in plaintext (you can read the value from a file).
440 441 """
441 442 )
442 443 def _cookie_secret_default(self):
443 444 if os.path.exists(self.cookie_secret_file):
444 445 with io.open(self.cookie_secret_file, 'rb') as f:
445 446 return f.read()
446 447 else:
447 448 secret = base64.encodestring(os.urandom(1024))
448 449 self._write_cookie_secret_file(secret)
449 450 return secret
450 451
451 452 def _write_cookie_secret_file(self, secret):
452 453 """write my secret to my secret_file"""
453 454 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
454 455 with io.open(self.cookie_secret_file, 'wb') as f:
455 456 f.write(secret)
456 457 try:
457 458 os.chmod(self.cookie_secret_file, 0o600)
458 459 except OSError:
459 460 self.log.warn(
460 461 "Could not set permissions on %s",
461 462 self.cookie_secret_file
462 463 )
463 464
464 465 password = Unicode(u'', config=True,
465 466 help="""Hashed password to use for web authentication.
466 467
467 468 To generate, type in a python/IPython shell:
468 469
469 470 from IPython.lib import passwd; passwd()
470 471
471 472 The string should be of the form type:salt:hashed-password.
472 473 """
473 474 )
474 475
475 476 open_browser = Bool(True, config=True,
476 477 help="""Whether to open in a browser after starting.
477 478 The specific browser used is platform dependent and
478 479 determined by the python standard library `webbrowser`
479 480 module, unless it is overridden using the --browser
480 481 (NotebookApp.browser) configuration option.
481 482 """)
482 483
483 484 browser = Unicode(u'', config=True,
484 485 help="""Specify what command to use to invoke a web
485 486 browser when opening the notebook. If not specified, the
486 487 default browser will be determined by the `webbrowser`
487 488 standard library module, which allows setting of the
488 489 BROWSER environment variable to override it.
489 490 """)
490 491
491 492 webapp_settings = Dict(config=True,
492 493 help="DEPRECATED, use tornado_settings"
493 494 )
494 495 def _webapp_settings_changed(self, name, old, new):
495 496 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
496 497 self.tornado_settings = new
497 498
498 499 tornado_settings = Dict(config=True,
499 500 help="Supply overrides for the tornado.web.Application that the "
500 501 "IPython notebook uses.")
501 502
502 503 jinja_environment_options = Dict(config=True,
503 504 help="Supply extra arguments that will be passed to Jinja environment.")
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
643 642 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
644 643 config=True,
645 644 help="""
646 645 The kernel spec manager class to use. Should be a subclass
647 646 of `IPython.kernel.kernelspec.KernelSpecManager`.
648 647
649 648 The Api of KernelSpecManager is provisional and might change
650 649 without warning between this version of IPython and the next stable one.
651 650 """)
652 651
652 login_handler = DottedObjectName('IPython.html.auth.login.LoginHandler',
653 config=True,
654 help='The login handler class to use.')
655
653 656 trust_xheaders = Bool(False, config=True,
654 657 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
655 658 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
656 659 )
657 660
658 661 info_file = Unicode()
659 662
660 663 def _info_file_default(self):
661 664 info_file = "nbserver-%s.json"%os.getpid()
662 665 return os.path.join(self.profile_dir.security_dir, info_file)
663 666
664 667 pylab = Unicode('disabled', config=True,
665 668 help="""
666 669 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
667 670 """
668 671 )
669 672 def _pylab_changed(self, name, old, new):
670 673 """when --pylab is specified, display a warning and exit"""
671 674 if new != 'warn':
672 675 backend = ' %s' % new
673 676 else:
674 677 backend = ''
675 678 self.log.error("Support for specifying --pylab on the command line has been removed.")
676 679 self.log.error(
677 680 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
678 681 )
679 682 self.exit(1)
680 683
681 684 notebook_dir = Unicode(config=True,
682 685 help="The directory to use for notebooks and kernels."
683 686 )
684 687
685 688 def _notebook_dir_default(self):
686 689 if self.file_to_run:
687 690 return os.path.dirname(os.path.abspath(self.file_to_run))
688 691 else:
689 692 return py3compat.getcwd()
690 693
691 694 def _notebook_dir_changed(self, name, old, new):
692 695 """Do a bit of validation of the notebook dir."""
693 696 if not os.path.isabs(new):
694 697 # If we receive a non-absolute path, make it absolute.
695 698 self.notebook_dir = os.path.abspath(new)
696 699 return
697 700 if not os.path.isdir(new):
698 701 raise TraitError("No such notebook dir: %r" % new)
699 702
700 703 # setting App.notebook_dir implies setting notebook and kernel dirs as well
701 704 self.config.FileContentsManager.root_dir = new
702 705 self.config.MappingKernelManager.root_dir = new
703
704 706
705 707 def parse_command_line(self, argv=None):
706 708 super(NotebookApp, self).parse_command_line(argv)
707 709
708 710 if self.extra_args:
709 711 arg0 = self.extra_args[0]
710 712 f = os.path.abspath(arg0)
711 713 self.argv.remove(arg0)
712 714 if not os.path.exists(f):
713 715 self.log.critical("No such file or directory: %s", f)
714 716 self.exit(1)
715 717
716 718 # Use config here, to ensure that it takes higher priority than
717 719 # anything that comes from the profile.
718 720 c = Config()
719 721 if os.path.isdir(f):
720 722 c.NotebookApp.notebook_dir = f
721 723 elif os.path.isfile(f):
722 724 c.NotebookApp.file_to_run = f
723 725 self.update_config(c)
724 726
725 727 def init_kernel_argv(self):
726 728 """add the profile-dir to arguments to be passed to IPython kernels"""
727 729 # FIXME: remove special treatment of IPython kernels
728 730 # Kernel should get *absolute* path to profile directory
729 731 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
730 732
731 733 def init_configurables(self):
732 734 # force Session default to be secure
733 735 default_secure(self.config)
734 736 kls = import_item(self.kernel_spec_manager_class)
735 737 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
736 738
737 739 kls = import_item(self.kernel_manager_class)
738 740 self.kernel_manager = kls(
739 741 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
740 742 connection_dir = self.profile_dir.security_dir,
741 743 )
742 744 kls = import_item(self.contents_manager_class)
743 745 self.contents_manager = kls(parent=self, log=self.log)
744 746 kls = import_item(self.session_manager_class)
745 747 self.session_manager = kls(parent=self, log=self.log,
746 748 kernel_manager=self.kernel_manager,
747 749 contents_manager=self.contents_manager)
748 750 kls = import_item(self.cluster_manager_class)
749 751 self.cluster_manager = kls(parent=self, log=self.log)
750 752 self.cluster_manager.update_profiles()
753 self.login_handler_class = import_item(self.login_handler)
751 754
752 755 kls = import_item(self.config_manager_class)
753 756 self.config_manager = kls(parent=self, log=self.log,
754 757 profile_dir=self.profile_dir.location)
755 758
756 759 def init_logging(self):
757 760 # This prevents double log messages because tornado use a root logger that
758 761 # self.log is a child of. The logging module dipatches log messages to a log
759 762 # and all of its ancenstors until propagate is set to False.
760 763 self.log.propagate = False
761 764
762 765 for log in app_log, access_log, gen_log:
763 766 # consistent log output name (NotebookApp instead of tornado.access, etc.)
764 767 log.name = self.log.name
765 768 # hook up tornado 3's loggers to our app handlers
766 769 logger = logging.getLogger('tornado')
767 770 logger.propagate = True
768 771 logger.parent = self.log
769 772 logger.setLevel(self.log.level)
770 773
771 774 def init_webapp(self):
772 775 """initialize tornado webapp and httpserver"""
773 776 self.tornado_settings['allow_origin'] = self.allow_origin
774 777 if self.allow_origin_pat:
775 778 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
776 779 self.tornado_settings['allow_credentials'] = self.allow_credentials
777 780
778 781 self.web_app = NotebookWebApplication(
779 782 self, self.kernel_manager, self.contents_manager,
780 783 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
781 784 self.config_manager,
782 785 self.log, self.base_url, self.default_url, self.tornado_settings,
783 786 self.jinja_environment_options
784 787 )
785 788 if self.certfile:
786 789 ssl_options = dict(certfile=self.certfile)
787 790 if self.keyfile:
788 791 ssl_options['keyfile'] = self.keyfile
789 792 else:
790 793 ssl_options = None
791 self.web_app.password = self.password
794 self.login_handler_class.validate_notebook_app_security(self, ssl_options=ssl_options)
792 795 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
793 796 xheaders=self.trust_xheaders)
794 if not self.ip:
795 warning = "WARNING: The notebook server is listening on all IP addresses"
796 if ssl_options is None:
797 self.log.critical(warning + " and not using encryption. This "
798 "is not recommended.")
799 if not self.password:
800 self.log.critical(warning + " and not using authentication. "
801 "This is highly insecure and not recommended.")
797
802 798 success = None
803 799 for port in random_ports(self.port, self.port_retries+1):
804 800 try:
805 801 self.http_server.listen(port, self.ip)
806 802 except socket.error as e:
807 803 if e.errno == errno.EADDRINUSE:
808 804 self.log.info('The port %i is already in use, trying another random port.' % port)
809 805 continue
810 806 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
811 807 self.log.warn("Permission to listen on port %i denied" % port)
812 808 continue
813 809 else:
814 810 raise
815 811 else:
816 812 self.port = port
817 813 success = True
818 814 break
819 815 if not success:
820 816 self.log.critical('ERROR: the notebook server could not be started because '
821 817 'no available port could be found.')
822 818 self.exit(1)
823 819
824 820 @property
825 821 def display_url(self):
826 822 ip = self.ip if self.ip else '[all ip addresses on your system]'
827 823 return self._url(ip)
828 824
829 825 @property
830 826 def connection_url(self):
831 827 ip = self.ip if self.ip else 'localhost'
832 828 return self._url(ip)
833 829
834 830 def _url(self, ip):
835 831 proto = 'https' if self.certfile else 'http'
836 832 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
837 833
838 834 def init_terminals(self):
839 835 try:
840 836 from .terminal import initialize
841 837 initialize(self.web_app)
842 838 self.web_app.settings['terminals_available'] = True
843 839 except ImportError as e:
844 840 self.log.info("Terminals not available (error was %s)", e)
845 841
846 842 def init_signal(self):
847 843 if not sys.platform.startswith('win'):
848 844 signal.signal(signal.SIGINT, self._handle_sigint)
849 845 signal.signal(signal.SIGTERM, self._signal_stop)
850 846 if hasattr(signal, 'SIGUSR1'):
851 847 # Windows doesn't support SIGUSR1
852 848 signal.signal(signal.SIGUSR1, self._signal_info)
853 849 if hasattr(signal, 'SIGINFO'):
854 850 # only on BSD-based systems
855 851 signal.signal(signal.SIGINFO, self._signal_info)
856 852
857 853 def _handle_sigint(self, sig, frame):
858 854 """SIGINT handler spawns confirmation dialog"""
859 855 # register more forceful signal handler for ^C^C case
860 856 signal.signal(signal.SIGINT, self._signal_stop)
861 857 # request confirmation dialog in bg thread, to avoid
862 858 # blocking the App
863 859 thread = threading.Thread(target=self._confirm_exit)
864 860 thread.daemon = True
865 861 thread.start()
866 862
867 863 def _restore_sigint_handler(self):
868 864 """callback for restoring original SIGINT handler"""
869 865 signal.signal(signal.SIGINT, self._handle_sigint)
870 866
871 867 def _confirm_exit(self):
872 868 """confirm shutdown on ^C
873 869
874 870 A second ^C, or answering 'y' within 5s will cause shutdown,
875 871 otherwise original SIGINT handler will be restored.
876 872
877 873 This doesn't work on Windows.
878 874 """
879 875 info = self.log.info
880 876 info('interrupted')
881 877 print(self.notebook_info())
882 878 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
883 879 sys.stdout.flush()
884 880 r,w,x = select.select([sys.stdin], [], [], 5)
885 881 if r:
886 882 line = sys.stdin.readline()
887 883 if line.lower().startswith('y') and 'n' not in line.lower():
888 884 self.log.critical("Shutdown confirmed")
889 885 ioloop.IOLoop.instance().stop()
890 886 return
891 887 else:
892 888 print("No answer for 5s:", end=' ')
893 889 print("resuming operation...")
894 890 # no answer, or answer is no:
895 891 # set it back to original SIGINT handler
896 892 # use IOLoop.add_callback because signal.signal must be called
897 893 # from main thread
898 894 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
899 895
900 896 def _signal_stop(self, sig, frame):
901 897 self.log.critical("received signal %s, stopping", sig)
902 898 ioloop.IOLoop.instance().stop()
903 899
904 900 def _signal_info(self, sig, frame):
905 901 print(self.notebook_info())
906 902
907 903 def init_components(self):
908 904 """Check the components submodule, and warn if it's unclean"""
909 905 status = submodule.check_submodule_status()
910 906 if status == 'missing':
911 907 self.log.warn("components submodule missing, running `git submodule update`")
912 908 submodule.update_submodules(submodule.ipython_parent())
913 909 elif status == 'unclean':
914 910 self.log.warn("components submodule unclean, you may see 404s on static/components")
915 911 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
916 912
917 913 @catch_config_error
918 914 def initialize(self, argv=None):
919 915 super(NotebookApp, self).initialize(argv)
920 916 self.init_logging()
921 917 self.init_kernel_argv()
922 918 self.init_configurables()
923 919 self.init_components()
924 920 self.init_webapp()
925 921 self.init_terminals()
926 922 self.init_signal()
927 923
928 924 def cleanup_kernels(self):
929 925 """Shutdown all kernels.
930 926
931 927 The kernels will shutdown themselves when this process no longer exists,
932 928 but explicit shutdown allows the KernelManagers to cleanup the connection files.
933 929 """
934 930 self.log.info('Shutting down kernels')
935 931 self.kernel_manager.shutdown_all()
936 932
937 933 def notebook_info(self):
938 934 "Return the current working directory and the server url information"
939 935 info = self.contents_manager.info_string() + "\n"
940 936 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
941 937 return info + "The IPython Notebook is running at: %s" % self.display_url
942 938
943 939 def server_info(self):
944 940 """Return a JSONable dict of information about this server."""
945 941 return {'url': self.connection_url,
946 942 'hostname': self.ip if self.ip else 'localhost',
947 943 'port': self.port,
948 944 'secure': bool(self.certfile),
949 945 'base_url': self.base_url,
950 946 'notebook_dir': os.path.abspath(self.notebook_dir),
951 947 'pid': os.getpid()
952 948 }
953 949
954 950 def write_server_info_file(self):
955 951 """Write the result of server_info() to the JSON file info_file."""
956 952 with open(self.info_file, 'w') as f:
957 953 json.dump(self.server_info(), f, indent=2)
958 954
959 955 def remove_server_info_file(self):
960 956 """Remove the nbserver-<pid>.json file created for this server.
961 957
962 958 Ignores the error raised when the file has already been removed.
963 959 """
964 960 try:
965 961 os.unlink(self.info_file)
966 962 except OSError as e:
967 963 if e.errno != errno.ENOENT:
968 964 raise
969 965
970 966 def start(self):
971 967 """ Start the IPython Notebook server app, after initialization
972 968
973 969 This method takes no arguments so all configuration and initialization
974 970 must be done prior to calling this method."""
975 971 if self.subapp is not None:
976 972 return self.subapp.start()
977 973
978 974 info = self.log.info
979 975 for line in self.notebook_info().split("\n"):
980 976 info(line)
981 977 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
982 978
983 979 self.write_server_info_file()
984 980
985 981 if self.open_browser or self.file_to_run:
986 982 try:
987 983 browser = webbrowser.get(self.browser or None)
988 984 except webbrowser.Error as e:
989 985 self.log.warn('No web browser found: %s.' % e)
990 986 browser = None
991 987
992 988 if self.file_to_run:
993 989 if not os.path.exists(self.file_to_run):
994 990 self.log.critical("%s does not exist" % self.file_to_run)
995 991 self.exit(1)
996 992
997 993 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
998 994 uri = url_path_join('notebooks', *relpath.split(os.sep))
999 995 else:
1000 996 uri = 'tree'
1001 997 if browser:
1002 998 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1003 999 new=2)
1004 1000 threading.Thread(target=b).start()
1005 1001 try:
1006 1002 ioloop.IOLoop.instance().start()
1007 1003 except KeyboardInterrupt:
1008 1004 info("Interrupted...")
1009 1005 finally:
1010 1006 self.cleanup_kernels()
1011 1007 self.remove_server_info_file()
1012 1008
1013 1009
1014 1010 def list_running_servers(profile='default'):
1015 1011 """Iterate over the server info files of running notebook servers.
1016 1012
1017 1013 Given a profile name, find nbserver-* files in the security directory of
1018 1014 that profile, and yield dicts of their information, each one pertaining to
1019 1015 a currently running notebook server instance.
1020 1016 """
1021 1017 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1022 1018 for file in os.listdir(pd.security_dir):
1023 1019 if file.startswith('nbserver-'):
1024 1020 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1025 1021 info = json.load(f)
1026 1022
1027 1023 # Simple check whether that process is really still running
1028 1024 # Also remove leftover files from IPython 2.x without a pid field
1029 1025 if ('pid' in info) and check_pid(info['pid']):
1030 1026 yield info
1031 1027 else:
1032 1028 # If the process has died, try to delete its info file
1033 1029 try:
1034 1030 os.unlink(file)
1035 1031 except OSError:
1036 1032 pass # TODO: This should warn or log or something
1037 1033 #-----------------------------------------------------------------------------
1038 1034 # Main entry point
1039 1035 #-----------------------------------------------------------------------------
1040 1036
1041 1037 launch_new_instance = NotebookApp.launch_instance
1042 1038
General Comments 0
You need to be logged in to leave comments. Login now