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