##// END OF EJS Templates
Fix for CVE-2014-3566 'POODLE' SSL attack, original commit '22c4922f4796078c5613de9e07e66b8764549cad'
Brian Drawert -
Show More
@@ -1,1127 +1,1131 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 importlib
13 13 import io
14 14 import json
15 15 import logging
16 16 import os
17 17 import random
18 18 import re
19 19 import select
20 20 import signal
21 21 import socket
22 import ssl
22 23 import sys
23 24 import threading
24 25 import webbrowser
25 26
26 27
27 28 # check for pyzmq
28 29 from IPython.utils.zmqrelated import check_for_zmq
29 30 check_for_zmq('13', 'IPython.html')
30 31
31 32 from jinja2 import Environment, FileSystemLoader
32 33
33 34 # Install the pyzmq ioloop. This has to be done before anything else from
34 35 # tornado is imported.
35 36 from zmq.eventloop import ioloop
36 37 ioloop.install()
37 38
38 39 # check for tornado 3.1.0
39 40 msg = "The IPython Notebook requires tornado >= 4.0"
40 41 try:
41 42 import tornado
42 43 except ImportError:
43 44 raise ImportError(msg)
44 45 try:
45 46 version_info = tornado.version_info
46 47 except AttributeError:
47 48 raise ImportError(msg + ", but you have < 1.1.0")
48 49 if version_info < (4,0):
49 50 raise ImportError(msg + ", but you have %s" % tornado.version)
50 51
51 52 from tornado import httpserver
52 53 from tornado import web
53 54 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 55
55 56 from IPython.html import (
56 57 DEFAULT_STATIC_FILES_PATH,
57 58 DEFAULT_TEMPLATE_PATH_LIST,
58 59 )
59 60 from .base.handlers import Template404
60 61 from .log import log_request
61 62 from .services.kernels.kernelmanager import MappingKernelManager
62 63 from .services.config import ConfigManager
63 64 from .services.contents.manager import ContentsManager
64 65 from .services.contents.filemanager import FileContentsManager
65 66 from .services.clusters.clustermanager import ClusterManager
66 67 from .services.sessions.sessionmanager import SessionManager
67 68
68 69 from .auth.login import LoginHandler
69 70 from .auth.logout import LogoutHandler
70 71 from .base.handlers import IPythonHandler, FileFindHandler
71 72
72 73 from IPython.config import Config
73 74 from IPython.config.application import catch_config_error, boolean_flag
74 75 from IPython.core.application import (
75 76 BaseIPythonApplication, base_flags, base_aliases,
76 77 )
77 78 from IPython.core.profiledir import ProfileDir
78 79 from IPython.kernel import KernelManager
79 80 from IPython.kernel.kernelspec import KernelSpecManager
80 81 from IPython.kernel.zmq.session import Session
81 82 from IPython.nbformat.sign import NotebookNotary
82 83 from IPython.utils.importstring import import_item
83 84 from IPython.utils import submodule
84 85 from IPython.utils.process import check_pid
85 86 from IPython.utils.traitlets import (
86 87 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
87 88 TraitError, Type,
88 89 )
89 90 from IPython.utils import py3compat
90 91 from IPython.utils.path import filefind, get_ipython_dir
91 92 from IPython.utils.sysinfo import get_sys_info
92 93
93 94 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
94 95 from .utils import url_path_join
95 96
96 97 #-----------------------------------------------------------------------------
97 98 # Module globals
98 99 #-----------------------------------------------------------------------------
99 100
100 101 _examples = """
101 102 ipython notebook # start the notebook
102 103 ipython notebook --profile=sympy # use the sympy profile
103 104 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
104 105 """
105 106
106 107 #-----------------------------------------------------------------------------
107 108 # Helper functions
108 109 #-----------------------------------------------------------------------------
109 110
110 111 def random_ports(port, n):
111 112 """Generate a list of n random ports near the given port.
112 113
113 114 The first 5 ports will be sequential, and the remaining n-5 will be
114 115 randomly selected in the range [port-2*n, port+2*n].
115 116 """
116 117 for i in range(min(5, n)):
117 118 yield port + i
118 119 for i in range(n-5):
119 120 yield max(1, port + random.randint(-2*n, 2*n))
120 121
121 122 def load_handlers(name):
122 123 """Load the (URL pattern, handler) tuples for each component."""
123 124 name = 'IPython.html.' + name
124 125 mod = __import__(name, fromlist=['default_handlers'])
125 126 return mod.default_handlers
126 127
127 128 #-----------------------------------------------------------------------------
128 129 # The Tornado web application
129 130 #-----------------------------------------------------------------------------
130 131
131 132 class NotebookWebApplication(web.Application):
132 133
133 134 def __init__(self, ipython_app, kernel_manager, contents_manager,
134 135 cluster_manager, session_manager, kernel_spec_manager,
135 136 config_manager, log,
136 137 base_url, default_url, settings_overrides, jinja_env_options):
137 138
138 139 settings = self.init_settings(
139 140 ipython_app, kernel_manager, contents_manager, cluster_manager,
140 141 session_manager, kernel_spec_manager, config_manager, log, base_url,
141 142 default_url, settings_overrides, jinja_env_options)
142 143 handlers = self.init_handlers(settings)
143 144
144 145 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 146
146 147 def init_settings(self, ipython_app, kernel_manager, contents_manager,
147 148 cluster_manager, session_manager, kernel_spec_manager,
148 149 config_manager,
149 150 log, base_url, default_url, settings_overrides,
150 151 jinja_env_options=None):
151 152
152 153 _template_path = settings_overrides.get(
153 154 "template_path",
154 155 ipython_app.template_file_path,
155 156 )
156 157 if isinstance(_template_path, str):
157 158 _template_path = (_template_path,)
158 159 template_path = [os.path.expanduser(path) for path in _template_path]
159 160
160 161 jenv_opt = jinja_env_options if jinja_env_options else {}
161 162 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
162 163
163 164 sys_info = get_sys_info()
164 165 if sys_info['commit_source'] == 'repository':
165 166 # don't cache (rely on 304) when working from master
166 167 version_hash = ''
167 168 else:
168 169 # reset the cache on server restart
169 170 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
170 171
171 172 settings = dict(
172 173 # basics
173 174 log_function=log_request,
174 175 base_url=base_url,
175 176 default_url=default_url,
176 177 template_path=template_path,
177 178 static_path=ipython_app.static_file_path,
178 179 static_handler_class = FileFindHandler,
179 180 static_url_prefix = url_path_join(base_url,'/static/'),
180 181 static_handler_args = {
181 182 # don't cache custom.js
182 183 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
183 184 },
184 185 version_hash=version_hash,
185 186
186 187 # authentication
187 188 cookie_secret=ipython_app.cookie_secret,
188 189 login_url=url_path_join(base_url,'/login'),
189 190 login_handler_class=ipython_app.login_handler_class,
190 191 logout_handler_class=ipython_app.logout_handler_class,
191 192 password=ipython_app.password,
192 193
193 194 # managers
194 195 kernel_manager=kernel_manager,
195 196 contents_manager=contents_manager,
196 197 cluster_manager=cluster_manager,
197 198 session_manager=session_manager,
198 199 kernel_spec_manager=kernel_spec_manager,
199 200 config_manager=config_manager,
200 201
201 202 # IPython stuff
202 203 nbextensions_path=ipython_app.nbextensions_path,
203 204 websocket_url=ipython_app.websocket_url,
204 205 mathjax_url=ipython_app.mathjax_url,
205 206 config=ipython_app.config,
206 207 jinja2_env=env,
207 208 terminals_available=False, # Set later if terminals are available
208 209 )
209 210
210 211 # allow custom overrides for the tornado web app.
211 212 settings.update(settings_overrides)
212 213 return settings
213 214
214 215 def init_handlers(self, settings):
215 216 """Load the (URL pattern, handler) tuples for each component."""
216 217
217 218 # Order matters. The first handler to match the URL will handle the request.
218 219 handlers = []
219 220 handlers.extend(load_handlers('tree.handlers'))
220 221 handlers.extend([(r"/login", settings['login_handler_class'])])
221 222 handlers.extend([(r"/logout", settings['logout_handler_class'])])
222 223 handlers.extend(load_handlers('files.handlers'))
223 224 handlers.extend(load_handlers('notebook.handlers'))
224 225 handlers.extend(load_handlers('nbconvert.handlers'))
225 226 handlers.extend(load_handlers('kernelspecs.handlers'))
226 227 handlers.extend(load_handlers('edit.handlers'))
227 228 handlers.extend(load_handlers('services.config.handlers'))
228 229 handlers.extend(load_handlers('services.kernels.handlers'))
229 230 handlers.extend(load_handlers('services.contents.handlers'))
230 231 handlers.extend(load_handlers('services.clusters.handlers'))
231 232 handlers.extend(load_handlers('services.sessions.handlers'))
232 233 handlers.extend(load_handlers('services.nbconvert.handlers'))
233 234 handlers.extend(load_handlers('services.kernelspecs.handlers'))
234 235 handlers.extend(load_handlers('services.security.handlers'))
235 236 handlers.append(
236 237 (r"/nbextensions/(.*)", FileFindHandler, {
237 238 'path': settings['nbextensions_path'],
238 239 'no_cache_paths': ['/'], # don't cache anything in nbextensions
239 240 }),
240 241 )
241 242 # register base handlers last
242 243 handlers.extend(load_handlers('base.handlers'))
243 244 # set the URL that will be redirected from `/`
244 245 handlers.append(
245 246 (r'/?', web.RedirectHandler, {
246 247 'url' : settings['default_url'],
247 248 'permanent': False, # want 302, not 301
248 249 })
249 250 )
250 251 # prepend base_url onto the patterns that we match
251 252 new_handlers = []
252 253 for handler in handlers:
253 254 pattern = url_path_join(settings['base_url'], handler[0])
254 255 new_handler = tuple([pattern] + list(handler[1:]))
255 256 new_handlers.append(new_handler)
256 257 # add 404 on the end, which will catch everything that falls through
257 258 new_handlers.append((r'(.*)', Template404))
258 259 return new_handlers
259 260
260 261
261 262 class NbserverListApp(BaseIPythonApplication):
262 263
263 264 description="List currently running notebook servers in this profile."
264 265
265 266 flags = dict(
266 267 json=({'NbserverListApp': {'json': True}},
267 268 "Produce machine-readable JSON output."),
268 269 )
269 270
270 271 json = Bool(False, config=True,
271 272 help="If True, each line of output will be a JSON object with the "
272 273 "details from the server info file.")
273 274
274 275 def start(self):
275 276 if not self.json:
276 277 print("Currently running servers:")
277 278 for serverinfo in list_running_servers(self.profile):
278 279 if self.json:
279 280 print(json.dumps(serverinfo))
280 281 else:
281 282 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
282 283
283 284 #-----------------------------------------------------------------------------
284 285 # Aliases and Flags
285 286 #-----------------------------------------------------------------------------
286 287
287 288 flags = dict(base_flags)
288 289 flags['no-browser']=(
289 290 {'NotebookApp' : {'open_browser' : False}},
290 291 "Don't open the notebook in a browser after startup."
291 292 )
292 293 flags['pylab']=(
293 294 {'NotebookApp' : {'pylab' : 'warn'}},
294 295 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
295 296 )
296 297 flags['no-mathjax']=(
297 298 {'NotebookApp' : {'enable_mathjax' : False}},
298 299 """Disable MathJax
299 300
300 301 MathJax is the javascript library IPython uses to render math/LaTeX. It is
301 302 very large, so you may want to disable it if you have a slow internet
302 303 connection, or for offline use of the notebook.
303 304
304 305 When disabled, equations etc. will appear as their untransformed TeX source.
305 306 """
306 307 )
307 308
308 309 # Add notebook manager flags
309 310 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
310 311 'DEPRECATED, IGNORED',
311 312 'DEPRECATED, IGNORED'))
312 313
313 314 aliases = dict(base_aliases)
314 315
315 316 aliases.update({
316 317 'ip': 'NotebookApp.ip',
317 318 'port': 'NotebookApp.port',
318 319 'port-retries': 'NotebookApp.port_retries',
319 320 'transport': 'KernelManager.transport',
320 321 'keyfile': 'NotebookApp.keyfile',
321 322 'certfile': 'NotebookApp.certfile',
322 323 'notebook-dir': 'NotebookApp.notebook_dir',
323 324 'browser': 'NotebookApp.browser',
324 325 'pylab': 'NotebookApp.pylab',
325 326 })
326 327
327 328 #-----------------------------------------------------------------------------
328 329 # NotebookApp
329 330 #-----------------------------------------------------------------------------
330 331
331 332 class NotebookApp(BaseIPythonApplication):
332 333
333 334 name = 'ipython-notebook'
334 335
335 336 description = """
336 337 The IPython HTML Notebook.
337 338
338 339 This launches a Tornado based HTML Notebook Server that serves up an
339 340 HTML5/Javascript Notebook client.
340 341 """
341 342 examples = _examples
342 343 aliases = aliases
343 344 flags = flags
344 345
345 346 classes = [
346 347 KernelManager, ProfileDir, Session, MappingKernelManager,
347 348 ContentsManager, FileContentsManager, NotebookNotary,
348 349 KernelSpecManager,
349 350 ]
350 351 flags = Dict(flags)
351 352 aliases = Dict(aliases)
352 353
353 354 subcommands = dict(
354 355 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
355 356 )
356 357
357 358 _log_formatter_cls = LogFormatter
358 359
359 360 def _log_level_default(self):
360 361 return logging.INFO
361 362
362 363 def _log_datefmt_default(self):
363 364 """Exclude date from default date format"""
364 365 return "%H:%M:%S"
365 366
366 367 def _log_format_default(self):
367 368 """override default log format to include time"""
368 369 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
369 370
370 371 # create requested profiles by default, if they don't exist:
371 372 auto_create = Bool(True)
372 373
373 374 # file to be opened in the notebook server
374 375 file_to_run = Unicode('', config=True)
375 376
376 377 # Network related information
377 378
378 379 allow_origin = Unicode('', config=True,
379 380 help="""Set the Access-Control-Allow-Origin header
380 381
381 382 Use '*' to allow any origin to access your server.
382 383
383 384 Takes precedence over allow_origin_pat.
384 385 """
385 386 )
386 387
387 388 allow_origin_pat = Unicode('', config=True,
388 389 help="""Use a regular expression for the Access-Control-Allow-Origin header
389 390
390 391 Requests from an origin matching the expression will get replies with:
391 392
392 393 Access-Control-Allow-Origin: origin
393 394
394 395 where `origin` is the origin of the request.
395 396
396 397 Ignored if allow_origin is set.
397 398 """
398 399 )
399 400
400 401 allow_credentials = Bool(False, config=True,
401 402 help="Set the Access-Control-Allow-Credentials: true header"
402 403 )
403 404
404 405 default_url = Unicode('/tree', config=True,
405 406 help="The default URL to redirect to from `/`"
406 407 )
407 408
408 409 ip = Unicode('localhost', config=True,
409 410 help="The IP address the notebook server will listen on."
410 411 )
411 412 def _ip_default(self):
412 413 """Return localhost if available, 127.0.0.1 otherwise.
413 414
414 415 On some (horribly broken) systems, localhost cannot be bound.
415 416 """
416 417 s = socket.socket()
417 418 try:
418 419 s.bind(('localhost', 0))
419 420 except socket.error as e:
420 421 self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
421 422 return '127.0.0.1'
422 423 else:
423 424 s.close()
424 425 return 'localhost'
425 426
426 427 def _ip_changed(self, name, old, new):
427 428 if new == u'*': self.ip = u''
428 429
429 430 port = Integer(8888, config=True,
430 431 help="The port the notebook server will listen on."
431 432 )
432 433 port_retries = Integer(50, config=True,
433 434 help="The number of additional ports to try if the specified port is not available."
434 435 )
435 436
436 437 certfile = Unicode(u'', config=True,
437 438 help="""The full path to an SSL/TLS certificate file."""
438 439 )
439 440
440 441 keyfile = Unicode(u'', config=True,
441 442 help="""The full path to a private key file for usage with SSL/TLS."""
442 443 )
443 444
444 445 cookie_secret_file = Unicode(config=True,
445 446 help="""The file where the cookie secret is stored."""
446 447 )
447 448 def _cookie_secret_file_default(self):
448 449 if self.profile_dir is None:
449 450 return ''
450 451 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
451 452
452 453 cookie_secret = Bytes(b'', config=True,
453 454 help="""The random bytes used to secure cookies.
454 455 By default this is a new random number every time you start the Notebook.
455 456 Set it to a value in a config file to enable logins to persist across server sessions.
456 457
457 458 Note: Cookie secrets should be kept private, do not share config files with
458 459 cookie_secret stored in plaintext (you can read the value from a file).
459 460 """
460 461 )
461 462 def _cookie_secret_default(self):
462 463 if os.path.exists(self.cookie_secret_file):
463 464 with io.open(self.cookie_secret_file, 'rb') as f:
464 465 return f.read()
465 466 else:
466 467 secret = base64.encodestring(os.urandom(1024))
467 468 self._write_cookie_secret_file(secret)
468 469 return secret
469 470
470 471 def _write_cookie_secret_file(self, secret):
471 472 """write my secret to my secret_file"""
472 473 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
473 474 with io.open(self.cookie_secret_file, 'wb') as f:
474 475 f.write(secret)
475 476 try:
476 477 os.chmod(self.cookie_secret_file, 0o600)
477 478 except OSError:
478 479 self.log.warn(
479 480 "Could not set permissions on %s",
480 481 self.cookie_secret_file
481 482 )
482 483
483 484 password = Unicode(u'', config=True,
484 485 help="""Hashed password to use for web authentication.
485 486
486 487 To generate, type in a python/IPython shell:
487 488
488 489 from IPython.lib import passwd; passwd()
489 490
490 491 The string should be of the form type:salt:hashed-password.
491 492 """
492 493 )
493 494
494 495 open_browser = Bool(True, config=True,
495 496 help="""Whether to open in a browser after starting.
496 497 The specific browser used is platform dependent and
497 498 determined by the python standard library `webbrowser`
498 499 module, unless it is overridden using the --browser
499 500 (NotebookApp.browser) configuration option.
500 501 """)
501 502
502 503 browser = Unicode(u'', config=True,
503 504 help="""Specify what command to use to invoke a web
504 505 browser when opening the notebook. If not specified, the
505 506 default browser will be determined by the `webbrowser`
506 507 standard library module, which allows setting of the
507 508 BROWSER environment variable to override it.
508 509 """)
509 510
510 511 webapp_settings = Dict(config=True,
511 512 help="DEPRECATED, use tornado_settings"
512 513 )
513 514 def _webapp_settings_changed(self, name, old, new):
514 515 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
515 516 self.tornado_settings = new
516 517
517 518 tornado_settings = Dict(config=True,
518 519 help="Supply overrides for the tornado.web.Application that the "
519 520 "IPython notebook uses.")
520 521
521 522 ssl_options = Dict(config=True,
522 523 help="""Supply SSL options for the tornado HTTPServer.
523 524 See the tornado docs for details.""")
524 525
525 526 jinja_environment_options = Dict(config=True,
526 527 help="Supply extra arguments that will be passed to Jinja environment.")
527 528
528 529 enable_mathjax = Bool(True, config=True,
529 530 help="""Whether to enable MathJax for typesetting math/TeX
530 531
531 532 MathJax is the javascript library IPython uses to render math/LaTeX. It is
532 533 very large, so you may want to disable it if you have a slow internet
533 534 connection, or for offline use of the notebook.
534 535
535 536 When disabled, equations etc. will appear as their untransformed TeX source.
536 537 """
537 538 )
538 539 def _enable_mathjax_changed(self, name, old, new):
539 540 """set mathjax url to empty if mathjax is disabled"""
540 541 if not new:
541 542 self.mathjax_url = u''
542 543
543 544 base_url = Unicode('/', config=True,
544 545 help='''The base URL for the notebook server.
545 546
546 547 Leading and trailing slashes can be omitted,
547 548 and will automatically be added.
548 549 ''')
549 550 def _base_url_changed(self, name, old, new):
550 551 if not new.startswith('/'):
551 552 self.base_url = '/'+new
552 553 elif not new.endswith('/'):
553 554 self.base_url = new+'/'
554 555
555 556 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
556 557 def _base_project_url_changed(self, name, old, new):
557 558 self.log.warn("base_project_url is deprecated, use base_url")
558 559 self.base_url = new
559 560
560 561 extra_static_paths = List(Unicode, config=True,
561 562 help="""Extra paths to search for serving static files.
562 563
563 564 This allows adding javascript/css to be available from the notebook server machine,
564 565 or overriding individual files in the IPython"""
565 566 )
566 567 def _extra_static_paths_default(self):
567 568 return [os.path.join(self.profile_dir.location, 'static')]
568 569
569 570 @property
570 571 def static_file_path(self):
571 572 """return extra paths + the default location"""
572 573 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
573 574
574 575 extra_template_paths = List(Unicode, config=True,
575 576 help="""Extra paths to search for serving jinja templates.
576 577
577 578 Can be used to override templates from IPython.html.templates."""
578 579 )
579 580 def _extra_template_paths_default(self):
580 581 return []
581 582
582 583 @property
583 584 def template_file_path(self):
584 585 """return extra paths + the default locations"""
585 586 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
586 587
587 588 extra_nbextensions_path = List(Unicode, config=True,
588 589 help="""extra paths to look for Javascript notebook extensions"""
589 590 )
590 591
591 592 @property
592 593 def nbextensions_path(self):
593 594 """The path to look for Javascript notebook extensions"""
594 595 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
595 596
596 597 websocket_url = Unicode("", config=True,
597 598 help="""The base URL for websockets,
598 599 if it differs from the HTTP server (hint: it almost certainly doesn't).
599 600
600 601 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
601 602 """
602 603 )
603 604 mathjax_url = Unicode("", config=True,
604 605 help="""The url for MathJax.js."""
605 606 )
606 607 def _mathjax_url_default(self):
607 608 if not self.enable_mathjax:
608 609 return u''
609 610 static_url_prefix = self.tornado_settings.get("static_url_prefix",
610 611 url_path_join(self.base_url, "static")
611 612 )
612 613
613 614 # try local mathjax, either in nbextensions/mathjax or static/mathjax
614 615 for (url_prefix, search_path) in [
615 616 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
616 617 (static_url_prefix, self.static_file_path),
617 618 ]:
618 619 self.log.debug("searching for local mathjax in %s", search_path)
619 620 try:
620 621 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
621 622 except IOError:
622 623 continue
623 624 else:
624 625 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
625 626 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
626 627 return url
627 628
628 629 # no local mathjax, serve from CDN
629 630 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
630 631 self.log.info("Using MathJax from CDN: %s", url)
631 632 return url
632 633
633 634 def _mathjax_url_changed(self, name, old, new):
634 635 if new and not self.enable_mathjax:
635 636 # enable_mathjax=False overrides mathjax_url
636 637 self.mathjax_url = u''
637 638 else:
638 639 self.log.info("Using MathJax: %s", new)
639 640
640 641 contents_manager_class = Type(
641 642 default_value=FileContentsManager,
642 643 klass=ContentsManager,
643 644 config=True,
644 645 help='The notebook manager class to use.'
645 646 )
646 647 kernel_manager_class = Type(
647 648 default_value=MappingKernelManager,
648 649 config=True,
649 650 help='The kernel manager class to use.'
650 651 )
651 652 session_manager_class = Type(
652 653 default_value=SessionManager,
653 654 config=True,
654 655 help='The session manager class to use.'
655 656 )
656 657 cluster_manager_class = Type(
657 658 default_value=ClusterManager,
658 659 config=True,
659 660 help='The cluster manager class to use.'
660 661 )
661 662
662 663 config_manager_class = Type(
663 664 default_value=ConfigManager,
664 665 config = True,
665 666 help='The config manager class to use'
666 667 )
667 668
668 669 kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
669 670
670 671 kernel_spec_manager_class = Type(
671 672 default_value=KernelSpecManager,
672 673 config=True,
673 674 help="""
674 675 The kernel spec manager class to use. Should be a subclass
675 676 of `IPython.kernel.kernelspec.KernelSpecManager`.
676 677
677 678 The Api of KernelSpecManager is provisional and might change
678 679 without warning between this version of IPython and the next stable one.
679 680 """
680 681 )
681 682
682 683 login_handler_class = Type(
683 684 default_value=LoginHandler,
684 685 klass=web.RequestHandler,
685 686 config=True,
686 687 help='The login handler class to use.',
687 688 )
688 689
689 690 logout_handler_class = Type(
690 691 default_value=LogoutHandler,
691 692 klass=web.RequestHandler,
692 693 config=True,
693 694 help='The logout handler class to use.',
694 695 )
695 696
696 697 trust_xheaders = Bool(False, config=True,
697 698 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
698 699 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
699 700 )
700 701
701 702 info_file = Unicode()
702 703
703 704 def _info_file_default(self):
704 705 info_file = "nbserver-%s.json"%os.getpid()
705 706 return os.path.join(self.profile_dir.security_dir, info_file)
706 707
707 708 pylab = Unicode('disabled', config=True,
708 709 help="""
709 710 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
710 711 """
711 712 )
712 713 def _pylab_changed(self, name, old, new):
713 714 """when --pylab is specified, display a warning and exit"""
714 715 if new != 'warn':
715 716 backend = ' %s' % new
716 717 else:
717 718 backend = ''
718 719 self.log.error("Support for specifying --pylab on the command line has been removed.")
719 720 self.log.error(
720 721 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
721 722 )
722 723 self.exit(1)
723 724
724 725 notebook_dir = Unicode(config=True,
725 726 help="The directory to use for notebooks and kernels."
726 727 )
727 728
728 729 def _notebook_dir_default(self):
729 730 if self.file_to_run:
730 731 return os.path.dirname(os.path.abspath(self.file_to_run))
731 732 else:
732 733 return py3compat.getcwd()
733 734
734 735 def _notebook_dir_changed(self, name, old, new):
735 736 """Do a bit of validation of the notebook dir."""
736 737 if not os.path.isabs(new):
737 738 # If we receive a non-absolute path, make it absolute.
738 739 self.notebook_dir = os.path.abspath(new)
739 740 return
740 741 if not os.path.isdir(new):
741 742 raise TraitError("No such notebook dir: %r" % new)
742 743
743 744 # setting App.notebook_dir implies setting notebook and kernel dirs as well
744 745 self.config.FileContentsManager.root_dir = new
745 746 self.config.MappingKernelManager.root_dir = new
746 747
747 748 server_extensions = List(Unicode(), config=True,
748 749 help=("Python modules to load as notebook server extensions. "
749 750 "This is an experimental API, and may change in future releases.")
750 751 )
751 752
752 753 reraise_server_extension_failures = Bool(
753 754 False,
754 755 config=True,
755 756 help="Reraise exceptions encountered loading server extensions?",
756 757 )
757 758
758 759 def parse_command_line(self, argv=None):
759 760 super(NotebookApp, self).parse_command_line(argv)
760 761
761 762 if self.extra_args:
762 763 arg0 = self.extra_args[0]
763 764 f = os.path.abspath(arg0)
764 765 self.argv.remove(arg0)
765 766 if not os.path.exists(f):
766 767 self.log.critical("No such file or directory: %s", f)
767 768 self.exit(1)
768 769
769 770 # Use config here, to ensure that it takes higher priority than
770 771 # anything that comes from the profile.
771 772 c = Config()
772 773 if os.path.isdir(f):
773 774 c.NotebookApp.notebook_dir = f
774 775 elif os.path.isfile(f):
775 776 c.NotebookApp.file_to_run = f
776 777 self.update_config(c)
777 778
778 779 def init_configurables(self):
779 780 self.kernel_spec_manager = self.kernel_spec_manager_class(
780 781 parent=self,
781 782 ipython_dir=self.ipython_dir,
782 783 )
783 784 self.kernel_manager = self.kernel_manager_class(
784 785 parent=self,
785 786 log=self.log,
786 787 connection_dir=self.profile_dir.security_dir,
787 788 )
788 789 self.contents_manager = self.contents_manager_class(
789 790 parent=self,
790 791 log=self.log,
791 792 )
792 793 self.session_manager = self.session_manager_class(
793 794 parent=self,
794 795 log=self.log,
795 796 kernel_manager=self.kernel_manager,
796 797 contents_manager=self.contents_manager,
797 798 )
798 799 self.cluster_manager = self.cluster_manager_class(
799 800 parent=self,
800 801 log=self.log,
801 802 )
802 803
803 804 self.config_manager = self.config_manager_class(
804 805 parent=self,
805 806 log=self.log,
806 807 profile_dir=self.profile_dir.location,
807 808 )
808 809
809 810 def init_logging(self):
810 811 # This prevents double log messages because tornado use a root logger that
811 812 # self.log is a child of. The logging module dipatches log messages to a log
812 813 # and all of its ancenstors until propagate is set to False.
813 814 self.log.propagate = False
814 815
815 816 for log in app_log, access_log, gen_log:
816 817 # consistent log output name (NotebookApp instead of tornado.access, etc.)
817 818 log.name = self.log.name
818 819 # hook up tornado 3's loggers to our app handlers
819 820 logger = logging.getLogger('tornado')
820 821 logger.propagate = True
821 822 logger.parent = self.log
822 823 logger.setLevel(self.log.level)
823 824
824 825 def init_webapp(self):
825 826 """initialize tornado webapp and httpserver"""
826 827 self.tornado_settings['allow_origin'] = self.allow_origin
827 828 if self.allow_origin_pat:
828 829 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
829 830 self.tornado_settings['allow_credentials'] = self.allow_credentials
830 831 # ensure default_url starts with base_url
831 832 if not self.default_url.startswith(self.base_url):
832 833 self.default_url = url_path_join(self.base_url, self.default_url)
833 834
834 835 self.web_app = NotebookWebApplication(
835 836 self, self.kernel_manager, self.contents_manager,
836 837 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
837 838 self.config_manager,
838 839 self.log, self.base_url, self.default_url, self.tornado_settings,
839 840 self.jinja_environment_options
840 841 )
841 842 ssl_options = self.ssl_options
842 843 if self.certfile:
843 844 ssl_options['certfile'] = self.certfile
844 845 if self.keyfile:
845 846 ssl_options['keyfile'] = self.keyfile
846 847 if not ssl_options:
847 848 # None indicates no SSL config
848 849 ssl_options = None
850 else:
851 # Disable SSLv3, since its use is discouraged.
852 ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
849 853 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
850 854 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
851 855 xheaders=self.trust_xheaders)
852 856
853 857 success = None
854 858 for port in random_ports(self.port, self.port_retries+1):
855 859 try:
856 860 self.http_server.listen(port, self.ip)
857 861 except socket.error as e:
858 862 if e.errno == errno.EADDRINUSE:
859 863 self.log.info('The port %i is already in use, trying another random port.' % port)
860 864 continue
861 865 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
862 866 self.log.warn("Permission to listen on port %i denied" % port)
863 867 continue
864 868 else:
865 869 raise
866 870 else:
867 871 self.port = port
868 872 success = True
869 873 break
870 874 if not success:
871 875 self.log.critical('ERROR: the notebook server could not be started because '
872 876 'no available port could be found.')
873 877 self.exit(1)
874 878
875 879 @property
876 880 def display_url(self):
877 881 ip = self.ip if self.ip else '[all ip addresses on your system]'
878 882 return self._url(ip)
879 883
880 884 @property
881 885 def connection_url(self):
882 886 ip = self.ip if self.ip else 'localhost'
883 887 return self._url(ip)
884 888
885 889 def _url(self, ip):
886 890 proto = 'https' if self.certfile else 'http'
887 891 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
888 892
889 893 def init_terminals(self):
890 894 try:
891 895 from .terminal import initialize
892 896 initialize(self.web_app)
893 897 self.web_app.settings['terminals_available'] = True
894 898 except ImportError as e:
895 899 log = self.log.debug if sys.platform == 'win32' else self.log.warn
896 900 log("Terminals not available (error was %s)", e)
897 901
898 902 def init_signal(self):
899 903 if not sys.platform.startswith('win'):
900 904 signal.signal(signal.SIGINT, self._handle_sigint)
901 905 signal.signal(signal.SIGTERM, self._signal_stop)
902 906 if hasattr(signal, 'SIGUSR1'):
903 907 # Windows doesn't support SIGUSR1
904 908 signal.signal(signal.SIGUSR1, self._signal_info)
905 909 if hasattr(signal, 'SIGINFO'):
906 910 # only on BSD-based systems
907 911 signal.signal(signal.SIGINFO, self._signal_info)
908 912
909 913 def _handle_sigint(self, sig, frame):
910 914 """SIGINT handler spawns confirmation dialog"""
911 915 # register more forceful signal handler for ^C^C case
912 916 signal.signal(signal.SIGINT, self._signal_stop)
913 917 # request confirmation dialog in bg thread, to avoid
914 918 # blocking the App
915 919 thread = threading.Thread(target=self._confirm_exit)
916 920 thread.daemon = True
917 921 thread.start()
918 922
919 923 def _restore_sigint_handler(self):
920 924 """callback for restoring original SIGINT handler"""
921 925 signal.signal(signal.SIGINT, self._handle_sigint)
922 926
923 927 def _confirm_exit(self):
924 928 """confirm shutdown on ^C
925 929
926 930 A second ^C, or answering 'y' within 5s will cause shutdown,
927 931 otherwise original SIGINT handler will be restored.
928 932
929 933 This doesn't work on Windows.
930 934 """
931 935 info = self.log.info
932 936 info('interrupted')
933 937 print(self.notebook_info())
934 938 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
935 939 sys.stdout.flush()
936 940 r,w,x = select.select([sys.stdin], [], [], 5)
937 941 if r:
938 942 line = sys.stdin.readline()
939 943 if line.lower().startswith('y') and 'n' not in line.lower():
940 944 self.log.critical("Shutdown confirmed")
941 945 ioloop.IOLoop.current().stop()
942 946 return
943 947 else:
944 948 print("No answer for 5s:", end=' ')
945 949 print("resuming operation...")
946 950 # no answer, or answer is no:
947 951 # set it back to original SIGINT handler
948 952 # use IOLoop.add_callback because signal.signal must be called
949 953 # from main thread
950 954 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
951 955
952 956 def _signal_stop(self, sig, frame):
953 957 self.log.critical("received signal %s, stopping", sig)
954 958 ioloop.IOLoop.current().stop()
955 959
956 960 def _signal_info(self, sig, frame):
957 961 print(self.notebook_info())
958 962
959 963 def init_components(self):
960 964 """Check the components submodule, and warn if it's unclean"""
961 965 status = submodule.check_submodule_status()
962 966 if status == 'missing':
963 967 self.log.warn("components submodule missing, running `git submodule update`")
964 968 submodule.update_submodules(submodule.ipython_parent())
965 969 elif status == 'unclean':
966 970 self.log.warn("components submodule unclean, you may see 404s on static/components")
967 971 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
968 972
969 973 def init_server_extensions(self):
970 974 """Load any extensions specified by config.
971 975
972 976 Import the module, then call the load_jupyter_server_extension function,
973 977 if one exists.
974 978
975 979 The extension API is experimental, and may change in future releases.
976 980 """
977 981 for modulename in self.server_extensions:
978 982 try:
979 983 mod = importlib.import_module(modulename)
980 984 func = getattr(mod, 'load_jupyter_server_extension', None)
981 985 if func is not None:
982 986 func(self)
983 987 except Exception:
984 988 if self.reraise_server_extension_failures:
985 989 raise
986 990 self.log.warn("Error loading server extension %s", modulename,
987 991 exc_info=True)
988 992
989 993 @catch_config_error
990 994 def initialize(self, argv=None):
991 995 super(NotebookApp, self).initialize(argv)
992 996 self.init_logging()
993 997 self.init_configurables()
994 998 self.init_components()
995 999 self.init_webapp()
996 1000 self.init_terminals()
997 1001 self.init_signal()
998 1002 self.init_server_extensions()
999 1003
1000 1004 def cleanup_kernels(self):
1001 1005 """Shutdown all kernels.
1002 1006
1003 1007 The kernels will shutdown themselves when this process no longer exists,
1004 1008 but explicit shutdown allows the KernelManagers to cleanup the connection files.
1005 1009 """
1006 1010 self.log.info('Shutting down kernels')
1007 1011 self.kernel_manager.shutdown_all()
1008 1012
1009 1013 def notebook_info(self):
1010 1014 "Return the current working directory and the server url information"
1011 1015 info = self.contents_manager.info_string() + "\n"
1012 1016 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
1013 1017 return info + "The IPython Notebook is running at: %s" % self.display_url
1014 1018
1015 1019 def server_info(self):
1016 1020 """Return a JSONable dict of information about this server."""
1017 1021 return {'url': self.connection_url,
1018 1022 'hostname': self.ip if self.ip else 'localhost',
1019 1023 'port': self.port,
1020 1024 'secure': bool(self.certfile),
1021 1025 'base_url': self.base_url,
1022 1026 'notebook_dir': os.path.abspath(self.notebook_dir),
1023 1027 'pid': os.getpid()
1024 1028 }
1025 1029
1026 1030 def write_server_info_file(self):
1027 1031 """Write the result of server_info() to the JSON file info_file."""
1028 1032 with open(self.info_file, 'w') as f:
1029 1033 json.dump(self.server_info(), f, indent=2)
1030 1034
1031 1035 def remove_server_info_file(self):
1032 1036 """Remove the nbserver-<pid>.json file created for this server.
1033 1037
1034 1038 Ignores the error raised when the file has already been removed.
1035 1039 """
1036 1040 try:
1037 1041 os.unlink(self.info_file)
1038 1042 except OSError as e:
1039 1043 if e.errno != errno.ENOENT:
1040 1044 raise
1041 1045
1042 1046 def start(self):
1043 1047 """ Start the IPython Notebook server app, after initialization
1044 1048
1045 1049 This method takes no arguments so all configuration and initialization
1046 1050 must be done prior to calling this method."""
1047 1051 if self.subapp is not None:
1048 1052 return self.subapp.start()
1049 1053
1050 1054 info = self.log.info
1051 1055 for line in self.notebook_info().split("\n"):
1052 1056 info(line)
1053 1057 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1054 1058
1055 1059 self.write_server_info_file()
1056 1060
1057 1061 if self.open_browser or self.file_to_run:
1058 1062 try:
1059 1063 browser = webbrowser.get(self.browser or None)
1060 1064 except webbrowser.Error as e:
1061 1065 self.log.warn('No web browser found: %s.' % e)
1062 1066 browser = None
1063 1067
1064 1068 if self.file_to_run:
1065 1069 if not os.path.exists(self.file_to_run):
1066 1070 self.log.critical("%s does not exist" % self.file_to_run)
1067 1071 self.exit(1)
1068 1072
1069 1073 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1070 1074 uri = url_path_join('notebooks', *relpath.split(os.sep))
1071 1075 else:
1072 1076 uri = 'tree'
1073 1077 if browser:
1074 1078 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1075 1079 new=2)
1076 1080 threading.Thread(target=b).start()
1077 1081
1078 1082 self.io_loop = ioloop.IOLoop.current()
1079 1083 if sys.platform.startswith('win'):
1080 1084 # add no-op to wake every 5s
1081 1085 # to handle signals that may be ignored by the inner loop
1082 1086 pc = ioloop.PeriodicCallback(lambda : None, 5000)
1083 1087 pc.start()
1084 1088 try:
1085 1089 self.io_loop.start()
1086 1090 except KeyboardInterrupt:
1087 1091 info("Interrupted...")
1088 1092 finally:
1089 1093 self.cleanup_kernels()
1090 1094 self.remove_server_info_file()
1091 1095
1092 1096 def stop(self):
1093 1097 def _stop():
1094 1098 self.http_server.stop()
1095 1099 self.io_loop.stop()
1096 1100 self.io_loop.add_callback(_stop)
1097 1101
1098 1102
1099 1103 def list_running_servers(profile='default'):
1100 1104 """Iterate over the server info files of running notebook servers.
1101 1105
1102 1106 Given a profile name, find nbserver-* files in the security directory of
1103 1107 that profile, and yield dicts of their information, each one pertaining to
1104 1108 a currently running notebook server instance.
1105 1109 """
1106 1110 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1107 1111 for file in os.listdir(pd.security_dir):
1108 1112 if file.startswith('nbserver-'):
1109 1113 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1110 1114 info = json.load(f)
1111 1115
1112 1116 # Simple check whether that process is really still running
1113 1117 # Also remove leftover files from IPython 2.x without a pid field
1114 1118 if ('pid' in info) and check_pid(info['pid']):
1115 1119 yield info
1116 1120 else:
1117 1121 # If the process has died, try to delete its info file
1118 1122 try:
1119 1123 os.unlink(file)
1120 1124 except OSError:
1121 1125 pass # TODO: This should warn or log or something
1122 1126 #-----------------------------------------------------------------------------
1123 1127 # Main entry point
1124 1128 #-----------------------------------------------------------------------------
1125 1129
1126 1130 launch_new_instance = NotebookApp.launch_instance
1127 1131
General Comments 0
You need to be logged in to leave comments. Login now