##// END OF EJS Templates
Notaries sign notebooks now
MinRK -
Show More
@@ -1,874 +1,874 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 from __future__ import print_function
8 from __future__ import print_function
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # stdlib
20 # stdlib
21 import errno
21 import errno
22 import io
22 import io
23 import json
23 import json
24 import logging
24 import logging
25 import os
25 import os
26 import random
26 import random
27 import select
27 import select
28 import signal
28 import signal
29 import socket
29 import socket
30 import sys
30 import sys
31 import threading
31 import threading
32 import time
32 import time
33 import webbrowser
33 import webbrowser
34
34
35
35
36 # Third party
36 # Third party
37 # check for pyzmq 2.1.11
37 # check for pyzmq 2.1.11
38 from IPython.utils.zmqrelated import check_for_zmq
38 from IPython.utils.zmqrelated import check_for_zmq
39 check_for_zmq('2.1.11', 'IPython.html')
39 check_for_zmq('2.1.11', 'IPython.html')
40
40
41 from jinja2 import Environment, FileSystemLoader
41 from jinja2 import Environment, FileSystemLoader
42
42
43 # Install the pyzmq ioloop. This has to be done before anything else from
43 # Install the pyzmq ioloop. This has to be done before anything else from
44 # tornado is imported.
44 # tornado is imported.
45 from zmq.eventloop import ioloop
45 from zmq.eventloop import ioloop
46 ioloop.install()
46 ioloop.install()
47
47
48 # check for tornado 3.1.0
48 # check for tornado 3.1.0
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 try:
50 try:
51 import tornado
51 import tornado
52 except ImportError:
52 except ImportError:
53 raise ImportError(msg)
53 raise ImportError(msg)
54 try:
54 try:
55 version_info = tornado.version_info
55 version_info = tornado.version_info
56 except AttributeError:
56 except AttributeError:
57 raise ImportError(msg + ", but you have < 1.1.0")
57 raise ImportError(msg + ", but you have < 1.1.0")
58 if version_info < (3,1,0):
58 if version_info < (3,1,0):
59 raise ImportError(msg + ", but you have %s" % tornado.version)
59 raise ImportError(msg + ", but you have %s" % tornado.version)
60
60
61 from tornado import httpserver
61 from tornado import httpserver
62 from tornado import web
62 from tornado import web
63
63
64 # Our own libraries
64 # Our own libraries
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 from .base.handlers import Template404
66 from .base.handlers import Template404
67 from .log import log_request
67 from .log import log_request
68 from .services.kernels.kernelmanager import MappingKernelManager
68 from .services.kernels.kernelmanager import MappingKernelManager
69 from .services.notebooks.nbmanager import NotebookManager
69 from .services.notebooks.nbmanager import NotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
71 from .services.clusters.clustermanager import ClusterManager
71 from .services.clusters.clustermanager import ClusterManager
72 from .services.sessions.sessionmanager import SessionManager
72 from .services.sessions.sessionmanager import SessionManager
73
73
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75
75
76 from IPython.config.application import catch_config_error, boolean_flag
76 from IPython.config.application import catch_config_error, boolean_flag
77 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.application import BaseIPythonApplication
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.consoleapp import IPythonConsoleApp
79 from IPython.consoleapp import IPythonConsoleApp
80 from IPython.kernel import swallow_argv
80 from IPython.kernel import swallow_argv
81 from IPython.kernel.zmq.session import default_secure
81 from IPython.kernel.zmq.session import default_secure
82 from IPython.kernel.zmq.kernelapp import (
82 from IPython.kernel.zmq.kernelapp import (
83 kernel_flags,
83 kernel_flags,
84 kernel_aliases,
84 kernel_aliases,
85 )
85 )
86 from IPython.nbformat import current, sign
86 from IPython.nbformat import current, sign
87 from IPython.utils.importstring import import_item
87 from IPython.utils.importstring import import_item
88 from IPython.utils.localinterfaces import localhost
88 from IPython.utils.localinterfaces import localhost
89 from IPython.utils import submodule
89 from IPython.utils import submodule
90 from IPython.utils.traitlets import (
90 from IPython.utils.traitlets import (
91 Dict, Unicode, Integer, List, Bool, Bytes,
91 Dict, Unicode, Integer, List, Bool, Bytes,
92 DottedObjectName, Instance,
92 DottedObjectName, Instance,
93 )
93 )
94 from IPython.utils import py3compat
94 from IPython.utils import py3compat
95 from IPython.utils.path import filefind, get_ipython_dir
95 from IPython.utils.path import filefind, get_ipython_dir
96
96
97 from .utils import url_path_join
97 from .utils import url_path_join
98
98
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100 # Module globals
100 # Module globals
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102
102
103 _examples = """
103 _examples = """
104 ipython notebook # start the notebook
104 ipython notebook # start the notebook
105 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --profile=sympy # use the sympy profile
106 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
107 """
107 """
108
108
109 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
110 # Helper functions
110 # Helper functions
111 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
112
112
113 def random_ports(port, n):
113 def random_ports(port, n):
114 """Generate a list of n random ports near the given port.
114 """Generate a list of n random ports near the given port.
115
115
116 The first 5 ports will be sequential, and the remaining n-5 will be
116 The first 5 ports will be sequential, and the remaining n-5 will be
117 randomly selected in the range [port-2*n, port+2*n].
117 randomly selected in the range [port-2*n, port+2*n].
118 """
118 """
119 for i in range(min(5, n)):
119 for i in range(min(5, n)):
120 yield port + i
120 yield port + i
121 for i in range(n-5):
121 for i in range(n-5):
122 yield max(1, port + random.randint(-2*n, 2*n))
122 yield max(1, port + random.randint(-2*n, 2*n))
123
123
124 def load_handlers(name):
124 def load_handlers(name):
125 """Load the (URL pattern, handler) tuples for each component."""
125 """Load the (URL pattern, handler) tuples for each component."""
126 name = 'IPython.html.' + name
126 name = 'IPython.html.' + name
127 mod = __import__(name, fromlist=['default_handlers'])
127 mod = __import__(name, fromlist=['default_handlers'])
128 return mod.default_handlers
128 return mod.default_handlers
129
129
130 #-----------------------------------------------------------------------------
130 #-----------------------------------------------------------------------------
131 # The Tornado web application
131 # The Tornado web application
132 #-----------------------------------------------------------------------------
132 #-----------------------------------------------------------------------------
133
133
134 class NotebookWebApplication(web.Application):
134 class NotebookWebApplication(web.Application):
135
135
136 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 def __init__(self, ipython_app, kernel_manager, notebook_manager,
137 cluster_manager, session_manager, log, base_project_url,
137 cluster_manager, session_manager, log, base_project_url,
138 settings_overrides):
138 settings_overrides):
139
139
140 settings = self.init_settings(
140 settings = self.init_settings(
141 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 ipython_app, kernel_manager, notebook_manager, cluster_manager,
142 session_manager, log, base_project_url, settings_overrides)
142 session_manager, log, base_project_url, settings_overrides)
143 handlers = self.init_handlers(settings)
143 handlers = self.init_handlers(settings)
144
144
145 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 super(NotebookWebApplication, self).__init__(handlers, **settings)
146
146
147 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
148 cluster_manager, session_manager, log, base_project_url,
148 cluster_manager, session_manager, log, base_project_url,
149 settings_overrides):
149 settings_overrides):
150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
151 # base_project_url will always be unicode, which will in turn
151 # base_project_url will always be unicode, which will in turn
152 # make the patterns unicode, and ultimately result in unicode
152 # make the patterns unicode, and ultimately result in unicode
153 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # keys in kwargs to handler._execute(**kwargs) in tornado.
154 # This enforces that base_project_url be ascii in that situation.
154 # This enforces that base_project_url be ascii in that situation.
155 #
155 #
156 # Note that the URLs these patterns check against are escaped,
156 # Note that the URLs these patterns check against are escaped,
157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
158 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
158 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
159 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
160 settings = dict(
160 settings = dict(
161 # basics
161 # basics
162 log_function=log_request,
162 log_function=log_request,
163 base_project_url=base_project_url,
163 base_project_url=base_project_url,
164 base_kernel_url=ipython_app.base_kernel_url,
164 base_kernel_url=ipython_app.base_kernel_url,
165 template_path=template_path,
165 template_path=template_path,
166 static_path=ipython_app.static_file_path,
166 static_path=ipython_app.static_file_path,
167 static_handler_class = FileFindHandler,
167 static_handler_class = FileFindHandler,
168 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 static_url_prefix = url_path_join(base_project_url,'/static/'),
169
169
170 # authentication
170 # authentication
171 cookie_secret=ipython_app.cookie_secret,
171 cookie_secret=ipython_app.cookie_secret,
172 login_url=url_path_join(base_project_url,'/login'),
172 login_url=url_path_join(base_project_url,'/login'),
173 password=ipython_app.password,
173 password=ipython_app.password,
174
174
175 # managers
175 # managers
176 kernel_manager=kernel_manager,
176 kernel_manager=kernel_manager,
177 notebook_manager=notebook_manager,
177 notebook_manager=notebook_manager,
178 cluster_manager=cluster_manager,
178 cluster_manager=cluster_manager,
179 session_manager=session_manager,
179 session_manager=session_manager,
180
180
181 # IPython stuff
181 # IPython stuff
182 nbextensions_path = ipython_app.nbextensions_path,
182 nbextensions_path = ipython_app.nbextensions_path,
183 mathjax_url=ipython_app.mathjax_url,
183 mathjax_url=ipython_app.mathjax_url,
184 config=ipython_app.config,
184 config=ipython_app.config,
185 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
186 )
186 )
187
187
188 # allow custom overrides for the tornado web app.
188 # allow custom overrides for the tornado web app.
189 settings.update(settings_overrides)
189 settings.update(settings_overrides)
190 return settings
190 return settings
191
191
192 def init_handlers(self, settings):
192 def init_handlers(self, settings):
193 # Load the (URL pattern, handler) tuples for each component.
193 # Load the (URL pattern, handler) tuples for each component.
194 handlers = []
194 handlers = []
195 handlers.extend(load_handlers('base.handlers'))
195 handlers.extend(load_handlers('base.handlers'))
196 handlers.extend(load_handlers('tree.handlers'))
196 handlers.extend(load_handlers('tree.handlers'))
197 handlers.extend(load_handlers('auth.login'))
197 handlers.extend(load_handlers('auth.login'))
198 handlers.extend(load_handlers('auth.logout'))
198 handlers.extend(load_handlers('auth.logout'))
199 handlers.extend(load_handlers('notebook.handlers'))
199 handlers.extend(load_handlers('notebook.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
201 handlers.extend(load_handlers('services.kernels.handlers'))
201 handlers.extend(load_handlers('services.kernels.handlers'))
202 handlers.extend(load_handlers('services.notebooks.handlers'))
202 handlers.extend(load_handlers('services.notebooks.handlers'))
203 handlers.extend(load_handlers('services.clusters.handlers'))
203 handlers.extend(load_handlers('services.clusters.handlers'))
204 handlers.extend(load_handlers('services.sessions.handlers'))
204 handlers.extend(load_handlers('services.sessions.handlers'))
205 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 handlers.extend(load_handlers('services.nbconvert.handlers'))
206 handlers.extend([
206 handlers.extend([
207 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
209 ])
209 ])
210 # prepend base_project_url onto the patterns that we match
210 # prepend base_project_url onto the patterns that we match
211 new_handlers = []
211 new_handlers = []
212 for handler in handlers:
212 for handler in handlers:
213 pattern = url_path_join(settings['base_project_url'], handler[0])
213 pattern = url_path_join(settings['base_project_url'], handler[0])
214 new_handler = tuple([pattern] + list(handler[1:]))
214 new_handler = tuple([pattern] + list(handler[1:]))
215 new_handlers.append(new_handler)
215 new_handlers.append(new_handler)
216 # add 404 on the end, which will catch everything that falls through
216 # add 404 on the end, which will catch everything that falls through
217 new_handlers.append((r'(.*)', Template404))
217 new_handlers.append((r'(.*)', Template404))
218 return new_handlers
218 return new_handlers
219
219
220
220
221 #-----------------------------------------------------------------------------
221 #-----------------------------------------------------------------------------
222 # Subcommands
222 # Subcommands
223 #-----------------------------------------------------------------------------
223 #-----------------------------------------------------------------------------
224
224
225 class NbserverListApp(BaseIPythonApplication):
225 class NbserverListApp(BaseIPythonApplication):
226
226
227 description="List currently running notebook servers in this profile."
227 description="List currently running notebook servers in this profile."
228
228
229 flags = dict(
229 flags = dict(
230 json=({'NbserverListApp': {'json': True}},
230 json=({'NbserverListApp': {'json': True}},
231 "Produce machine-readable JSON output."),
231 "Produce machine-readable JSON output."),
232 )
232 )
233
233
234 json = Bool(False, config=True,
234 json = Bool(False, config=True,
235 help="If True, each line of output will be a JSON object with the "
235 help="If True, each line of output will be a JSON object with the "
236 "details from the server info file.")
236 "details from the server info file.")
237
237
238 def start(self):
238 def start(self):
239 if not self.json:
239 if not self.json:
240 print("Currently running servers:")
240 print("Currently running servers:")
241 for serverinfo in list_running_servers(self.profile):
241 for serverinfo in list_running_servers(self.profile):
242 if self.json:
242 if self.json:
243 print(json.dumps(serverinfo))
243 print(json.dumps(serverinfo))
244 else:
244 else:
245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
246
246
247
247
248 class NotebookTrustApp(BaseIPythonApplication):
248 class NotebookTrustApp(BaseIPythonApplication):
249
249
250 description="""Sign one or more IPython notebooks with your key,
250 description="""Sign one or more IPython notebooks with your key,
251 to trust their dynamic (HTML, Javascript) output."""
251 to trust their dynamic (HTML, Javascript) output."""
252
252
253 examples="""ipython notebook trust mynotebook.ipynb"""
253 examples="""ipython notebook trust mynotebook.ipynb"""
254
254
255 notary = Instance(sign.NotebookNotary)
255 notary = Instance(sign.NotebookNotary)
256 def _notary_default(self):
256 def _notary_default(self):
257 return sign.NotebookNotary(parent=self, profile_dir=self.profile_dir)
257 return sign.NotebookNotary(parent=self, profile_dir=self.profile_dir)
258
258
259 def sign_notebook(self, notebook_path):
259 def sign_notebook(self, notebook_path):
260 if not os.path.exists(notebook_path):
260 if not os.path.exists(notebook_path):
261 self.log.error("Notebook missing: %s" % notebook_path)
261 self.log.error("Notebook missing: %s" % notebook_path)
262 self.exit(1)
262 self.exit(1)
263 with io.open(notebook_path, encoding='utf8') as f:
263 with io.open(notebook_path, encoding='utf8') as f:
264 nb = current.read(f, 'json')
264 nb = current.read(f, 'json')
265 sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme)
265 self.notary.sign(nb)
266 with io.open(notebook_path, 'w', encoding='utf8') as f:
266 with io.open(notebook_path, 'w', encoding='utf8') as f:
267 current.write(nb, f, 'json')
267 current.write(nb, f, 'json')
268
268
269 def start(self):
269 def start(self):
270 if not self.extra_args:
270 if not self.extra_args:
271 self.log.critical("Specify at least one notebook to sign.")
271 self.log.critical("Specify at least one notebook to sign.")
272 self.exit(1)
272 self.exit(1)
273
273
274 for notebook_path in self.extra_args:
274 for notebook_path in self.extra_args:
275 print("Signing notebook: %s" % notebook_path)
275 print("Signing notebook: %s" % notebook_path)
276 self.sign_notebook(notebook_path)
276 self.sign_notebook(notebook_path)
277
277
278 #-----------------------------------------------------------------------------
278 #-----------------------------------------------------------------------------
279 # Aliases and Flags
279 # Aliases and Flags
280 #-----------------------------------------------------------------------------
280 #-----------------------------------------------------------------------------
281
281
282 flags = dict(kernel_flags)
282 flags = dict(kernel_flags)
283 flags['no-browser']=(
283 flags['no-browser']=(
284 {'NotebookApp' : {'open_browser' : False}},
284 {'NotebookApp' : {'open_browser' : False}},
285 "Don't open the notebook in a browser after startup."
285 "Don't open the notebook in a browser after startup."
286 )
286 )
287 flags['no-mathjax']=(
287 flags['no-mathjax']=(
288 {'NotebookApp' : {'enable_mathjax' : False}},
288 {'NotebookApp' : {'enable_mathjax' : False}},
289 """Disable MathJax
289 """Disable MathJax
290
290
291 MathJax is the javascript library IPython uses to render math/LaTeX. It is
291 MathJax is the javascript library IPython uses to render math/LaTeX. It is
292 very large, so you may want to disable it if you have a slow internet
292 very large, so you may want to disable it if you have a slow internet
293 connection, or for offline use of the notebook.
293 connection, or for offline use of the notebook.
294
294
295 When disabled, equations etc. will appear as their untransformed TeX source.
295 When disabled, equations etc. will appear as their untransformed TeX source.
296 """
296 """
297 )
297 )
298
298
299 # Add notebook manager flags
299 # Add notebook manager flags
300 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
300 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
301 'Auto-save a .py script everytime the .ipynb notebook is saved',
301 'Auto-save a .py script everytime the .ipynb notebook is saved',
302 'Do not auto-save .py scripts for every notebook'))
302 'Do not auto-save .py scripts for every notebook'))
303
303
304 # the flags that are specific to the frontend
304 # the flags that are specific to the frontend
305 # these must be scrubbed before being passed to the kernel,
305 # these must be scrubbed before being passed to the kernel,
306 # or it will raise an error on unrecognized flags
306 # or it will raise an error on unrecognized flags
307 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
307 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
308
308
309 aliases = dict(kernel_aliases)
309 aliases = dict(kernel_aliases)
310
310
311 aliases.update({
311 aliases.update({
312 'ip': 'NotebookApp.ip',
312 'ip': 'NotebookApp.ip',
313 'port': 'NotebookApp.port',
313 'port': 'NotebookApp.port',
314 'port-retries': 'NotebookApp.port_retries',
314 'port-retries': 'NotebookApp.port_retries',
315 'transport': 'KernelManager.transport',
315 'transport': 'KernelManager.transport',
316 'keyfile': 'NotebookApp.keyfile',
316 'keyfile': 'NotebookApp.keyfile',
317 'certfile': 'NotebookApp.certfile',
317 'certfile': 'NotebookApp.certfile',
318 'notebook-dir': 'NotebookManager.notebook_dir',
318 'notebook-dir': 'NotebookManager.notebook_dir',
319 'browser': 'NotebookApp.browser',
319 'browser': 'NotebookApp.browser',
320 })
320 })
321
321
322 # remove ipkernel flags that are singletons, and don't make sense in
322 # remove ipkernel flags that are singletons, and don't make sense in
323 # multi-kernel evironment:
323 # multi-kernel evironment:
324 aliases.pop('f', None)
324 aliases.pop('f', None)
325
325
326 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
326 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
327 u'notebook-dir', u'profile', u'profile-dir']
327 u'notebook-dir', u'profile', u'profile-dir']
328
328
329 #-----------------------------------------------------------------------------
329 #-----------------------------------------------------------------------------
330 # NotebookApp
330 # NotebookApp
331 #-----------------------------------------------------------------------------
331 #-----------------------------------------------------------------------------
332
332
333 class NotebookApp(BaseIPythonApplication):
333 class NotebookApp(BaseIPythonApplication):
334
334
335 name = 'ipython-notebook'
335 name = 'ipython-notebook'
336
336
337 description = """
337 description = """
338 The IPython HTML Notebook.
338 The IPython HTML Notebook.
339
339
340 This launches a Tornado based HTML Notebook Server that serves up an
340 This launches a Tornado based HTML Notebook Server that serves up an
341 HTML5/Javascript Notebook client.
341 HTML5/Javascript Notebook client.
342 """
342 """
343 examples = _examples
343 examples = _examples
344
344
345 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
345 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
346 FileNotebookManager]
346 FileNotebookManager]
347 flags = Dict(flags)
347 flags = Dict(flags)
348 aliases = Dict(aliases)
348 aliases = Dict(aliases)
349
349
350 subcommands = dict(
350 subcommands = dict(
351 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
351 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
352 trust=(NotebookTrustApp, NotebookTrustApp.description),
352 trust=(NotebookTrustApp, NotebookTrustApp.description),
353 )
353 )
354
354
355 kernel_argv = List(Unicode)
355 kernel_argv = List(Unicode)
356
356
357 def _log_level_default(self):
357 def _log_level_default(self):
358 return logging.INFO
358 return logging.INFO
359
359
360 def _log_format_default(self):
360 def _log_format_default(self):
361 """override default log format to include time"""
361 """override default log format to include time"""
362 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
362 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
363
363
364 # create requested profiles by default, if they don't exist:
364 # create requested profiles by default, if they don't exist:
365 auto_create = Bool(True)
365 auto_create = Bool(True)
366
366
367 # file to be opened in the notebook server
367 # file to be opened in the notebook server
368 file_to_run = Unicode('')
368 file_to_run = Unicode('')
369
369
370 # Network related information.
370 # Network related information.
371
371
372 ip = Unicode(config=True,
372 ip = Unicode(config=True,
373 help="The IP address the notebook server will listen on."
373 help="The IP address the notebook server will listen on."
374 )
374 )
375 def _ip_default(self):
375 def _ip_default(self):
376 return localhost()
376 return localhost()
377
377
378 def _ip_changed(self, name, old, new):
378 def _ip_changed(self, name, old, new):
379 if new == u'*': self.ip = u''
379 if new == u'*': self.ip = u''
380
380
381 port = Integer(8888, config=True,
381 port = Integer(8888, config=True,
382 help="The port the notebook server will listen on."
382 help="The port the notebook server will listen on."
383 )
383 )
384 port_retries = Integer(50, config=True,
384 port_retries = Integer(50, config=True,
385 help="The number of additional ports to try if the specified port is not available."
385 help="The number of additional ports to try if the specified port is not available."
386 )
386 )
387
387
388 certfile = Unicode(u'', config=True,
388 certfile = Unicode(u'', config=True,
389 help="""The full path to an SSL/TLS certificate file."""
389 help="""The full path to an SSL/TLS certificate file."""
390 )
390 )
391
391
392 keyfile = Unicode(u'', config=True,
392 keyfile = Unicode(u'', config=True,
393 help="""The full path to a private key file for usage with SSL/TLS."""
393 help="""The full path to a private key file for usage with SSL/TLS."""
394 )
394 )
395
395
396 cookie_secret = Bytes(b'', config=True,
396 cookie_secret = Bytes(b'', config=True,
397 help="""The random bytes used to secure cookies.
397 help="""The random bytes used to secure cookies.
398 By default this is a new random number every time you start the Notebook.
398 By default this is a new random number every time you start the Notebook.
399 Set it to a value in a config file to enable logins to persist across server sessions.
399 Set it to a value in a config file to enable logins to persist across server sessions.
400
400
401 Note: Cookie secrets should be kept private, do not share config files with
401 Note: Cookie secrets should be kept private, do not share config files with
402 cookie_secret stored in plaintext (you can read the value from a file).
402 cookie_secret stored in plaintext (you can read the value from a file).
403 """
403 """
404 )
404 )
405 def _cookie_secret_default(self):
405 def _cookie_secret_default(self):
406 return os.urandom(1024)
406 return os.urandom(1024)
407
407
408 password = Unicode(u'', config=True,
408 password = Unicode(u'', config=True,
409 help="""Hashed password to use for web authentication.
409 help="""Hashed password to use for web authentication.
410
410
411 To generate, type in a python/IPython shell:
411 To generate, type in a python/IPython shell:
412
412
413 from IPython.lib import passwd; passwd()
413 from IPython.lib import passwd; passwd()
414
414
415 The string should be of the form type:salt:hashed-password.
415 The string should be of the form type:salt:hashed-password.
416 """
416 """
417 )
417 )
418
418
419 open_browser = Bool(True, config=True,
419 open_browser = Bool(True, config=True,
420 help="""Whether to open in a browser after starting.
420 help="""Whether to open in a browser after starting.
421 The specific browser used is platform dependent and
421 The specific browser used is platform dependent and
422 determined by the python standard library `webbrowser`
422 determined by the python standard library `webbrowser`
423 module, unless it is overridden using the --browser
423 module, unless it is overridden using the --browser
424 (NotebookApp.browser) configuration option.
424 (NotebookApp.browser) configuration option.
425 """)
425 """)
426
426
427 browser = Unicode(u'', config=True,
427 browser = Unicode(u'', config=True,
428 help="""Specify what command to use to invoke a web
428 help="""Specify what command to use to invoke a web
429 browser when opening the notebook. If not specified, the
429 browser when opening the notebook. If not specified, the
430 default browser will be determined by the `webbrowser`
430 default browser will be determined by the `webbrowser`
431 standard library module, which allows setting of the
431 standard library module, which allows setting of the
432 BROWSER environment variable to override it.
432 BROWSER environment variable to override it.
433 """)
433 """)
434
434
435 webapp_settings = Dict(config=True,
435 webapp_settings = Dict(config=True,
436 help="Supply overrides for the tornado.web.Application that the "
436 help="Supply overrides for the tornado.web.Application that the "
437 "IPython notebook uses.")
437 "IPython notebook uses.")
438
438
439 enable_mathjax = Bool(True, config=True,
439 enable_mathjax = Bool(True, config=True,
440 help="""Whether to enable MathJax for typesetting math/TeX
440 help="""Whether to enable MathJax for typesetting math/TeX
441
441
442 MathJax is the javascript library IPython uses to render math/LaTeX. It is
442 MathJax is the javascript library IPython uses to render math/LaTeX. It is
443 very large, so you may want to disable it if you have a slow internet
443 very large, so you may want to disable it if you have a slow internet
444 connection, or for offline use of the notebook.
444 connection, or for offline use of the notebook.
445
445
446 When disabled, equations etc. will appear as their untransformed TeX source.
446 When disabled, equations etc. will appear as their untransformed TeX source.
447 """
447 """
448 )
448 )
449 def _enable_mathjax_changed(self, name, old, new):
449 def _enable_mathjax_changed(self, name, old, new):
450 """set mathjax url to empty if mathjax is disabled"""
450 """set mathjax url to empty if mathjax is disabled"""
451 if not new:
451 if not new:
452 self.mathjax_url = u''
452 self.mathjax_url = u''
453
453
454 base_project_url = Unicode('/', config=True,
454 base_project_url = Unicode('/', config=True,
455 help='''The base URL for the notebook server.
455 help='''The base URL for the notebook server.
456
456
457 Leading and trailing slashes can be omitted,
457 Leading and trailing slashes can be omitted,
458 and will automatically be added.
458 and will automatically be added.
459 ''')
459 ''')
460 def _base_project_url_changed(self, name, old, new):
460 def _base_project_url_changed(self, name, old, new):
461 if not new.startswith('/'):
461 if not new.startswith('/'):
462 self.base_project_url = '/'+new
462 self.base_project_url = '/'+new
463 elif not new.endswith('/'):
463 elif not new.endswith('/'):
464 self.base_project_url = new+'/'
464 self.base_project_url = new+'/'
465
465
466 base_kernel_url = Unicode('/', config=True,
466 base_kernel_url = Unicode('/', config=True,
467 help='''The base URL for the kernel server
467 help='''The base URL for the kernel server
468
468
469 Leading and trailing slashes can be omitted,
469 Leading and trailing slashes can be omitted,
470 and will automatically be added.
470 and will automatically be added.
471 ''')
471 ''')
472 def _base_kernel_url_changed(self, name, old, new):
472 def _base_kernel_url_changed(self, name, old, new):
473 if not new.startswith('/'):
473 if not new.startswith('/'):
474 self.base_kernel_url = '/'+new
474 self.base_kernel_url = '/'+new
475 elif not new.endswith('/'):
475 elif not new.endswith('/'):
476 self.base_kernel_url = new+'/'
476 self.base_kernel_url = new+'/'
477
477
478 websocket_url = Unicode("", config=True,
478 websocket_url = Unicode("", config=True,
479 help="""The base URL for the websocket server,
479 help="""The base URL for the websocket server,
480 if it differs from the HTTP server (hint: it almost certainly doesn't).
480 if it differs from the HTTP server (hint: it almost certainly doesn't).
481
481
482 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
482 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
483 """
483 """
484 )
484 )
485
485
486 extra_static_paths = List(Unicode, config=True,
486 extra_static_paths = List(Unicode, config=True,
487 help="""Extra paths to search for serving static files.
487 help="""Extra paths to search for serving static files.
488
488
489 This allows adding javascript/css to be available from the notebook server machine,
489 This allows adding javascript/css to be available from the notebook server machine,
490 or overriding individual files in the IPython"""
490 or overriding individual files in the IPython"""
491 )
491 )
492 def _extra_static_paths_default(self):
492 def _extra_static_paths_default(self):
493 return [os.path.join(self.profile_dir.location, 'static')]
493 return [os.path.join(self.profile_dir.location, 'static')]
494
494
495 @property
495 @property
496 def static_file_path(self):
496 def static_file_path(self):
497 """return extra paths + the default location"""
497 """return extra paths + the default location"""
498 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
498 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
499
499
500 nbextensions_path = List(Unicode, config=True,
500 nbextensions_path = List(Unicode, config=True,
501 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
501 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
502 )
502 )
503 def _nbextensions_path_default(self):
503 def _nbextensions_path_default(self):
504 return [os.path.join(get_ipython_dir(), 'nbextensions')]
504 return [os.path.join(get_ipython_dir(), 'nbextensions')]
505
505
506 mathjax_url = Unicode("", config=True,
506 mathjax_url = Unicode("", config=True,
507 help="""The url for MathJax.js."""
507 help="""The url for MathJax.js."""
508 )
508 )
509 def _mathjax_url_default(self):
509 def _mathjax_url_default(self):
510 if not self.enable_mathjax:
510 if not self.enable_mathjax:
511 return u''
511 return u''
512 static_url_prefix = self.webapp_settings.get("static_url_prefix",
512 static_url_prefix = self.webapp_settings.get("static_url_prefix",
513 url_path_join(self.base_project_url, "static")
513 url_path_join(self.base_project_url, "static")
514 )
514 )
515
515
516 # try local mathjax, either in nbextensions/mathjax or static/mathjax
516 # try local mathjax, either in nbextensions/mathjax or static/mathjax
517 for (url_prefix, search_path) in [
517 for (url_prefix, search_path) in [
518 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
518 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
519 (static_url_prefix, self.static_file_path),
519 (static_url_prefix, self.static_file_path),
520 ]:
520 ]:
521 self.log.debug("searching for local mathjax in %s", search_path)
521 self.log.debug("searching for local mathjax in %s", search_path)
522 try:
522 try:
523 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
523 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
524 except IOError:
524 except IOError:
525 continue
525 continue
526 else:
526 else:
527 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
527 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
528 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
528 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
529 return url
529 return url
530
530
531 # no local mathjax, serve from CDN
531 # no local mathjax, serve from CDN
532 if self.certfile:
532 if self.certfile:
533 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
533 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
534 host = u"https://c328740.ssl.cf1.rackcdn.com"
534 host = u"https://c328740.ssl.cf1.rackcdn.com"
535 else:
535 else:
536 host = u"http://cdn.mathjax.org"
536 host = u"http://cdn.mathjax.org"
537
537
538 url = host + u"/mathjax/latest/MathJax.js"
538 url = host + u"/mathjax/latest/MathJax.js"
539 self.log.info("Using MathJax from CDN: %s", url)
539 self.log.info("Using MathJax from CDN: %s", url)
540 return url
540 return url
541
541
542 def _mathjax_url_changed(self, name, old, new):
542 def _mathjax_url_changed(self, name, old, new):
543 if new and not self.enable_mathjax:
543 if new and not self.enable_mathjax:
544 # enable_mathjax=False overrides mathjax_url
544 # enable_mathjax=False overrides mathjax_url
545 self.mathjax_url = u''
545 self.mathjax_url = u''
546 else:
546 else:
547 self.log.info("Using MathJax: %s", new)
547 self.log.info("Using MathJax: %s", new)
548
548
549 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
549 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
550 config=True,
550 config=True,
551 help='The notebook manager class to use.')
551 help='The notebook manager class to use.')
552
552
553 trust_xheaders = Bool(False, config=True,
553 trust_xheaders = Bool(False, config=True,
554 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
554 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
555 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
555 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
556 )
556 )
557
557
558 info_file = Unicode()
558 info_file = Unicode()
559
559
560 def _info_file_default(self):
560 def _info_file_default(self):
561 info_file = "nbserver-%s.json"%os.getpid()
561 info_file = "nbserver-%s.json"%os.getpid()
562 return os.path.join(self.profile_dir.security_dir, info_file)
562 return os.path.join(self.profile_dir.security_dir, info_file)
563
563
564 def parse_command_line(self, argv=None):
564 def parse_command_line(self, argv=None):
565 super(NotebookApp, self).parse_command_line(argv)
565 super(NotebookApp, self).parse_command_line(argv)
566
566
567 if self.extra_args:
567 if self.extra_args:
568 arg0 = self.extra_args[0]
568 arg0 = self.extra_args[0]
569 f = os.path.abspath(arg0)
569 f = os.path.abspath(arg0)
570 self.argv.remove(arg0)
570 self.argv.remove(arg0)
571 if not os.path.exists(f):
571 if not os.path.exists(f):
572 self.log.critical("No such file or directory: %s", f)
572 self.log.critical("No such file or directory: %s", f)
573 self.exit(1)
573 self.exit(1)
574 if os.path.isdir(f):
574 if os.path.isdir(f):
575 self.config.FileNotebookManager.notebook_dir = f
575 self.config.FileNotebookManager.notebook_dir = f
576 elif os.path.isfile(f):
576 elif os.path.isfile(f):
577 self.file_to_run = f
577 self.file_to_run = f
578
578
579 def init_kernel_argv(self):
579 def init_kernel_argv(self):
580 """construct the kernel arguments"""
580 """construct the kernel arguments"""
581 # Scrub frontend-specific flags
581 # Scrub frontend-specific flags
582 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
582 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
583 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
583 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
584 self.log.warn('\n '.join([
584 self.log.warn('\n '.join([
585 "Starting all kernels in pylab mode is not recommended,",
585 "Starting all kernels in pylab mode is not recommended,",
586 "and will be disabled in a future release.",
586 "and will be disabled in a future release.",
587 "Please use the %matplotlib magic to enable matplotlib instead.",
587 "Please use the %matplotlib magic to enable matplotlib instead.",
588 "pylab implies many imports, which can have confusing side effects",
588 "pylab implies many imports, which can have confusing side effects",
589 "and harm the reproducibility of your notebooks.",
589 "and harm the reproducibility of your notebooks.",
590 ]))
590 ]))
591 # Kernel should inherit default config file from frontend
591 # Kernel should inherit default config file from frontend
592 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
592 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
593 # Kernel should get *absolute* path to profile directory
593 # Kernel should get *absolute* path to profile directory
594 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
594 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
595
595
596 def init_configurables(self):
596 def init_configurables(self):
597 # force Session default to be secure
597 # force Session default to be secure
598 default_secure(self.config)
598 default_secure(self.config)
599 self.kernel_manager = MappingKernelManager(
599 self.kernel_manager = MappingKernelManager(
600 parent=self, log=self.log, kernel_argv=self.kernel_argv,
600 parent=self, log=self.log, kernel_argv=self.kernel_argv,
601 connection_dir = self.profile_dir.security_dir,
601 connection_dir = self.profile_dir.security_dir,
602 )
602 )
603 kls = import_item(self.notebook_manager_class)
603 kls = import_item(self.notebook_manager_class)
604 self.notebook_manager = kls(parent=self, log=self.log)
604 self.notebook_manager = kls(parent=self, log=self.log)
605 self.session_manager = SessionManager(parent=self, log=self.log)
605 self.session_manager = SessionManager(parent=self, log=self.log)
606 self.cluster_manager = ClusterManager(parent=self, log=self.log)
606 self.cluster_manager = ClusterManager(parent=self, log=self.log)
607 self.cluster_manager.update_profiles()
607 self.cluster_manager.update_profiles()
608
608
609 def init_logging(self):
609 def init_logging(self):
610 # This prevents double log messages because tornado use a root logger that
610 # This prevents double log messages because tornado use a root logger that
611 # self.log is a child of. The logging module dipatches log messages to a log
611 # self.log is a child of. The logging module dipatches log messages to a log
612 # and all of its ancenstors until propagate is set to False.
612 # and all of its ancenstors until propagate is set to False.
613 self.log.propagate = False
613 self.log.propagate = False
614
614
615 # hook up tornado 3's loggers to our app handlers
615 # hook up tornado 3's loggers to our app handlers
616 for name in ('access', 'application', 'general'):
616 for name in ('access', 'application', 'general'):
617 logger = logging.getLogger('tornado.%s' % name)
617 logger = logging.getLogger('tornado.%s' % name)
618 logger.parent = self.log
618 logger.parent = self.log
619 logger.setLevel(self.log.level)
619 logger.setLevel(self.log.level)
620
620
621 def init_webapp(self):
621 def init_webapp(self):
622 """initialize tornado webapp and httpserver"""
622 """initialize tornado webapp and httpserver"""
623 self.web_app = NotebookWebApplication(
623 self.web_app = NotebookWebApplication(
624 self, self.kernel_manager, self.notebook_manager,
624 self, self.kernel_manager, self.notebook_manager,
625 self.cluster_manager, self.session_manager,
625 self.cluster_manager, self.session_manager,
626 self.log, self.base_project_url, self.webapp_settings
626 self.log, self.base_project_url, self.webapp_settings
627 )
627 )
628 if self.certfile:
628 if self.certfile:
629 ssl_options = dict(certfile=self.certfile)
629 ssl_options = dict(certfile=self.certfile)
630 if self.keyfile:
630 if self.keyfile:
631 ssl_options['keyfile'] = self.keyfile
631 ssl_options['keyfile'] = self.keyfile
632 else:
632 else:
633 ssl_options = None
633 ssl_options = None
634 self.web_app.password = self.password
634 self.web_app.password = self.password
635 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
635 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
636 xheaders=self.trust_xheaders)
636 xheaders=self.trust_xheaders)
637 if not self.ip:
637 if not self.ip:
638 warning = "WARNING: The notebook server is listening on all IP addresses"
638 warning = "WARNING: The notebook server is listening on all IP addresses"
639 if ssl_options is None:
639 if ssl_options is None:
640 self.log.critical(warning + " and not using encryption. This "
640 self.log.critical(warning + " and not using encryption. This "
641 "is not recommended.")
641 "is not recommended.")
642 if not self.password:
642 if not self.password:
643 self.log.critical(warning + " and not using authentication. "
643 self.log.critical(warning + " and not using authentication. "
644 "This is highly insecure and not recommended.")
644 "This is highly insecure and not recommended.")
645 success = None
645 success = None
646 for port in random_ports(self.port, self.port_retries+1):
646 for port in random_ports(self.port, self.port_retries+1):
647 try:
647 try:
648 self.http_server.listen(port, self.ip)
648 self.http_server.listen(port, self.ip)
649 except socket.error as e:
649 except socket.error as e:
650 if e.errno == errno.EADDRINUSE:
650 if e.errno == errno.EADDRINUSE:
651 self.log.info('The port %i is already in use, trying another random port.' % port)
651 self.log.info('The port %i is already in use, trying another random port.' % port)
652 continue
652 continue
653 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
653 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
654 self.log.warn("Permission to listen on port %i denied" % port)
654 self.log.warn("Permission to listen on port %i denied" % port)
655 continue
655 continue
656 else:
656 else:
657 raise
657 raise
658 else:
658 else:
659 self.port = port
659 self.port = port
660 success = True
660 success = True
661 break
661 break
662 if not success:
662 if not success:
663 self.log.critical('ERROR: the notebook server could not be started because '
663 self.log.critical('ERROR: the notebook server could not be started because '
664 'no available port could be found.')
664 'no available port could be found.')
665 self.exit(1)
665 self.exit(1)
666
666
667 @property
667 @property
668 def display_url(self):
668 def display_url(self):
669 ip = self.ip if self.ip else '[all ip addresses on your system]'
669 ip = self.ip if self.ip else '[all ip addresses on your system]'
670 return self._url(ip)
670 return self._url(ip)
671
671
672 @property
672 @property
673 def connection_url(self):
673 def connection_url(self):
674 ip = self.ip if self.ip else localhost()
674 ip = self.ip if self.ip else localhost()
675 return self._url(ip)
675 return self._url(ip)
676
676
677 def _url(self, ip):
677 def _url(self, ip):
678 proto = 'https' if self.certfile else 'http'
678 proto = 'https' if self.certfile else 'http'
679 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
679 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
680
680
681 def init_signal(self):
681 def init_signal(self):
682 if not sys.platform.startswith('win'):
682 if not sys.platform.startswith('win'):
683 signal.signal(signal.SIGINT, self._handle_sigint)
683 signal.signal(signal.SIGINT, self._handle_sigint)
684 signal.signal(signal.SIGTERM, self._signal_stop)
684 signal.signal(signal.SIGTERM, self._signal_stop)
685 if hasattr(signal, 'SIGUSR1'):
685 if hasattr(signal, 'SIGUSR1'):
686 # Windows doesn't support SIGUSR1
686 # Windows doesn't support SIGUSR1
687 signal.signal(signal.SIGUSR1, self._signal_info)
687 signal.signal(signal.SIGUSR1, self._signal_info)
688 if hasattr(signal, 'SIGINFO'):
688 if hasattr(signal, 'SIGINFO'):
689 # only on BSD-based systems
689 # only on BSD-based systems
690 signal.signal(signal.SIGINFO, self._signal_info)
690 signal.signal(signal.SIGINFO, self._signal_info)
691
691
692 def _handle_sigint(self, sig, frame):
692 def _handle_sigint(self, sig, frame):
693 """SIGINT handler spawns confirmation dialog"""
693 """SIGINT handler spawns confirmation dialog"""
694 # register more forceful signal handler for ^C^C case
694 # register more forceful signal handler for ^C^C case
695 signal.signal(signal.SIGINT, self._signal_stop)
695 signal.signal(signal.SIGINT, self._signal_stop)
696 # request confirmation dialog in bg thread, to avoid
696 # request confirmation dialog in bg thread, to avoid
697 # blocking the App
697 # blocking the App
698 thread = threading.Thread(target=self._confirm_exit)
698 thread = threading.Thread(target=self._confirm_exit)
699 thread.daemon = True
699 thread.daemon = True
700 thread.start()
700 thread.start()
701
701
702 def _restore_sigint_handler(self):
702 def _restore_sigint_handler(self):
703 """callback for restoring original SIGINT handler"""
703 """callback for restoring original SIGINT handler"""
704 signal.signal(signal.SIGINT, self._handle_sigint)
704 signal.signal(signal.SIGINT, self._handle_sigint)
705
705
706 def _confirm_exit(self):
706 def _confirm_exit(self):
707 """confirm shutdown on ^C
707 """confirm shutdown on ^C
708
708
709 A second ^C, or answering 'y' within 5s will cause shutdown,
709 A second ^C, or answering 'y' within 5s will cause shutdown,
710 otherwise original SIGINT handler will be restored.
710 otherwise original SIGINT handler will be restored.
711
711
712 This doesn't work on Windows.
712 This doesn't work on Windows.
713 """
713 """
714 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
714 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
715 time.sleep(0.1)
715 time.sleep(0.1)
716 info = self.log.info
716 info = self.log.info
717 info('interrupted')
717 info('interrupted')
718 print(self.notebook_info())
718 print(self.notebook_info())
719 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
719 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
720 sys.stdout.flush()
720 sys.stdout.flush()
721 r,w,x = select.select([sys.stdin], [], [], 5)
721 r,w,x = select.select([sys.stdin], [], [], 5)
722 if r:
722 if r:
723 line = sys.stdin.readline()
723 line = sys.stdin.readline()
724 if line.lower().startswith('y'):
724 if line.lower().startswith('y'):
725 self.log.critical("Shutdown confirmed")
725 self.log.critical("Shutdown confirmed")
726 ioloop.IOLoop.instance().stop()
726 ioloop.IOLoop.instance().stop()
727 return
727 return
728 else:
728 else:
729 print("No answer for 5s:", end=' ')
729 print("No answer for 5s:", end=' ')
730 print("resuming operation...")
730 print("resuming operation...")
731 # no answer, or answer is no:
731 # no answer, or answer is no:
732 # set it back to original SIGINT handler
732 # set it back to original SIGINT handler
733 # use IOLoop.add_callback because signal.signal must be called
733 # use IOLoop.add_callback because signal.signal must be called
734 # from main thread
734 # from main thread
735 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
735 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
736
736
737 def _signal_stop(self, sig, frame):
737 def _signal_stop(self, sig, frame):
738 self.log.critical("received signal %s, stopping", sig)
738 self.log.critical("received signal %s, stopping", sig)
739 ioloop.IOLoop.instance().stop()
739 ioloop.IOLoop.instance().stop()
740
740
741 def _signal_info(self, sig, frame):
741 def _signal_info(self, sig, frame):
742 print(self.notebook_info())
742 print(self.notebook_info())
743
743
744 def init_components(self):
744 def init_components(self):
745 """Check the components submodule, and warn if it's unclean"""
745 """Check the components submodule, and warn if it's unclean"""
746 status = submodule.check_submodule_status()
746 status = submodule.check_submodule_status()
747 if status == 'missing':
747 if status == 'missing':
748 self.log.warn("components submodule missing, running `git submodule update`")
748 self.log.warn("components submodule missing, running `git submodule update`")
749 submodule.update_submodules(submodule.ipython_parent())
749 submodule.update_submodules(submodule.ipython_parent())
750 elif status == 'unclean':
750 elif status == 'unclean':
751 self.log.warn("components submodule unclean, you may see 404s on static/components")
751 self.log.warn("components submodule unclean, you may see 404s on static/components")
752 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
752 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
753
753
754 @catch_config_error
754 @catch_config_error
755 def initialize(self, argv=None):
755 def initialize(self, argv=None):
756 super(NotebookApp, self).initialize(argv)
756 super(NotebookApp, self).initialize(argv)
757 self.init_logging()
757 self.init_logging()
758 self.init_kernel_argv()
758 self.init_kernel_argv()
759 self.init_configurables()
759 self.init_configurables()
760 self.init_components()
760 self.init_components()
761 self.init_webapp()
761 self.init_webapp()
762 self.init_signal()
762 self.init_signal()
763
763
764 def cleanup_kernels(self):
764 def cleanup_kernels(self):
765 """Shutdown all kernels.
765 """Shutdown all kernels.
766
766
767 The kernels will shutdown themselves when this process no longer exists,
767 The kernels will shutdown themselves when this process no longer exists,
768 but explicit shutdown allows the KernelManagers to cleanup the connection files.
768 but explicit shutdown allows the KernelManagers to cleanup the connection files.
769 """
769 """
770 self.log.info('Shutting down kernels')
770 self.log.info('Shutting down kernels')
771 self.kernel_manager.shutdown_all()
771 self.kernel_manager.shutdown_all()
772
772
773 def notebook_info(self):
773 def notebook_info(self):
774 "Return the current working directory and the server url information"
774 "Return the current working directory and the server url information"
775 info = self.notebook_manager.info_string() + "\n"
775 info = self.notebook_manager.info_string() + "\n"
776 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
776 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
777 return info + "The IPython Notebook is running at: %s" % self.display_url
777 return info + "The IPython Notebook is running at: %s" % self.display_url
778
778
779 def server_info(self):
779 def server_info(self):
780 """Return a JSONable dict of information about this server."""
780 """Return a JSONable dict of information about this server."""
781 return {'url': self.connection_url,
781 return {'url': self.connection_url,
782 'hostname': self.ip if self.ip else 'localhost',
782 'hostname': self.ip if self.ip else 'localhost',
783 'port': self.port,
783 'port': self.port,
784 'secure': bool(self.certfile),
784 'secure': bool(self.certfile),
785 'base_project_url': self.base_project_url,
785 'base_project_url': self.base_project_url,
786 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
786 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
787 }
787 }
788
788
789 def write_server_info_file(self):
789 def write_server_info_file(self):
790 """Write the result of server_info() to the JSON file info_file."""
790 """Write the result of server_info() to the JSON file info_file."""
791 with open(self.info_file, 'w') as f:
791 with open(self.info_file, 'w') as f:
792 json.dump(self.server_info(), f, indent=2)
792 json.dump(self.server_info(), f, indent=2)
793
793
794 def remove_server_info_file(self):
794 def remove_server_info_file(self):
795 """Remove the nbserver-<pid>.json file created for this server.
795 """Remove the nbserver-<pid>.json file created for this server.
796
796
797 Ignores the error raised when the file has already been removed.
797 Ignores the error raised when the file has already been removed.
798 """
798 """
799 try:
799 try:
800 os.unlink(self.info_file)
800 os.unlink(self.info_file)
801 except OSError as e:
801 except OSError as e:
802 if e.errno != errno.ENOENT:
802 if e.errno != errno.ENOENT:
803 raise
803 raise
804
804
805 def start(self):
805 def start(self):
806 """ Start the IPython Notebook server app, after initialization
806 """ Start the IPython Notebook server app, after initialization
807
807
808 This method takes no arguments so all configuration and initialization
808 This method takes no arguments so all configuration and initialization
809 must be done prior to calling this method."""
809 must be done prior to calling this method."""
810 if self.subapp is not None:
810 if self.subapp is not None:
811 return self.subapp.start()
811 return self.subapp.start()
812
812
813 info = self.log.info
813 info = self.log.info
814 for line in self.notebook_info().split("\n"):
814 for line in self.notebook_info().split("\n"):
815 info(line)
815 info(line)
816 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
816 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
817
817
818 self.write_server_info_file()
818 self.write_server_info_file()
819
819
820 if self.open_browser or self.file_to_run:
820 if self.open_browser or self.file_to_run:
821 try:
821 try:
822 browser = webbrowser.get(self.browser or None)
822 browser = webbrowser.get(self.browser or None)
823 except webbrowser.Error as e:
823 except webbrowser.Error as e:
824 self.log.warn('No web browser found: %s.' % e)
824 self.log.warn('No web browser found: %s.' % e)
825 browser = None
825 browser = None
826
826
827 f = self.file_to_run
827 f = self.file_to_run
828 if f:
828 if f:
829 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
829 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
830 if f.startswith(nbdir):
830 if f.startswith(nbdir):
831 f = f[len(nbdir):]
831 f = f[len(nbdir):]
832 else:
832 else:
833 self.log.warn(
833 self.log.warn(
834 "Probably won't be able to open notebook %s "
834 "Probably won't be able to open notebook %s "
835 "because it is not in notebook_dir %s",
835 "because it is not in notebook_dir %s",
836 f, nbdir,
836 f, nbdir,
837 )
837 )
838
838
839 if os.path.isfile(self.file_to_run):
839 if os.path.isfile(self.file_to_run):
840 url = url_path_join('notebooks', f)
840 url = url_path_join('notebooks', f)
841 else:
841 else:
842 url = url_path_join('tree', f)
842 url = url_path_join('tree', f)
843 if browser:
843 if browser:
844 b = lambda : browser.open("%s%s" % (self.connection_url, url),
844 b = lambda : browser.open("%s%s" % (self.connection_url, url),
845 new=2)
845 new=2)
846 threading.Thread(target=b).start()
846 threading.Thread(target=b).start()
847 try:
847 try:
848 ioloop.IOLoop.instance().start()
848 ioloop.IOLoop.instance().start()
849 except KeyboardInterrupt:
849 except KeyboardInterrupt:
850 info("Interrupted...")
850 info("Interrupted...")
851 finally:
851 finally:
852 self.cleanup_kernels()
852 self.cleanup_kernels()
853 self.remove_server_info_file()
853 self.remove_server_info_file()
854
854
855
855
856 def list_running_servers(profile='default'):
856 def list_running_servers(profile='default'):
857 """Iterate over the server info files of running notebook servers.
857 """Iterate over the server info files of running notebook servers.
858
858
859 Given a profile name, find nbserver-* files in the security directory of
859 Given a profile name, find nbserver-* files in the security directory of
860 that profile, and yield dicts of their information, each one pertaining to
860 that profile, and yield dicts of their information, each one pertaining to
861 a currently running notebook server instance.
861 a currently running notebook server instance.
862 """
862 """
863 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
863 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
864 for file in os.listdir(pd.security_dir):
864 for file in os.listdir(pd.security_dir):
865 if file.startswith('nbserver-'):
865 if file.startswith('nbserver-'):
866 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
866 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
867 yield json.load(f)
867 yield json.load(f)
868
868
869 #-----------------------------------------------------------------------------
869 #-----------------------------------------------------------------------------
870 # Main entry point
870 # Main entry point
871 #-----------------------------------------------------------------------------
871 #-----------------------------------------------------------------------------
872
872
873 launch_new_instance = NotebookApp.launch_instance
873 launch_new_instance = NotebookApp.launch_instance
874
874
@@ -1,419 +1,425 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import io
20 import io
21 import itertools
21 import itertools
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from tornado import web
26 from tornado import web
27
27
28 from .nbmanager import NotebookManager
28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current, sign
29 from IPython.nbformat import current, sign
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
31 from IPython.utils import tz
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes
34 # Classes
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class FileNotebookManager(NotebookManager):
37 class FileNotebookManager(NotebookManager):
38
38
39 save_script = Bool(False, config=True,
39 save_script = Bool(False, config=True,
40 help="""Automatically create a Python script when saving the notebook.
40 help="""Automatically create a Python script when saving the notebook.
41
41
42 For easier use of import, %run and %load across notebooks, a
42 For easier use of import, %run and %load across notebooks, a
43 <notebook-name>.py script will be created next to any
43 <notebook-name>.py script will be created next to any
44 <notebook-name>.ipynb on each save. This can also be set with the
44 <notebook-name>.ipynb on each save. This can also be set with the
45 short `--script` flag.
45 short `--script` flag.
46 """
46 """
47 )
47 )
48
48
49 checkpoint_dir = Unicode(config=True,
49 checkpoint_dir = Unicode(config=True,
50 help="""The location in which to keep notebook checkpoints
50 help="""The location in which to keep notebook checkpoints
51
51
52 By default, it is notebook-dir/.ipynb_checkpoints
52 By default, it is notebook-dir/.ipynb_checkpoints
53 """
53 """
54 )
54 )
55 def _checkpoint_dir_default(self):
55 def _checkpoint_dir_default(self):
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57
57
58 def _checkpoint_dir_changed(self, name, old, new):
58 def _checkpoint_dir_changed(self, name, old, new):
59 """do a bit of validation of the checkpoint dir"""
59 """do a bit of validation of the checkpoint dir"""
60 if not os.path.isabs(new):
60 if not os.path.isabs(new):
61 # If we receive a non-absolute path, make it absolute.
61 # If we receive a non-absolute path, make it absolute.
62 abs_new = os.path.abspath(new)
62 abs_new = os.path.abspath(new)
63 self.checkpoint_dir = abs_new
63 self.checkpoint_dir = abs_new
64 return
64 return
65 if os.path.exists(new) and not os.path.isdir(new):
65 if os.path.exists(new) and not os.path.isdir(new):
66 raise TraitError("checkpoint dir %r is not a directory" % new)
66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 if not os.path.exists(new):
67 if not os.path.exists(new):
68 self.log.info("Creating checkpoint dir %s", new)
68 self.log.info("Creating checkpoint dir %s", new)
69 try:
69 try:
70 os.mkdir(new)
70 os.mkdir(new)
71 except:
71 except:
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73
73
74 def get_notebook_names(self, path=''):
74 def get_notebook_names(self, path=''):
75 """List all notebook names in the notebook dir and path."""
75 """List all notebook names in the notebook dir and path."""
76 path = path.strip('/')
76 path = path.strip('/')
77 if not os.path.isdir(self.get_os_path(path=path)):
77 if not os.path.isdir(self.get_os_path(path=path)):
78 raise web.HTTPError(404, 'Directory not found: ' + path)
78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = [os.path.basename(name)
80 names = [os.path.basename(name)
81 for name in names]
81 for name in names]
82 return names
82 return names
83
83
84 def increment_filename(self, basename, path='', ext='.ipynb'):
84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 """Return a non-used filename of the form basename<int>."""
85 """Return a non-used filename of the form basename<int>."""
86 path = path.strip('/')
86 path = path.strip('/')
87 for i in itertools.count():
87 for i in itertools.count():
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 os_path = self.get_os_path(name, path)
89 os_path = self.get_os_path(name, path)
90 if not os.path.isfile(os_path):
90 if not os.path.isfile(os_path):
91 break
91 break
92 return name
92 return name
93
93
94 def path_exists(self, path):
94 def path_exists(self, path):
95 """Does the API-style path (directory) actually exist?
95 """Does the API-style path (directory) actually exist?
96
96
97 Parameters
97 Parameters
98 ----------
98 ----------
99 path : string
99 path : string
100 The path to check. This is an API path (`/` separated,
100 The path to check. This is an API path (`/` separated,
101 relative to base notebook-dir).
101 relative to base notebook-dir).
102
102
103 Returns
103 Returns
104 -------
104 -------
105 exists : bool
105 exists : bool
106 Whether the path is indeed a directory.
106 Whether the path is indeed a directory.
107 """
107 """
108 path = path.strip('/')
108 path = path.strip('/')
109 os_path = self.get_os_path(path=path)
109 os_path = self.get_os_path(path=path)
110 return os.path.isdir(os_path)
110 return os.path.isdir(os_path)
111
111
112 def get_os_path(self, name=None, path=''):
112 def get_os_path(self, name=None, path=''):
113 """Given a notebook name and a URL path, return its file system
113 """Given a notebook name and a URL path, return its file system
114 path.
114 path.
115
115
116 Parameters
116 Parameters
117 ----------
117 ----------
118 name : string
118 name : string
119 The name of a notebook file with the .ipynb extension
119 The name of a notebook file with the .ipynb extension
120 path : string
120 path : string
121 The relative URL path (with '/' as separator) to the named
121 The relative URL path (with '/' as separator) to the named
122 notebook.
122 notebook.
123
123
124 Returns
124 Returns
125 -------
125 -------
126 path : string
126 path : string
127 A file system path that combines notebook_dir (location where
127 A file system path that combines notebook_dir (location where
128 server started), the relative path, and the filename with the
128 server started), the relative path, and the filename with the
129 current operating system's url.
129 current operating system's url.
130 """
130 """
131 parts = path.strip('/').split('/')
131 parts = path.strip('/').split('/')
132 parts = [p for p in parts if p != ''] # remove duplicate splits
132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 if name is not None:
133 if name is not None:
134 parts.append(name)
134 parts.append(name)
135 path = os.path.join(self.notebook_dir, *parts)
135 path = os.path.join(self.notebook_dir, *parts)
136 return path
136 return path
137
137
138 def notebook_exists(self, name, path=''):
138 def notebook_exists(self, name, path=''):
139 """Returns a True if the notebook exists. Else, returns False.
139 """Returns a True if the notebook exists. Else, returns False.
140
140
141 Parameters
141 Parameters
142 ----------
142 ----------
143 name : string
143 name : string
144 The name of the notebook you are checking.
144 The name of the notebook you are checking.
145 path : string
145 path : string
146 The relative path to the notebook (with '/' as separator)
146 The relative path to the notebook (with '/' as separator)
147
147
148 Returns
148 Returns
149 -------
149 -------
150 bool
150 bool
151 """
151 """
152 path = path.strip('/')
152 path = path.strip('/')
153 nbpath = self.get_os_path(name, path=path)
153 nbpath = self.get_os_path(name, path=path)
154 return os.path.isfile(nbpath)
154 return os.path.isfile(nbpath)
155
155
156 def list_notebooks(self, path):
156 def list_notebooks(self, path):
157 """Returns a list of dictionaries that are the standard model
157 """Returns a list of dictionaries that are the standard model
158 for all notebooks in the relative 'path'.
158 for all notebooks in the relative 'path'.
159
159
160 Parameters
160 Parameters
161 ----------
161 ----------
162 path : str
162 path : str
163 the URL path that describes the relative path for the
163 the URL path that describes the relative path for the
164 listed notebooks
164 listed notebooks
165
165
166 Returns
166 Returns
167 -------
167 -------
168 notebooks : list of dicts
168 notebooks : list of dicts
169 a list of the notebook models without 'content'
169 a list of the notebook models without 'content'
170 """
170 """
171 path = path.strip('/')
171 path = path.strip('/')
172 notebook_names = self.get_notebook_names(path)
172 notebook_names = self.get_notebook_names(path)
173 notebooks = []
173 notebooks = []
174 for name in notebook_names:
174 for name in notebook_names:
175 model = self.get_notebook_model(name, path, content=False)
175 model = self.get_notebook_model(name, path, content=False)
176 notebooks.append(model)
176 notebooks.append(model)
177 notebooks = sorted(notebooks, key=lambda item: item['name'])
177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 return notebooks
178 return notebooks
179
179
180 def get_notebook_model(self, name, path='', content=True):
180 def get_notebook_model(self, name, path='', content=True):
181 """ Takes a path and name for a notebook and returns its model
181 """ Takes a path and name for a notebook and returns its model
182
182
183 Parameters
183 Parameters
184 ----------
184 ----------
185 name : str
185 name : str
186 the name of the notebook
186 the name of the notebook
187 path : str
187 path : str
188 the URL path that describes the relative path for
188 the URL path that describes the relative path for
189 the notebook
189 the notebook
190
190
191 Returns
191 Returns
192 -------
192 -------
193 model : dict
193 model : dict
194 the notebook model. If contents=True, returns the 'contents'
194 the notebook model. If contents=True, returns the 'contents'
195 dict in the model as well.
195 dict in the model as well.
196 """
196 """
197 path = path.strip('/')
197 path = path.strip('/')
198 if not self.notebook_exists(name=name, path=path):
198 if not self.notebook_exists(name=name, path=path):
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 os_path = self.get_os_path(name, path)
200 os_path = self.get_os_path(name, path)
201 info = os.stat(os_path)
201 info = os.stat(os_path)
202 last_modified = tz.utcfromtimestamp(info.st_mtime)
202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 created = tz.utcfromtimestamp(info.st_ctime)
203 created = tz.utcfromtimestamp(info.st_ctime)
204 # Create the notebook model.
204 # Create the notebook model.
205 model ={}
205 model ={}
206 model['name'] = name
206 model['name'] = name
207 model['path'] = path
207 model['path'] = path
208 model['last_modified'] = last_modified
208 model['last_modified'] = last_modified
209 model['created'] = created
209 model['created'] = created
210 if content:
210 if content:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 try:
212 try:
213 nb = current.read(f, u'json')
213 nb = current.read(f, u'json')
214 except Exception as e:
214 except Exception as e:
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 model['content'] = nb
216 model['content'] = nb
217 sign.mark_trusted_cells(nb, self.notary.secret, self.notary.scheme)
217 trusted = self.notary.check_signature(nb)
218 if not trusted:
219 self.log.warn("Notebook %s/%s is not trusted", model['path'], model['name'])
220 self.notary.mark_cells(nb, trusted)
218 return model
221 return model
219
222
220 def save_notebook_model(self, model, name='', path=''):
223 def save_notebook_model(self, model, name='', path=''):
221 """Save the notebook model and return the model with no content."""
224 """Save the notebook model and return the model with no content."""
222 path = path.strip('/')
225 path = path.strip('/')
223
226
224 if 'content' not in model:
227 if 'content' not in model:
225 raise web.HTTPError(400, u'No notebook JSON data provided')
228 raise web.HTTPError(400, u'No notebook JSON data provided')
226
229
227 # One checkpoint should always exist
230 # One checkpoint should always exist
228 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
231 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
229 self.create_checkpoint(name, path)
232 self.create_checkpoint(name, path)
230
233
231 new_path = model.get('path', path).strip('/')
234 new_path = model.get('path', path).strip('/')
232 new_name = model.get('name', name)
235 new_name = model.get('name', name)
233
236
234 if path != new_path or name != new_name:
237 if path != new_path or name != new_name:
235 self.rename_notebook(name, path, new_name, new_path)
238 self.rename_notebook(name, path, new_name, new_path)
236
239
237 # Save the notebook file
240 # Save the notebook file
238 os_path = self.get_os_path(new_name, new_path)
241 os_path = self.get_os_path(new_name, new_path)
239 nb = current.to_notebook_json(model['content'])
242 nb = current.to_notebook_json(model['content'])
240
243
241 if sign.check_trusted_cells(nb):
244 if self.notary.check_cells(nb):
242 sign.trust_notebook(nb, self.notary.secret, self.notary.scheme)
245 self.notary.sign(nb)
246 else:
247 self.log.warn("Saving untrusted notebook %s/%s", new_path, new_name)
248
243
249
244 if 'name' in nb['metadata']:
250 if 'name' in nb['metadata']:
245 nb['metadata']['name'] = u''
251 nb['metadata']['name'] = u''
246 try:
252 try:
247 self.log.debug("Autosaving notebook %s", os_path)
253 self.log.debug("Autosaving notebook %s", os_path)
248 with io.open(os_path, 'w', encoding='utf-8') as f:
254 with io.open(os_path, 'w', encoding='utf-8') as f:
249 current.write(nb, f, u'json')
255 current.write(nb, f, u'json')
250 except Exception as e:
256 except Exception as e:
251 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
257 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
252
258
253 # Save .py script as well
259 # Save .py script as well
254 if self.save_script:
260 if self.save_script:
255 py_path = os.path.splitext(os_path)[0] + '.py'
261 py_path = os.path.splitext(os_path)[0] + '.py'
256 self.log.debug("Writing script %s", py_path)
262 self.log.debug("Writing script %s", py_path)
257 try:
263 try:
258 with io.open(py_path, 'w', encoding='utf-8') as f:
264 with io.open(py_path, 'w', encoding='utf-8') as f:
259 current.write(nb, f, u'py')
265 current.write(nb, f, u'py')
260 except Exception as e:
266 except Exception as e:
261 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
267 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
262
268
263 model = self.get_notebook_model(new_name, new_path, content=False)
269 model = self.get_notebook_model(new_name, new_path, content=False)
264 return model
270 return model
265
271
266 def update_notebook_model(self, model, name, path=''):
272 def update_notebook_model(self, model, name, path=''):
267 """Update the notebook's path and/or name"""
273 """Update the notebook's path and/or name"""
268 path = path.strip('/')
274 path = path.strip('/')
269 new_name = model.get('name', name)
275 new_name = model.get('name', name)
270 new_path = model.get('path', path).strip('/')
276 new_path = model.get('path', path).strip('/')
271 if path != new_path or name != new_name:
277 if path != new_path or name != new_name:
272 self.rename_notebook(name, path, new_name, new_path)
278 self.rename_notebook(name, path, new_name, new_path)
273 model = self.get_notebook_model(new_name, new_path, content=False)
279 model = self.get_notebook_model(new_name, new_path, content=False)
274 return model
280 return model
275
281
276 def delete_notebook_model(self, name, path=''):
282 def delete_notebook_model(self, name, path=''):
277 """Delete notebook by name and path."""
283 """Delete notebook by name and path."""
278 path = path.strip('/')
284 path = path.strip('/')
279 os_path = self.get_os_path(name, path)
285 os_path = self.get_os_path(name, path)
280 if not os.path.isfile(os_path):
286 if not os.path.isfile(os_path):
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
287 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
282
288
283 # clear checkpoints
289 # clear checkpoints
284 for checkpoint in self.list_checkpoints(name, path):
290 for checkpoint in self.list_checkpoints(name, path):
285 checkpoint_id = checkpoint['id']
291 checkpoint_id = checkpoint['id']
286 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
292 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
287 if os.path.isfile(cp_path):
293 if os.path.isfile(cp_path):
288 self.log.debug("Unlinking checkpoint %s", cp_path)
294 self.log.debug("Unlinking checkpoint %s", cp_path)
289 os.unlink(cp_path)
295 os.unlink(cp_path)
290
296
291 self.log.debug("Unlinking notebook %s", os_path)
297 self.log.debug("Unlinking notebook %s", os_path)
292 os.unlink(os_path)
298 os.unlink(os_path)
293
299
294 def rename_notebook(self, old_name, old_path, new_name, new_path):
300 def rename_notebook(self, old_name, old_path, new_name, new_path):
295 """Rename a notebook."""
301 """Rename a notebook."""
296 old_path = old_path.strip('/')
302 old_path = old_path.strip('/')
297 new_path = new_path.strip('/')
303 new_path = new_path.strip('/')
298 if new_name == old_name and new_path == old_path:
304 if new_name == old_name and new_path == old_path:
299 return
305 return
300
306
301 new_os_path = self.get_os_path(new_name, new_path)
307 new_os_path = self.get_os_path(new_name, new_path)
302 old_os_path = self.get_os_path(old_name, old_path)
308 old_os_path = self.get_os_path(old_name, old_path)
303
309
304 # Should we proceed with the move?
310 # Should we proceed with the move?
305 if os.path.isfile(new_os_path):
311 if os.path.isfile(new_os_path):
306 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
312 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
307 if self.save_script:
313 if self.save_script:
308 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
314 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
309 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
315 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
310 if os.path.isfile(new_py_path):
316 if os.path.isfile(new_py_path):
311 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
317 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
312
318
313 # Move the notebook file
319 # Move the notebook file
314 try:
320 try:
315 os.rename(old_os_path, new_os_path)
321 os.rename(old_os_path, new_os_path)
316 except Exception as e:
322 except Exception as e:
317 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
323 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
318
324
319 # Move the checkpoints
325 # Move the checkpoints
320 old_checkpoints = self.list_checkpoints(old_name, old_path)
326 old_checkpoints = self.list_checkpoints(old_name, old_path)
321 for cp in old_checkpoints:
327 for cp in old_checkpoints:
322 checkpoint_id = cp['id']
328 checkpoint_id = cp['id']
323 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
329 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
324 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
330 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
325 if os.path.isfile(old_cp_path):
331 if os.path.isfile(old_cp_path):
326 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
332 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
327 os.rename(old_cp_path, new_cp_path)
333 os.rename(old_cp_path, new_cp_path)
328
334
329 # Move the .py script
335 # Move the .py script
330 if self.save_script:
336 if self.save_script:
331 os.rename(old_py_path, new_py_path)
337 os.rename(old_py_path, new_py_path)
332
338
333 # Checkpoint-related utilities
339 # Checkpoint-related utilities
334
340
335 def get_checkpoint_path(self, checkpoint_id, name, path=''):
341 def get_checkpoint_path(self, checkpoint_id, name, path=''):
336 """find the path to a checkpoint"""
342 """find the path to a checkpoint"""
337 path = path.strip('/')
343 path = path.strip('/')
338 basename, _ = os.path.splitext(name)
344 basename, _ = os.path.splitext(name)
339 filename = u"{name}-{checkpoint_id}{ext}".format(
345 filename = u"{name}-{checkpoint_id}{ext}".format(
340 name=basename,
346 name=basename,
341 checkpoint_id=checkpoint_id,
347 checkpoint_id=checkpoint_id,
342 ext=self.filename_ext,
348 ext=self.filename_ext,
343 )
349 )
344 cp_path = os.path.join(path, self.checkpoint_dir, filename)
350 cp_path = os.path.join(path, self.checkpoint_dir, filename)
345 return cp_path
351 return cp_path
346
352
347 def get_checkpoint_model(self, checkpoint_id, name, path=''):
353 def get_checkpoint_model(self, checkpoint_id, name, path=''):
348 """construct the info dict for a given checkpoint"""
354 """construct the info dict for a given checkpoint"""
349 path = path.strip('/')
355 path = path.strip('/')
350 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
356 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
351 stats = os.stat(cp_path)
357 stats = os.stat(cp_path)
352 last_modified = tz.utcfromtimestamp(stats.st_mtime)
358 last_modified = tz.utcfromtimestamp(stats.st_mtime)
353 info = dict(
359 info = dict(
354 id = checkpoint_id,
360 id = checkpoint_id,
355 last_modified = last_modified,
361 last_modified = last_modified,
356 )
362 )
357 return info
363 return info
358
364
359 # public checkpoint API
365 # public checkpoint API
360
366
361 def create_checkpoint(self, name, path=''):
367 def create_checkpoint(self, name, path=''):
362 """Create a checkpoint from the current state of a notebook"""
368 """Create a checkpoint from the current state of a notebook"""
363 path = path.strip('/')
369 path = path.strip('/')
364 nb_path = self.get_os_path(name, path)
370 nb_path = self.get_os_path(name, path)
365 # only the one checkpoint ID:
371 # only the one checkpoint ID:
366 checkpoint_id = u"checkpoint"
372 checkpoint_id = u"checkpoint"
367 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
373 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
368 self.log.debug("creating checkpoint for notebook %s", name)
374 self.log.debug("creating checkpoint for notebook %s", name)
369 if not os.path.exists(self.checkpoint_dir):
375 if not os.path.exists(self.checkpoint_dir):
370 os.mkdir(self.checkpoint_dir)
376 os.mkdir(self.checkpoint_dir)
371 shutil.copy2(nb_path, cp_path)
377 shutil.copy2(nb_path, cp_path)
372
378
373 # return the checkpoint info
379 # return the checkpoint info
374 return self.get_checkpoint_model(checkpoint_id, name, path)
380 return self.get_checkpoint_model(checkpoint_id, name, path)
375
381
376 def list_checkpoints(self, name, path=''):
382 def list_checkpoints(self, name, path=''):
377 """list the checkpoints for a given notebook
383 """list the checkpoints for a given notebook
378
384
379 This notebook manager currently only supports one checkpoint per notebook.
385 This notebook manager currently only supports one checkpoint per notebook.
380 """
386 """
381 path = path.strip('/')
387 path = path.strip('/')
382 checkpoint_id = "checkpoint"
388 checkpoint_id = "checkpoint"
383 path = self.get_checkpoint_path(checkpoint_id, name, path)
389 path = self.get_checkpoint_path(checkpoint_id, name, path)
384 if not os.path.exists(path):
390 if not os.path.exists(path):
385 return []
391 return []
386 else:
392 else:
387 return [self.get_checkpoint_model(checkpoint_id, name, path)]
393 return [self.get_checkpoint_model(checkpoint_id, name, path)]
388
394
389
395
390 def restore_checkpoint(self, checkpoint_id, name, path=''):
396 def restore_checkpoint(self, checkpoint_id, name, path=''):
391 """restore a notebook to a checkpointed state"""
397 """restore a notebook to a checkpointed state"""
392 path = path.strip('/')
398 path = path.strip('/')
393 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
399 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
394 nb_path = self.get_os_path(name, path)
400 nb_path = self.get_os_path(name, path)
395 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
401 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
396 if not os.path.isfile(cp_path):
402 if not os.path.isfile(cp_path):
397 self.log.debug("checkpoint file does not exist: %s", cp_path)
403 self.log.debug("checkpoint file does not exist: %s", cp_path)
398 raise web.HTTPError(404,
404 raise web.HTTPError(404,
399 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
405 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
400 )
406 )
401 # ensure notebook is readable (never restore from an unreadable notebook)
407 # ensure notebook is readable (never restore from an unreadable notebook)
402 with io.open(cp_path, 'r', encoding='utf-8') as f:
408 with io.open(cp_path, 'r', encoding='utf-8') as f:
403 nb = current.read(f, u'json')
409 nb = current.read(f, u'json')
404 shutil.copy2(cp_path, nb_path)
410 shutil.copy2(cp_path, nb_path)
405 self.log.debug("copying %s -> %s", cp_path, nb_path)
411 self.log.debug("copying %s -> %s", cp_path, nb_path)
406
412
407 def delete_checkpoint(self, checkpoint_id, name, path=''):
413 def delete_checkpoint(self, checkpoint_id, name, path=''):
408 """delete a notebook's checkpoint"""
414 """delete a notebook's checkpoint"""
409 path = path.strip('/')
415 path = path.strip('/')
410 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
411 if not os.path.isfile(cp_path):
417 if not os.path.isfile(cp_path):
412 raise web.HTTPError(404,
418 raise web.HTTPError(404,
413 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
419 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
414 )
420 )
415 self.log.debug("unlinking %s", cp_path)
421 self.log.debug("unlinking %s", cp_path)
416 os.unlink(cp_path)
422 os.unlink(cp_path)
417
423
418 def info_string(self):
424 def info_string(self):
419 return "Serving notebooks from local directory: %s" % self.notebook_dir
425 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,199 +1,203 b''
1 """Functions for signing notebooks"""
1 """Functions for signing notebooks"""
2 #-----------------------------------------------------------------------------
2 #-----------------------------------------------------------------------------
3 # Copyright (C) 2014, The IPython Development Team
3 # Copyright (C) 2014, The IPython Development Team
4 #
4 #
5 # Distributed under the terms of the BSD License. The full license is in
5 # Distributed under the terms of the BSD License. The full license is in
6 # the file COPYING, distributed as part of this software.
6 # the file COPYING, distributed as part of this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Imports
10 # Imports
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 import base64
13 import base64
14 from contextlib import contextmanager
14 from contextlib import contextmanager
15 import hashlib
15 import hashlib
16 from hmac import HMAC
16 from hmac import HMAC
17 import io
17 import io
18 import os
18 import os
19
19
20 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
20 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
21 from IPython.config import LoggingConfigurable
21 from IPython.config import LoggingConfigurable
22 from IPython.utils.traitlets import Instance, Bytes, Enum
22 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Code
25 # Code
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28
28
29 def yield_everything(obj):
29 def yield_everything(obj):
30 """Yield every item in a container as bytes
30 """Yield every item in a container as bytes
31
31
32 Allows any JSONable object to be passed to an HMAC digester
32 Allows any JSONable object to be passed to an HMAC digester
33 without having to serialize the whole thing.
33 without having to serialize the whole thing.
34 """
34 """
35 if isinstance(obj, dict):
35 if isinstance(obj, dict):
36 for key in sorted(obj):
36 for key in sorted(obj):
37 value = obj[key]
37 value = obj[key]
38 yield cast_bytes(key)
38 yield cast_bytes(key)
39 for b in yield_everything(value):
39 for b in yield_everything(value):
40 yield b
40 yield b
41 elif isinstance(obj, (list, tuple)):
41 elif isinstance(obj, (list, tuple)):
42 for element in obj:
42 for element in obj:
43 for b in yield_everything(element):
43 for b in yield_everything(element):
44 yield b
44 yield b
45 elif isinstance(obj, unicode_type):
45 elif isinstance(obj, unicode_type):
46 yield obj.encode('utf8')
46 yield obj.encode('utf8')
47 else:
47 else:
48 yield unicode_type(obj).encode('utf8')
48 yield unicode_type(obj).encode('utf8')
49
49
50
50
51 @contextmanager
51 @contextmanager
52 def signature_removed(nb):
52 def signature_removed(nb):
53 """Context manager for operating on a notebook with its signature removed
53 """Context manager for operating on a notebook with its signature removed
54
54
55 Used for excluding the previous signature when computing a notebook's signature.
55 Used for excluding the previous signature when computing a notebook's signature.
56 """
56 """
57 save_signature = nb['metadata'].pop('signature', None)
57 save_signature = nb['metadata'].pop('signature', None)
58 try:
58 try:
59 yield
59 yield
60 finally:
60 finally:
61 if save_signature is not None:
61 if save_signature is not None:
62 nb['metadata']['signature'] = save_signature
62 nb['metadata']['signature'] = save_signature
63
63
64
64
65 def notebook_signature(nb, secret, scheme):
66 """Compute a notebook's signature
67
68 by hashing the entire contents of the notebook via HMAC digest.
69 scheme is the hashing scheme, which must be an attribute of the hashlib module,
70 as listed in hashlib.algorithms.
71 """
72 hmac = HMAC(secret, digestmod=getattr(hashlib, scheme))
73 # don't include the previous hash in the content to hash
74 with signature_removed(nb):
75 # sign the whole thing
76 for b in yield_everything(nb):
77 hmac.update(b)
78
79 return hmac.hexdigest()
80
81
82 def check_notebook_signature(nb, secret, scheme):
83 """Check a notebook's stored signature
84
85 If a signature is stored in the notebook's metadata,
86 a new signature is computed and compared with the stored value.
87
88 Returns True if the signature is found and matches, False otherwise.
89
90 The following conditions must all be met for a notebook to be trusted:
91 - a signature is stored in the form 'scheme:hexdigest'
92 - the stored scheme matches the requested scheme
93 - the requested scheme is available from hashlib
94 - the computed hash from notebook_signature matches the stored hash
95 """
96 stored_signature = nb['metadata'].get('signature', None)
97 if not stored_signature \
98 or not isinstance(stored_signature, string_types) \
99 or ':' not in stored_signature:
100 return False
101 stored_scheme, sig = stored_signature.split(':', 1)
102 if scheme != stored_scheme:
103 return False
104 try:
105 my_signature = notebook_signature(nb, secret, scheme)
106 except AttributeError:
107 return False
108 return my_signature == sig
109
110
111 def trust_notebook(nb, secret, scheme):
112 """Re-sign a notebook, indicating that its output is trusted
113
114 stores 'scheme:hmac-hexdigest' in notebook.metadata.signature
115
116 e.g. 'sha256:deadbeef123...'
117 """
118 signature = notebook_signature(nb, secret, scheme)
119 nb['metadata']['signature'] = "%s:%s" % (scheme, signature)
120
121
122 def mark_trusted_cells(nb, secret, scheme):
123 """Mark cells as trusted if the notebook's signature can be verified
124
125 Sets ``cell.trusted = True | False`` on all code cells,
126 depending on whether the stored signature can be verified.
127 """
128 if not nb['worksheets']:
129 # nothing to mark if there are no cells
130 return True
131 trusted = check_notebook_signature(nb, secret, scheme)
132 for cell in nb['worksheets'][0]['cells']:
133 if cell['cell_type'] == 'code':
134 cell['trusted'] = trusted
135 return trusted
136
137
138 def check_trusted_cells(nb):
139 """Return whether all code cells are trusted
140
141 If there are no code cells, return True.
142 """
143 if not nb['worksheets']:
144 return True
145 for cell in nb['worksheets'][0]['cells']:
146 if cell['cell_type'] != 'code':
147 continue
148 if not cell.get('trusted', False):
149 return False
150 return True
151
152
153 class NotebookNotary(LoggingConfigurable):
65 class NotebookNotary(LoggingConfigurable):
154 """A class for configuring notebook signatures
66 """A class for computing and verifying notebook signatures."""
155
156 It stores the secret with which to sign notebooks,
157 and the hashing scheme to use for notebook signatures.
158 """
159
160 scheme = Enum(hashlib.algorithms, default_value='sha256', config=True,
161 help="""The hashing algorithm used to sign notebooks."""
162 )
163
67
164 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
68 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
165 def _profile_dir_default(self):
69 def _profile_dir_default(self):
166 from IPython.core.application import BaseIPythonApplication
70 from IPython.core.application import BaseIPythonApplication
167 if BaseIPythonApplication.initialized():
71 if BaseIPythonApplication.initialized():
168 app = BaseIPythonApplication.instance()
72 app = BaseIPythonApplication.instance()
169 else:
73 else:
170 # create an app, without the global instance
74 # create an app, without the global instance
171 app = BaseIPythonApplication()
75 app = BaseIPythonApplication()
172 app.initialize()
76 app.initialize()
173 return app.profile_dir
77 return app.profile_dir
174
78
79 algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True,
80 help="""The hashing algorithm used to sign notebooks."""
81 )
82 def _algorithm_changed(self, name, old, new):
83 self.digestmod = getattr(hashlib, self.algorithm)
84
85 digestmod = Any()
86 def _digestmod_default(self):
87 return getattr(hashlib, self.algorithm)
88
89 secret_file = Unicode()
90 def _secret_file_default(self):
91 if self.profile_dir is None:
92 return ''
93 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
94
175 secret = Bytes(config=True,
95 secret = Bytes(config=True,
176 help="""The secret key with which notebooks are signed."""
96 help="""The secret key with which notebooks are signed."""
177 )
97 )
178 def _secret_default(self):
98 def _secret_default(self):
179 # note : this assumes an Application is running
99 # note : this assumes an Application is running
180 profile_dir = self.profile_dir
100 if os.path.exists(self.secret_file):
181 secret_file = os.path.join(profile_dir.security_dir, 'notebook_secret')
101 with io.open(self.secret_file, 'rb') as f:
182 if os.path.exists(secret_file):
183 with io.open(secret_file, 'rb') as f:
184 return f.read()
102 return f.read()
185 else:
103 else:
186 secret = base64.encodestring(os.urandom(1024))
104 secret = base64.encodestring(os.urandom(1024))
187 self.log.info("Writing output secret to %s", secret_file)
105 self._write_secret_file(secret)
188 with io.open(secret_file, 'wb') as f:
189 f.write(secret)
190 try:
191 os.chmod(secret_file, 0o600)
192 except OSError:
193 self.log.warn(
194 "Could not set permissions on %s",
195 secret_file
196 )
197 return secret
106 return secret
107
108 def _write_secret_file(self, secret):
109 """write my secret to my secret_file"""
110 self.log.info("Writing output secret to %s", self.secret_file)
111 with io.open(self.secret_file, 'wb') as f:
112 f.write(secret)
113 try:
114 os.chmod(self.secret_file, 0o600)
115 except OSError:
116 self.log.warn(
117 "Could not set permissions on %s",
118 self.secret_file
119 )
120 return secret
121
122 def compute_signature(self, nb):
123 """Compute a notebook's signature
124
125 by hashing the entire contents of the notebook via HMAC digest.
126 """
127 hmac = HMAC(self.secret, digestmod=self.digestmod)
128 # don't include the previous hash in the content to hash
129 with signature_removed(nb):
130 # sign the whole thing
131 for b in yield_everything(nb):
132 hmac.update(b)
133
134 return hmac.hexdigest()
135
136 def check_signature(self, nb):
137 """Check a notebook's stored signature
138
139 If a signature is stored in the notebook's metadata,
140 a new signature is computed and compared with the stored value.
141
142 Returns True if the signature is found and matches, False otherwise.
143
144 The following conditions must all be met for a notebook to be trusted:
145 - a signature is stored in the form 'scheme:hexdigest'
146 - the stored scheme matches the requested scheme
147 - the requested scheme is available from hashlib
148 - the computed hash from notebook_signature matches the stored hash
149 """
150 stored_signature = nb['metadata'].get('signature', None)
151 if not stored_signature \
152 or not isinstance(stored_signature, string_types) \
153 or ':' not in stored_signature:
154 return False
155 stored_algo, sig = stored_signature.split(':', 1)
156 if self.algorithm != stored_algo:
157 return False
158 my_signature = self.compute_signature(nb)
159 return my_signature == sig
160
161 def sign(self, nb):
162 """Sign a notebook, indicating that its output is trusted
163
164 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
165
166 e.g. 'sha256:deadbeef123...'
167 """
168 signature = self.compute_signature(nb)
169 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
170
171 def mark_cells(self, nb, trusted):
172 """Mark cells as trusted if the notebook's signature can be verified
173
174 Sets ``cell.trusted = True | False`` on all code cells,
175 depending on whether the stored signature can be verified.
176
177 This function is the inverse of check_cells
178 """
179 if not nb['worksheets']:
180 # nothing to mark if there are no cells
181 return
182 for cell in nb['worksheets'][0]['cells']:
183 if cell['cell_type'] == 'code':
184 cell['trusted'] = trusted
185
186 def check_cells(self, nb):
187 """Return whether all code cells are trusted
188
189 If there are no code cells, return True.
190
191 This function is the inverse of mark_cells.
192 """
193 if not nb['worksheets']:
194 return True
195 for cell in nb['worksheets'][0]['cells']:
196 if cell['cell_type'] != 'code':
197 continue
198 if not cell.get('trusted', False):
199 return False
200 return True
201
198
202
199 No newline at end of file
203
General Comments 0
You need to be logged in to leave comments. Login now