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