##// END OF EJS Templates
authenticate local file access...
MinRK -
Show More
@@ -1,614 +1,623 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
19 import logging
20 import Cookie
20 import Cookie
21 import uuid
21 import uuid
22
22
23 from tornado import web
23 from tornado import web
24 from tornado import websocket
24 from tornado import websocket
25
25
26 from zmq.eventloop import ioloop
26 from zmq.eventloop import ioloop
27 from zmq.utils import jsonapi
27 from zmq.utils import jsonapi
28
28
29 from IPython.external.decorator import decorator
29 from IPython.external.decorator import decorator
30 from IPython.zmq.session import Session
30 from IPython.zmq.session import Session
31 from IPython.lib.security import passwd_check
31 from IPython.lib.security import passwd_check
32
32
33 try:
33 try:
34 from docutils.core import publish_string
34 from docutils.core import publish_string
35 except ImportError:
35 except ImportError:
36 publish_string = None
36 publish_string = None
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # parts tornado cares about haven't really changed, so it's OK to continue
43 # parts tornado cares about haven't really changed, so it's OK to continue
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # version as of Oct 30/2011) the version check fails, see the issue report:
45 # version as of Oct 30/2011) the version check fails, see the issue report:
46
46
47 # https://github.com/facebook/tornado/issues/385
47 # https://github.com/facebook/tornado/issues/385
48
48
49 # This issue has been fixed in Tornado post 2.1.1:
49 # This issue has been fixed in Tornado post 2.1.1:
50
50
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52
52
53 # Here we manually apply the same patch as above so that users of IPython can
53 # Here we manually apply the same patch as above so that users of IPython can
54 # continue to work with an officially released Tornado. We make the
54 # continue to work with an officially released Tornado. We make the
55 # monkeypatch version check as narrow as possible to limit its effects; once
55 # monkeypatch version check as narrow as possible to limit its effects; once
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57
57
58 import tornado
58 import tornado
59
59
60 if tornado.version_info <= (2,1,1):
60 if tornado.version_info <= (2,1,1):
61
61
62 def _execute(self, transforms, *args, **kwargs):
62 def _execute(self, transforms, *args, **kwargs):
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64
64
65 self.open_args = args
65 self.open_args = args
66 self.open_kwargs = kwargs
66 self.open_kwargs = kwargs
67
67
68 # The difference between version 8 and 13 is that in 8 the
68 # The difference between version 8 and 13 is that in 8 the
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # simply "Origin".
70 # simply "Origin".
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol8(self)
72 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection.accept_connection()
73 self.ws_connection.accept_connection()
74
74
75 elif self.request.headers.get("Sec-WebSocket-Version"):
75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 self.stream.write(tornado.escape.utf8(
76 self.stream.write(tornado.escape.utf8(
77 "HTTP/1.1 426 Upgrade Required\r\n"
77 "HTTP/1.1 426 Upgrade Required\r\n"
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 self.stream.close()
79 self.stream.close()
80
80
81 else:
81 else:
82 self.ws_connection = WebSocketProtocol76(self)
82 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
84
84
85 websocket.WebSocketHandler._execute = _execute
85 websocket.WebSocketHandler._execute = _execute
86 del _execute
86 del _execute
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Decorator for disabling read-only handlers
89 # Decorator for disabling read-only handlers
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 @decorator
92 @decorator
93 def not_if_readonly(f, self, *args, **kwargs):
93 def not_if_readonly(f, self, *args, **kwargs):
94 if self.application.read_only:
94 if self.application.read_only:
95 raise web.HTTPError(403, "Notebook server is read-only")
95 raise web.HTTPError(403, "Notebook server is read-only")
96 else:
96 else:
97 return f(self, *args, **kwargs)
97 return f(self, *args, **kwargs)
98
98
99 @decorator
99 @decorator
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 """authenticate this page *unless* readonly view is active.
101 """authenticate this page *unless* readonly view is active.
102
102
103 In read-only mode, the notebook list and print view should
103 In read-only mode, the notebook list and print view should
104 be accessible without authentication.
104 be accessible without authentication.
105 """
105 """
106
106
107 @web.authenticated
107 @web.authenticated
108 def auth_f(self, *args, **kwargs):
108 def auth_f(self, *args, **kwargs):
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110
110 if self.application.read_only:
111 if self.application.read_only:
111 return f(self, *args, **kwargs)
112 return f(self, *args, **kwargs)
112 else:
113 else:
113 return auth_f(self, *args, **kwargs)
114 return auth_f(self, *args, **kwargs)
114
115
115 #-----------------------------------------------------------------------------
116 #-----------------------------------------------------------------------------
116 # Top-level handlers
117 # Top-level handlers
117 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
118
119
119 class RequestHandler(web.RequestHandler):
120 class RequestHandler(web.RequestHandler):
120 """RequestHandler with default variable setting."""
121 """RequestHandler with default variable setting."""
121
122
122 def render(*args, **kwargs):
123 def render(*args, **kwargs):
123 kwargs.setdefault('message', '')
124 kwargs.setdefault('message', '')
124 return web.RequestHandler.render(*args, **kwargs)
125 return web.RequestHandler.render(*args, **kwargs)
125
126
126 class AuthenticatedHandler(RequestHandler):
127 class AuthenticatedHandler(RequestHandler):
127 """A RequestHandler with an authenticated user."""
128 """A RequestHandler with an authenticated user."""
128
129
129 def get_current_user(self):
130 def get_current_user(self):
130 user_id = self.get_secure_cookie("username")
131 user_id = self.get_secure_cookie("username")
131 # For now the user_id should not return empty, but it could eventually
132 # For now the user_id should not return empty, but it could eventually
132 if user_id == '':
133 if user_id == '':
133 user_id = 'anonymous'
134 user_id = 'anonymous'
134 if user_id is None:
135 if user_id is None:
135 # prevent extra Invalid cookie sig warnings:
136 # prevent extra Invalid cookie sig warnings:
136 self.clear_cookie('username')
137 self.clear_cookie('username')
137 if not self.application.password and not self.application.read_only:
138 if not self.application.password and not self.application.read_only:
138 user_id = 'anonymous'
139 user_id = 'anonymous'
139 return user_id
140 return user_id
140
141
141 @property
142 @property
142 def logged_in(self):
143 def logged_in(self):
143 """Is a user currently logged in?
144 """Is a user currently logged in?
144
145
145 """
146 """
146 user = self.get_current_user()
147 user = self.get_current_user()
147 return (user and not user == 'anonymous')
148 return (user and not user == 'anonymous')
148
149
149 @property
150 @property
150 def login_available(self):
151 def login_available(self):
151 """May a user proceed to log in?
152 """May a user proceed to log in?
152
153
153 This returns True if login capability is available, irrespective of
154 This returns True if login capability is available, irrespective of
154 whether the user is already logged in or not.
155 whether the user is already logged in or not.
155
156
156 """
157 """
157 return bool(self.application.password)
158 return bool(self.application.password)
158
159
159 @property
160 @property
160 def read_only(self):
161 def read_only(self):
161 """Is the notebook read-only?
162 """Is the notebook read-only?
162
163
163 """
164 """
164 return self.application.read_only
165 return self.application.read_only
165
166
166 @property
167 @property
167 def ws_url(self):
168 def ws_url(self):
168 """websocket url matching the current request
169 """websocket url matching the current request
169
170
170 turns http[s]://host[:port] into
171 turns http[s]://host[:port] into
171 ws[s]://host[:port]
172 ws[s]://host[:port]
172 """
173 """
173 proto = self.request.protocol.replace('http', 'ws')
174 proto = self.request.protocol.replace('http', 'ws')
174 return "%s://%s" % (proto, self.request.host)
175 return "%s://%s" % (proto, self.request.host)
175
176
176
177
178 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
179 """static files should only be accessible when logged in"""
180
181 @authenticate_unless_readonly
182 def get(self, path):
183 return web.StaticFileHandler.get(self, path)
184
185
177 class ProjectDashboardHandler(AuthenticatedHandler):
186 class ProjectDashboardHandler(AuthenticatedHandler):
178
187
179 @authenticate_unless_readonly
188 @authenticate_unless_readonly
180 def get(self):
189 def get(self):
181 nbm = self.application.notebook_manager
190 nbm = self.application.notebook_manager
182 project = nbm.notebook_dir
191 project = nbm.notebook_dir
183 self.render(
192 self.render(
184 'projectdashboard.html', project=project,
193 'projectdashboard.html', project=project,
185 base_project_url=u'/', base_kernel_url=u'/',
194 base_project_url=u'/', base_kernel_url=u'/',
186 read_only=self.read_only,
195 read_only=self.read_only,
187 logged_in=self.logged_in,
196 logged_in=self.logged_in,
188 login_available=self.login_available
197 login_available=self.login_available
189 )
198 )
190
199
191
200
192 class LoginHandler(AuthenticatedHandler):
201 class LoginHandler(AuthenticatedHandler):
193
202
194 def _render(self, message=None):
203 def _render(self, message=None):
195 self.render('login.html',
204 self.render('login.html',
196 next=self.get_argument('next', default='/'),
205 next=self.get_argument('next', default='/'),
197 read_only=self.read_only,
206 read_only=self.read_only,
198 logged_in=self.logged_in,
207 logged_in=self.logged_in,
199 login_available=self.login_available,
208 login_available=self.login_available,
200 message=message
209 message=message
201 )
210 )
202
211
203 def get(self):
212 def get(self):
204 if self.current_user:
213 if self.current_user:
205 self.redirect(self.get_argument('next', default='/'))
214 self.redirect(self.get_argument('next', default='/'))
206 else:
215 else:
207 self._render()
216 self._render()
208
217
209 def post(self):
218 def post(self):
210 pwd = self.get_argument('password', default=u'')
219 pwd = self.get_argument('password', default=u'')
211 if self.application.password:
220 if self.application.password:
212 if passwd_check(self.application.password, pwd):
221 if passwd_check(self.application.password, pwd):
213 self.set_secure_cookie('username', str(uuid.uuid4()))
222 self.set_secure_cookie('username', str(uuid.uuid4()))
214 else:
223 else:
215 self._render(message={'error': 'Invalid password'})
224 self._render(message={'error': 'Invalid password'})
216 return
225 return
217
226
218 self.redirect(self.get_argument('next', default='/'))
227 self.redirect(self.get_argument('next', default='/'))
219
228
220
229
221 class LogoutHandler(AuthenticatedHandler):
230 class LogoutHandler(AuthenticatedHandler):
222
231
223 def get(self):
232 def get(self):
224 self.clear_cookie('username')
233 self.clear_cookie('username')
225 if self.login_available:
234 if self.login_available:
226 message = {'info': 'Successfully logged out.'}
235 message = {'info': 'Successfully logged out.'}
227 else:
236 else:
228 message = {'warning': 'Cannot log out. Notebook authentication '
237 message = {'warning': 'Cannot log out. Notebook authentication '
229 'is disabled.'}
238 'is disabled.'}
230
239
231 self.render('logout.html',
240 self.render('logout.html',
232 read_only=self.read_only,
241 read_only=self.read_only,
233 logged_in=self.logged_in,
242 logged_in=self.logged_in,
234 login_available=self.login_available,
243 login_available=self.login_available,
235 message=message)
244 message=message)
236
245
237
246
238 class NewHandler(AuthenticatedHandler):
247 class NewHandler(AuthenticatedHandler):
239
248
240 @web.authenticated
249 @web.authenticated
241 def get(self):
250 def get(self):
242 nbm = self.application.notebook_manager
251 nbm = self.application.notebook_manager
243 project = nbm.notebook_dir
252 project = nbm.notebook_dir
244 notebook_id = nbm.new_notebook()
253 notebook_id = nbm.new_notebook()
245 self.render(
254 self.render(
246 'notebook.html', project=project,
255 'notebook.html', project=project,
247 notebook_id=notebook_id,
256 notebook_id=notebook_id,
248 base_project_url=u'/', base_kernel_url=u'/',
257 base_project_url=u'/', base_kernel_url=u'/',
249 kill_kernel=False,
258 kill_kernel=False,
250 read_only=False,
259 read_only=False,
251 logged_in=self.logged_in,
260 logged_in=self.logged_in,
252 login_available=self.login_available,
261 login_available=self.login_available,
253 mathjax_url=self.application.ipython_app.mathjax_url,
262 mathjax_url=self.application.ipython_app.mathjax_url,
254 )
263 )
255
264
256
265
257 class NamedNotebookHandler(AuthenticatedHandler):
266 class NamedNotebookHandler(AuthenticatedHandler):
258
267
259 @authenticate_unless_readonly
268 @authenticate_unless_readonly
260 def get(self, notebook_id):
269 def get(self, notebook_id):
261 nbm = self.application.notebook_manager
270 nbm = self.application.notebook_manager
262 project = nbm.notebook_dir
271 project = nbm.notebook_dir
263 if not nbm.notebook_exists(notebook_id):
272 if not nbm.notebook_exists(notebook_id):
264 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
273 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
265
274
266 self.render(
275 self.render(
267 'notebook.html', project=project,
276 'notebook.html', project=project,
268 notebook_id=notebook_id,
277 notebook_id=notebook_id,
269 base_project_url=u'/', base_kernel_url=u'/',
278 base_project_url=u'/', base_kernel_url=u'/',
270 kill_kernel=False,
279 kill_kernel=False,
271 read_only=self.read_only,
280 read_only=self.read_only,
272 logged_in=self.logged_in,
281 logged_in=self.logged_in,
273 login_available=self.login_available,
282 login_available=self.login_available,
274 mathjax_url=self.application.ipython_app.mathjax_url,
283 mathjax_url=self.application.ipython_app.mathjax_url,
275 )
284 )
276
285
277
286
278 #-----------------------------------------------------------------------------
287 #-----------------------------------------------------------------------------
279 # Kernel handlers
288 # Kernel handlers
280 #-----------------------------------------------------------------------------
289 #-----------------------------------------------------------------------------
281
290
282
291
283 class MainKernelHandler(AuthenticatedHandler):
292 class MainKernelHandler(AuthenticatedHandler):
284
293
285 @web.authenticated
294 @web.authenticated
286 def get(self):
295 def get(self):
287 km = self.application.kernel_manager
296 km = self.application.kernel_manager
288 self.finish(jsonapi.dumps(km.kernel_ids))
297 self.finish(jsonapi.dumps(km.kernel_ids))
289
298
290 @web.authenticated
299 @web.authenticated
291 def post(self):
300 def post(self):
292 km = self.application.kernel_manager
301 km = self.application.kernel_manager
293 notebook_id = self.get_argument('notebook', default=None)
302 notebook_id = self.get_argument('notebook', default=None)
294 kernel_id = km.start_kernel(notebook_id)
303 kernel_id = km.start_kernel(notebook_id)
295 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
304 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
296 self.set_header('Location', '/'+kernel_id)
305 self.set_header('Location', '/'+kernel_id)
297 self.finish(jsonapi.dumps(data))
306 self.finish(jsonapi.dumps(data))
298
307
299
308
300 class KernelHandler(AuthenticatedHandler):
309 class KernelHandler(AuthenticatedHandler):
301
310
302 SUPPORTED_METHODS = ('DELETE')
311 SUPPORTED_METHODS = ('DELETE')
303
312
304 @web.authenticated
313 @web.authenticated
305 def delete(self, kernel_id):
314 def delete(self, kernel_id):
306 km = self.application.kernel_manager
315 km = self.application.kernel_manager
307 km.kill_kernel(kernel_id)
316 km.kill_kernel(kernel_id)
308 self.set_status(204)
317 self.set_status(204)
309 self.finish()
318 self.finish()
310
319
311
320
312 class KernelActionHandler(AuthenticatedHandler):
321 class KernelActionHandler(AuthenticatedHandler):
313
322
314 @web.authenticated
323 @web.authenticated
315 def post(self, kernel_id, action):
324 def post(self, kernel_id, action):
316 km = self.application.kernel_manager
325 km = self.application.kernel_manager
317 if action == 'interrupt':
326 if action == 'interrupt':
318 km.interrupt_kernel(kernel_id)
327 km.interrupt_kernel(kernel_id)
319 self.set_status(204)
328 self.set_status(204)
320 if action == 'restart':
329 if action == 'restart':
321 new_kernel_id = km.restart_kernel(kernel_id)
330 new_kernel_id = km.restart_kernel(kernel_id)
322 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
331 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
323 self.set_header('Location', '/'+new_kernel_id)
332 self.set_header('Location', '/'+new_kernel_id)
324 self.write(jsonapi.dumps(data))
333 self.write(jsonapi.dumps(data))
325 self.finish()
334 self.finish()
326
335
327
336
328 class ZMQStreamHandler(websocket.WebSocketHandler):
337 class ZMQStreamHandler(websocket.WebSocketHandler):
329
338
330 def _reserialize_reply(self, msg_list):
339 def _reserialize_reply(self, msg_list):
331 """Reserialize a reply message using JSON.
340 """Reserialize a reply message using JSON.
332
341
333 This takes the msg list from the ZMQ socket, unserializes it using
342 This takes the msg list from the ZMQ socket, unserializes it using
334 self.session and then serializes the result using JSON. This method
343 self.session and then serializes the result using JSON. This method
335 should be used by self._on_zmq_reply to build messages that can
344 should be used by self._on_zmq_reply to build messages that can
336 be sent back to the browser.
345 be sent back to the browser.
337 """
346 """
338 idents, msg_list = self.session.feed_identities(msg_list)
347 idents, msg_list = self.session.feed_identities(msg_list)
339 msg = self.session.unserialize(msg_list)
348 msg = self.session.unserialize(msg_list)
340 try:
349 try:
341 msg['header'].pop('date')
350 msg['header'].pop('date')
342 except KeyError:
351 except KeyError:
343 pass
352 pass
344 try:
353 try:
345 msg['parent_header'].pop('date')
354 msg['parent_header'].pop('date')
346 except KeyError:
355 except KeyError:
347 pass
356 pass
348 msg.pop('buffers')
357 msg.pop('buffers')
349 return jsonapi.dumps(msg)
358 return jsonapi.dumps(msg)
350
359
351 def _on_zmq_reply(self, msg_list):
360 def _on_zmq_reply(self, msg_list):
352 try:
361 try:
353 msg = self._reserialize_reply(msg_list)
362 msg = self._reserialize_reply(msg_list)
354 except:
363 except:
355 self.application.log.critical("Malformed message: %r" % msg_list)
364 self.application.log.critical("Malformed message: %r" % msg_list)
356 else:
365 else:
357 self.write_message(msg)
366 self.write_message(msg)
358
367
359
368
360 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
369 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
361
370
362 def open(self, kernel_id):
371 def open(self, kernel_id):
363 self.kernel_id = kernel_id.decode('ascii')
372 self.kernel_id = kernel_id.decode('ascii')
364 try:
373 try:
365 cfg = self.application.ipython_app.config
374 cfg = self.application.ipython_app.config
366 except AttributeError:
375 except AttributeError:
367 # protect from the case where this is run from something other than
376 # protect from the case where this is run from something other than
368 # the notebook app:
377 # the notebook app:
369 cfg = None
378 cfg = None
370 self.session = Session(config=cfg)
379 self.session = Session(config=cfg)
371 self.save_on_message = self.on_message
380 self.save_on_message = self.on_message
372 self.on_message = self.on_first_message
381 self.on_message = self.on_first_message
373
382
374 def get_current_user(self):
383 def get_current_user(self):
375 user_id = self.get_secure_cookie("username")
384 user_id = self.get_secure_cookie("username")
376 if user_id == '' or (user_id is None and not self.application.password):
385 if user_id == '' or (user_id is None and not self.application.password):
377 user_id = 'anonymous'
386 user_id = 'anonymous'
378 return user_id
387 return user_id
379
388
380 def _inject_cookie_message(self, msg):
389 def _inject_cookie_message(self, msg):
381 """Inject the first message, which is the document cookie,
390 """Inject the first message, which is the document cookie,
382 for authentication."""
391 for authentication."""
383 if isinstance(msg, unicode):
392 if isinstance(msg, unicode):
384 # Cookie can't constructor doesn't accept unicode strings for some reason
393 # Cookie can't constructor doesn't accept unicode strings for some reason
385 msg = msg.encode('utf8', 'replace')
394 msg = msg.encode('utf8', 'replace')
386 try:
395 try:
387 self.request._cookies = Cookie.SimpleCookie(msg)
396 self.request._cookies = Cookie.SimpleCookie(msg)
388 except:
397 except:
389 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
398 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
390
399
391 def on_first_message(self, msg):
400 def on_first_message(self, msg):
392 self._inject_cookie_message(msg)
401 self._inject_cookie_message(msg)
393 if self.get_current_user() is None:
402 if self.get_current_user() is None:
394 logging.warn("Couldn't authenticate WebSocket connection")
403 logging.warn("Couldn't authenticate WebSocket connection")
395 raise web.HTTPError(403)
404 raise web.HTTPError(403)
396 self.on_message = self.save_on_message
405 self.on_message = self.save_on_message
397
406
398
407
399 class IOPubHandler(AuthenticatedZMQStreamHandler):
408 class IOPubHandler(AuthenticatedZMQStreamHandler):
400
409
401 def initialize(self, *args, **kwargs):
410 def initialize(self, *args, **kwargs):
402 self._kernel_alive = True
411 self._kernel_alive = True
403 self._beating = False
412 self._beating = False
404 self.iopub_stream = None
413 self.iopub_stream = None
405 self.hb_stream = None
414 self.hb_stream = None
406
415
407 def on_first_message(self, msg):
416 def on_first_message(self, msg):
408 try:
417 try:
409 super(IOPubHandler, self).on_first_message(msg)
418 super(IOPubHandler, self).on_first_message(msg)
410 except web.HTTPError:
419 except web.HTTPError:
411 self.close()
420 self.close()
412 return
421 return
413 km = self.application.kernel_manager
422 km = self.application.kernel_manager
414 self.time_to_dead = km.time_to_dead
423 self.time_to_dead = km.time_to_dead
415 kernel_id = self.kernel_id
424 kernel_id = self.kernel_id
416 try:
425 try:
417 self.iopub_stream = km.create_iopub_stream(kernel_id)
426 self.iopub_stream = km.create_iopub_stream(kernel_id)
418 self.hb_stream = km.create_hb_stream(kernel_id)
427 self.hb_stream = km.create_hb_stream(kernel_id)
419 except web.HTTPError:
428 except web.HTTPError:
420 # WebSockets don't response to traditional error codes so we
429 # WebSockets don't response to traditional error codes so we
421 # close the connection.
430 # close the connection.
422 if not self.stream.closed():
431 if not self.stream.closed():
423 self.stream.close()
432 self.stream.close()
424 self.close()
433 self.close()
425 else:
434 else:
426 self.iopub_stream.on_recv(self._on_zmq_reply)
435 self.iopub_stream.on_recv(self._on_zmq_reply)
427 self.start_hb(self.kernel_died)
436 self.start_hb(self.kernel_died)
428
437
429 def on_message(self, msg):
438 def on_message(self, msg):
430 pass
439 pass
431
440
432 def on_close(self):
441 def on_close(self):
433 # This method can be called twice, once by self.kernel_died and once
442 # This method can be called twice, once by self.kernel_died and once
434 # from the WebSocket close event. If the WebSocket connection is
443 # from the WebSocket close event. If the WebSocket connection is
435 # closed before the ZMQ streams are setup, they could be None.
444 # closed before the ZMQ streams are setup, they could be None.
436 self.stop_hb()
445 self.stop_hb()
437 if self.iopub_stream is not None and not self.iopub_stream.closed():
446 if self.iopub_stream is not None and not self.iopub_stream.closed():
438 self.iopub_stream.on_recv(None)
447 self.iopub_stream.on_recv(None)
439 self.iopub_stream.close()
448 self.iopub_stream.close()
440 if self.hb_stream is not None and not self.hb_stream.closed():
449 if self.hb_stream is not None and not self.hb_stream.closed():
441 self.hb_stream.close()
450 self.hb_stream.close()
442
451
443 def start_hb(self, callback):
452 def start_hb(self, callback):
444 """Start the heartbeating and call the callback if the kernel dies."""
453 """Start the heartbeating and call the callback if the kernel dies."""
445 if not self._beating:
454 if not self._beating:
446 self._kernel_alive = True
455 self._kernel_alive = True
447
456
448 def ping_or_dead():
457 def ping_or_dead():
449 if self._kernel_alive:
458 if self._kernel_alive:
450 self._kernel_alive = False
459 self._kernel_alive = False
451 self.hb_stream.send(b'ping')
460 self.hb_stream.send(b'ping')
452 else:
461 else:
453 try:
462 try:
454 callback()
463 callback()
455 except:
464 except:
456 pass
465 pass
457 finally:
466 finally:
458 self._hb_periodic_callback.stop()
467 self._hb_periodic_callback.stop()
459
468
460 def beat_received(msg):
469 def beat_received(msg):
461 self._kernel_alive = True
470 self._kernel_alive = True
462
471
463 self.hb_stream.on_recv(beat_received)
472 self.hb_stream.on_recv(beat_received)
464 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
473 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
465 self._hb_periodic_callback.start()
474 self._hb_periodic_callback.start()
466 self._beating= True
475 self._beating= True
467
476
468 def stop_hb(self):
477 def stop_hb(self):
469 """Stop the heartbeating and cancel all related callbacks."""
478 """Stop the heartbeating and cancel all related callbacks."""
470 if self._beating:
479 if self._beating:
471 self._hb_periodic_callback.stop()
480 self._hb_periodic_callback.stop()
472 if not self.hb_stream.closed():
481 if not self.hb_stream.closed():
473 self.hb_stream.on_recv(None)
482 self.hb_stream.on_recv(None)
474
483
475 def kernel_died(self):
484 def kernel_died(self):
476 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
485 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
477 self.write_message(
486 self.write_message(
478 {'header': {'msg_type': 'status'},
487 {'header': {'msg_type': 'status'},
479 'parent_header': {},
488 'parent_header': {},
480 'content': {'execution_state':'dead'}
489 'content': {'execution_state':'dead'}
481 }
490 }
482 )
491 )
483 self.on_close()
492 self.on_close()
484
493
485
494
486 class ShellHandler(AuthenticatedZMQStreamHandler):
495 class ShellHandler(AuthenticatedZMQStreamHandler):
487
496
488 def initialize(self, *args, **kwargs):
497 def initialize(self, *args, **kwargs):
489 self.shell_stream = None
498 self.shell_stream = None
490
499
491 def on_first_message(self, msg):
500 def on_first_message(self, msg):
492 try:
501 try:
493 super(ShellHandler, self).on_first_message(msg)
502 super(ShellHandler, self).on_first_message(msg)
494 except web.HTTPError:
503 except web.HTTPError:
495 self.close()
504 self.close()
496 return
505 return
497 km = self.application.kernel_manager
506 km = self.application.kernel_manager
498 self.max_msg_size = km.max_msg_size
507 self.max_msg_size = km.max_msg_size
499 kernel_id = self.kernel_id
508 kernel_id = self.kernel_id
500 try:
509 try:
501 self.shell_stream = km.create_shell_stream(kernel_id)
510 self.shell_stream = km.create_shell_stream(kernel_id)
502 except web.HTTPError:
511 except web.HTTPError:
503 # WebSockets don't response to traditional error codes so we
512 # WebSockets don't response to traditional error codes so we
504 # close the connection.
513 # close the connection.
505 if not self.stream.closed():
514 if not self.stream.closed():
506 self.stream.close()
515 self.stream.close()
507 self.close()
516 self.close()
508 else:
517 else:
509 self.shell_stream.on_recv(self._on_zmq_reply)
518 self.shell_stream.on_recv(self._on_zmq_reply)
510
519
511 def on_message(self, msg):
520 def on_message(self, msg):
512 if len(msg) < self.max_msg_size:
521 if len(msg) < self.max_msg_size:
513 msg = jsonapi.loads(msg)
522 msg = jsonapi.loads(msg)
514 self.session.send(self.shell_stream, msg)
523 self.session.send(self.shell_stream, msg)
515
524
516 def on_close(self):
525 def on_close(self):
517 # Make sure the stream exists and is not already closed.
526 # Make sure the stream exists and is not already closed.
518 if self.shell_stream is not None and not self.shell_stream.closed():
527 if self.shell_stream is not None and not self.shell_stream.closed():
519 self.shell_stream.close()
528 self.shell_stream.close()
520
529
521
530
522 #-----------------------------------------------------------------------------
531 #-----------------------------------------------------------------------------
523 # Notebook web service handlers
532 # Notebook web service handlers
524 #-----------------------------------------------------------------------------
533 #-----------------------------------------------------------------------------
525
534
526 class NotebookRootHandler(AuthenticatedHandler):
535 class NotebookRootHandler(AuthenticatedHandler):
527
536
528 @authenticate_unless_readonly
537 @authenticate_unless_readonly
529 def get(self):
538 def get(self):
530
539
531 nbm = self.application.notebook_manager
540 nbm = self.application.notebook_manager
532 files = nbm.list_notebooks()
541 files = nbm.list_notebooks()
533 self.finish(jsonapi.dumps(files))
542 self.finish(jsonapi.dumps(files))
534
543
535 @web.authenticated
544 @web.authenticated
536 def post(self):
545 def post(self):
537 nbm = self.application.notebook_manager
546 nbm = self.application.notebook_manager
538 body = self.request.body.strip()
547 body = self.request.body.strip()
539 format = self.get_argument('format', default='json')
548 format = self.get_argument('format', default='json')
540 name = self.get_argument('name', default=None)
549 name = self.get_argument('name', default=None)
541 if body:
550 if body:
542 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
551 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
543 else:
552 else:
544 notebook_id = nbm.new_notebook()
553 notebook_id = nbm.new_notebook()
545 self.set_header('Location', '/'+notebook_id)
554 self.set_header('Location', '/'+notebook_id)
546 self.finish(jsonapi.dumps(notebook_id))
555 self.finish(jsonapi.dumps(notebook_id))
547
556
548
557
549 class NotebookHandler(AuthenticatedHandler):
558 class NotebookHandler(AuthenticatedHandler):
550
559
551 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
560 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
552
561
553 @authenticate_unless_readonly
562 @authenticate_unless_readonly
554 def get(self, notebook_id):
563 def get(self, notebook_id):
555 nbm = self.application.notebook_manager
564 nbm = self.application.notebook_manager
556 format = self.get_argument('format', default='json')
565 format = self.get_argument('format', default='json')
557 last_mod, name, data = nbm.get_notebook(notebook_id, format)
566 last_mod, name, data = nbm.get_notebook(notebook_id, format)
558
567
559 if format == u'json':
568 if format == u'json':
560 self.set_header('Content-Type', 'application/json')
569 self.set_header('Content-Type', 'application/json')
561 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
570 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
562 elif format == u'py':
571 elif format == u'py':
563 self.set_header('Content-Type', 'application/x-python')
572 self.set_header('Content-Type', 'application/x-python')
564 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
573 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
565 self.set_header('Last-Modified', last_mod)
574 self.set_header('Last-Modified', last_mod)
566 self.finish(data)
575 self.finish(data)
567
576
568 @web.authenticated
577 @web.authenticated
569 def put(self, notebook_id):
578 def put(self, notebook_id):
570 nbm = self.application.notebook_manager
579 nbm = self.application.notebook_manager
571 format = self.get_argument('format', default='json')
580 format = self.get_argument('format', default='json')
572 name = self.get_argument('name', default=None)
581 name = self.get_argument('name', default=None)
573 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
582 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
574 self.set_status(204)
583 self.set_status(204)
575 self.finish()
584 self.finish()
576
585
577 @web.authenticated
586 @web.authenticated
578 def delete(self, notebook_id):
587 def delete(self, notebook_id):
579 nbm = self.application.notebook_manager
588 nbm = self.application.notebook_manager
580 nbm.delete_notebook(notebook_id)
589 nbm.delete_notebook(notebook_id)
581 self.set_status(204)
590 self.set_status(204)
582 self.finish()
591 self.finish()
583
592
584 #-----------------------------------------------------------------------------
593 #-----------------------------------------------------------------------------
585 # RST web service handlers
594 # RST web service handlers
586 #-----------------------------------------------------------------------------
595 #-----------------------------------------------------------------------------
587
596
588
597
589 class RSTHandler(AuthenticatedHandler):
598 class RSTHandler(AuthenticatedHandler):
590
599
591 @web.authenticated
600 @web.authenticated
592 def post(self):
601 def post(self):
593 if publish_string is None:
602 if publish_string is None:
594 raise web.HTTPError(503, u'docutils not available')
603 raise web.HTTPError(503, u'docutils not available')
595 body = self.request.body.strip()
604 body = self.request.body.strip()
596 source = body
605 source = body
597 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
606 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
598 defaults = {'file_insertion_enabled': 0,
607 defaults = {'file_insertion_enabled': 0,
599 'raw_enabled': 0,
608 'raw_enabled': 0,
600 '_disable_config': 1,
609 '_disable_config': 1,
601 'stylesheet_path': 0
610 'stylesheet_path': 0
602 # 'template': template_path
611 # 'template': template_path
603 }
612 }
604 try:
613 try:
605 html = publish_string(source, writer_name='html',
614 html = publish_string(source, writer_name='html',
606 settings_overrides=defaults
615 settings_overrides=defaults
607 )
616 )
608 except:
617 except:
609 raise web.HTTPError(400, u'Invalid RST')
618 raise web.HTTPError(400, u'Invalid RST')
610 print html
619 print html
611 self.set_header('Content-Type', 'text/html')
620 self.set_header('Content-Type', 'text/html')
612 self.finish(html)
621 self.finish(html)
613
622
614
623
@@ -1,386 +1,387 b''
1 """A tornado based IPython notebook server.
1 """A tornado based IPython notebook server.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import signal
23 import signal
24 import socket
24 import socket
25 import sys
25 import sys
26 import threading
26 import threading
27 import webbrowser
27 import webbrowser
28
28
29 # Third party
29 # Third party
30 import zmq
30 import zmq
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
35 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
36 # when pyzmq dependency is updated beyond that.
36 # when pyzmq dependency is updated beyond that.
37 if hasattr(ioloop, 'install'):
37 if hasattr(ioloop, 'install'):
38 ioloop.install()
38 ioloop.install()
39 else:
39 else:
40 import tornado.ioloop
40 import tornado.ioloop
41 tornado.ioloop.IOLoop = ioloop.IOLoop
41 tornado.ioloop.IOLoop = ioloop.IOLoop
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
51 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler,
52 AuthenticatedFileHandler,
52 )
53 )
53 from .notebookmanager import NotebookManager
54 from .notebookmanager import NotebookManager
54
55
55 from IPython.config.application import catch_config_error, boolean_flag
56 from IPython.config.application import catch_config_error, boolean_flag
56 from IPython.core.application import BaseIPythonApplication
57 from IPython.core.application import BaseIPythonApplication
57 from IPython.core.profiledir import ProfileDir
58 from IPython.core.profiledir import ProfileDir
58 from IPython.lib.kernel import swallow_argv
59 from IPython.lib.kernel import swallow_argv
59 from IPython.zmq.session import Session, default_secure
60 from IPython.zmq.session import Session, default_secure
60 from IPython.zmq.zmqshell import ZMQInteractiveShell
61 from IPython.zmq.zmqshell import ZMQInteractiveShell
61 from IPython.zmq.ipkernel import (
62 from IPython.zmq.ipkernel import (
62 flags as ipkernel_flags,
63 flags as ipkernel_flags,
63 aliases as ipkernel_aliases,
64 aliases as ipkernel_aliases,
64 IPKernelApp
65 IPKernelApp
65 )
66 )
66 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
67 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
67
68
68 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
69 # Module globals
70 # Module globals
70 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
71
72
72 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
73 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
73 _kernel_action_regex = r"(?P<action>restart|interrupt)"
74 _kernel_action_regex = r"(?P<action>restart|interrupt)"
74 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
75 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
75
76
76 LOCALHOST = '127.0.0.1'
77 LOCALHOST = '127.0.0.1'
77
78
78 _examples = """
79 _examples = """
79 ipython notebook # start the notebook
80 ipython notebook # start the notebook
80 ipython notebook --profile=sympy # use the sympy profile
81 ipython notebook --profile=sympy # use the sympy profile
81 ipython notebook --pylab=inline # pylab in inline plotting mode
82 ipython notebook --pylab=inline # pylab in inline plotting mode
82 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
83 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
83 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
84 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
84 """
85 """
85
86
86 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
87 # The Tornado web application
88 # The Tornado web application
88 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
89
90
90 class NotebookWebApplication(web.Application):
91 class NotebookWebApplication(web.Application):
91
92
92 def __init__(self, ipython_app, kernel_manager, notebook_manager, log, settings_overrides):
93 def __init__(self, ipython_app, kernel_manager, notebook_manager, log, settings_overrides):
93 handlers = [
94 handlers = [
94 (r"/", ProjectDashboardHandler),
95 (r"/", ProjectDashboardHandler),
95 (r"/login", LoginHandler),
96 (r"/login", LoginHandler),
96 (r"/logout", LogoutHandler),
97 (r"/logout", LogoutHandler),
97 (r"/new", NewHandler),
98 (r"/new", NewHandler),
98 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
99 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
99 (r"/kernels", MainKernelHandler),
100 (r"/kernels", MainKernelHandler),
100 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
101 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
101 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
102 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
102 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
103 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
103 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
104 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
104 (r"/notebooks", NotebookRootHandler),
105 (r"/notebooks", NotebookRootHandler),
105 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
106 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
106 (r"/rstservice/render", RSTHandler),
107 (r"/rstservice/render", RSTHandler),
107 (r"/local/(.*)", web.StaticFileHandler, {'path' : notebook_manager.notebook_dir}),
108 (r"/local/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
108 ]
109 ]
109 settings = dict(
110 settings = dict(
110 template_path=os.path.join(os.path.dirname(__file__), "templates"),
111 template_path=os.path.join(os.path.dirname(__file__), "templates"),
111 static_path=os.path.join(os.path.dirname(__file__), "static"),
112 static_path=os.path.join(os.path.dirname(__file__), "static"),
112 cookie_secret=os.urandom(1024),
113 cookie_secret=os.urandom(1024),
113 login_url="/login",
114 login_url="/login",
114 )
115 )
115
116
116 # allow custom overrides for the tornado web app.
117 # allow custom overrides for the tornado web app.
117 settings.update(settings_overrides)
118 settings.update(settings_overrides)
118
119
119 super(NotebookWebApplication, self).__init__(handlers, **settings)
120 super(NotebookWebApplication, self).__init__(handlers, **settings)
120
121
121 self.kernel_manager = kernel_manager
122 self.kernel_manager = kernel_manager
122 self.log = log
123 self.log = log
123 self.notebook_manager = notebook_manager
124 self.notebook_manager = notebook_manager
124 self.ipython_app = ipython_app
125 self.ipython_app = ipython_app
125 self.read_only = self.ipython_app.read_only
126 self.read_only = self.ipython_app.read_only
126
127
127
128
128 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
129 # Aliases and Flags
130 # Aliases and Flags
130 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
131
132
132 flags = dict(ipkernel_flags)
133 flags = dict(ipkernel_flags)
133 flags['no-browser']=(
134 flags['no-browser']=(
134 {'NotebookApp' : {'open_browser' : False}},
135 {'NotebookApp' : {'open_browser' : False}},
135 "Don't open the notebook in a browser after startup."
136 "Don't open the notebook in a browser after startup."
136 )
137 )
137 flags['no-mathjax']=(
138 flags['no-mathjax']=(
138 {'NotebookApp' : {'enable_mathjax' : False}},
139 {'NotebookApp' : {'enable_mathjax' : False}},
139 """Disable MathJax
140 """Disable MathJax
140
141
141 MathJax is the javascript library IPython uses to render math/LaTeX. It is
142 MathJax is the javascript library IPython uses to render math/LaTeX. It is
142 very large, so you may want to disable it if you have a slow internet
143 very large, so you may want to disable it if you have a slow internet
143 connection, or for offline use of the notebook.
144 connection, or for offline use of the notebook.
144
145
145 When disabled, equations etc. will appear as their untransformed TeX source.
146 When disabled, equations etc. will appear as their untransformed TeX source.
146 """
147 """
147 )
148 )
148 flags['read-only'] = (
149 flags['read-only'] = (
149 {'NotebookApp' : {'read_only' : True}},
150 {'NotebookApp' : {'read_only' : True}},
150 """Allow read-only access to notebooks.
151 """Allow read-only access to notebooks.
151
152
152 When using a password to protect the notebook server, this flag
153 When using a password to protect the notebook server, this flag
153 allows unauthenticated clients to view the notebook list, and
154 allows unauthenticated clients to view the notebook list, and
154 individual notebooks, but not edit them, start kernels, or run
155 individual notebooks, but not edit them, start kernels, or run
155 code.
156 code.
156
157
157 If no password is set, the server will be entirely read-only.
158 If no password is set, the server will be entirely read-only.
158 """
159 """
159 )
160 )
160
161
161 # Add notebook manager flags
162 # Add notebook manager flags
162 flags.update(boolean_flag('script', 'NotebookManager.save_script',
163 flags.update(boolean_flag('script', 'NotebookManager.save_script',
163 'Auto-save a .py script everytime the .ipynb notebook is saved',
164 'Auto-save a .py script everytime the .ipynb notebook is saved',
164 'Do not auto-save .py scripts for every notebook'))
165 'Do not auto-save .py scripts for every notebook'))
165
166
166 # the flags that are specific to the frontend
167 # the flags that are specific to the frontend
167 # these must be scrubbed before being passed to the kernel,
168 # these must be scrubbed before being passed to the kernel,
168 # or it will raise an error on unrecognized flags
169 # or it will raise an error on unrecognized flags
169 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
170 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
170
171
171 aliases = dict(ipkernel_aliases)
172 aliases = dict(ipkernel_aliases)
172
173
173 aliases.update({
174 aliases.update({
174 'ip': 'NotebookApp.ip',
175 'ip': 'NotebookApp.ip',
175 'port': 'NotebookApp.port',
176 'port': 'NotebookApp.port',
176 'keyfile': 'NotebookApp.keyfile',
177 'keyfile': 'NotebookApp.keyfile',
177 'certfile': 'NotebookApp.certfile',
178 'certfile': 'NotebookApp.certfile',
178 'notebook-dir': 'NotebookManager.notebook_dir',
179 'notebook-dir': 'NotebookManager.notebook_dir',
179 })
180 })
180
181
181 # remove ipkernel flags that are singletons, and don't make sense in
182 # remove ipkernel flags that are singletons, and don't make sense in
182 # multi-kernel evironment:
183 # multi-kernel evironment:
183 aliases.pop('f', None)
184 aliases.pop('f', None)
184
185
185 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
186 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
186 u'notebook-dir']
187 u'notebook-dir']
187
188
188 #-----------------------------------------------------------------------------
189 #-----------------------------------------------------------------------------
189 # NotebookApp
190 # NotebookApp
190 #-----------------------------------------------------------------------------
191 #-----------------------------------------------------------------------------
191
192
192 class NotebookApp(BaseIPythonApplication):
193 class NotebookApp(BaseIPythonApplication):
193
194
194 name = 'ipython-notebook'
195 name = 'ipython-notebook'
195 default_config_file_name='ipython_notebook_config.py'
196 default_config_file_name='ipython_notebook_config.py'
196
197
197 description = """
198 description = """
198 The IPython HTML Notebook.
199 The IPython HTML Notebook.
199
200
200 This launches a Tornado based HTML Notebook Server that serves up an
201 This launches a Tornado based HTML Notebook Server that serves up an
201 HTML5/Javascript Notebook client.
202 HTML5/Javascript Notebook client.
202 """
203 """
203 examples = _examples
204 examples = _examples
204
205
205 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
206 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
206 MappingKernelManager, NotebookManager]
207 MappingKernelManager, NotebookManager]
207 flags = Dict(flags)
208 flags = Dict(flags)
208 aliases = Dict(aliases)
209 aliases = Dict(aliases)
209
210
210 kernel_argv = List(Unicode)
211 kernel_argv = List(Unicode)
211
212
212 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
213 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
213 default_value=logging.INFO,
214 default_value=logging.INFO,
214 config=True,
215 config=True,
215 help="Set the log level by value or name.")
216 help="Set the log level by value or name.")
216
217
217 # Network related information.
218 # Network related information.
218
219
219 ip = Unicode(LOCALHOST, config=True,
220 ip = Unicode(LOCALHOST, config=True,
220 help="The IP address the notebook server will listen on."
221 help="The IP address the notebook server will listen on."
221 )
222 )
222
223
223 def _ip_changed(self, name, old, new):
224 def _ip_changed(self, name, old, new):
224 if new == u'*': self.ip = u''
225 if new == u'*': self.ip = u''
225
226
226 port = Integer(8888, config=True,
227 port = Integer(8888, config=True,
227 help="The port the notebook server will listen on."
228 help="The port the notebook server will listen on."
228 )
229 )
229
230
230 certfile = Unicode(u'', config=True,
231 certfile = Unicode(u'', config=True,
231 help="""The full path to an SSL/TLS certificate file."""
232 help="""The full path to an SSL/TLS certificate file."""
232 )
233 )
233
234
234 keyfile = Unicode(u'', config=True,
235 keyfile = Unicode(u'', config=True,
235 help="""The full path to a private key file for usage with SSL/TLS."""
236 help="""The full path to a private key file for usage with SSL/TLS."""
236 )
237 )
237
238
238 password = Unicode(u'', config=True,
239 password = Unicode(u'', config=True,
239 help="""Hashed password to use for web authentication.
240 help="""Hashed password to use for web authentication.
240
241
241 To generate, type in a python/IPython shell:
242 To generate, type in a python/IPython shell:
242
243
243 from IPython.lib import passwd; passwd()
244 from IPython.lib import passwd; passwd()
244
245
245 The string should be of the form type:salt:hashed-password.
246 The string should be of the form type:salt:hashed-password.
246 """
247 """
247 )
248 )
248
249
249 open_browser = Bool(True, config=True,
250 open_browser = Bool(True, config=True,
250 help="Whether to open in a browser after starting.")
251 help="Whether to open in a browser after starting.")
251
252
252 read_only = Bool(False, config=True,
253 read_only = Bool(False, config=True,
253 help="Whether to prevent editing/execution of notebooks."
254 help="Whether to prevent editing/execution of notebooks."
254 )
255 )
255
256
256 webapp_settings = Dict(config=True,
257 webapp_settings = Dict(config=True,
257 help="Supply overrides for the tornado.web.Application that the "
258 help="Supply overrides for the tornado.web.Application that the "
258 "IPython notebook uses.")
259 "IPython notebook uses.")
259
260
260 enable_mathjax = Bool(True, config=True,
261 enable_mathjax = Bool(True, config=True,
261 help="""Whether to enable MathJax for typesetting math/TeX
262 help="""Whether to enable MathJax for typesetting math/TeX
262
263
263 MathJax is the javascript library IPython uses to render math/LaTeX. It is
264 MathJax is the javascript library IPython uses to render math/LaTeX. It is
264 very large, so you may want to disable it if you have a slow internet
265 very large, so you may want to disable it if you have a slow internet
265 connection, or for offline use of the notebook.
266 connection, or for offline use of the notebook.
266
267
267 When disabled, equations etc. will appear as their untransformed TeX source.
268 When disabled, equations etc. will appear as their untransformed TeX source.
268 """
269 """
269 )
270 )
270 def _enable_mathjax_changed(self, name, old, new):
271 def _enable_mathjax_changed(self, name, old, new):
271 """set mathjax url to empty if mathjax is disabled"""
272 """set mathjax url to empty if mathjax is disabled"""
272 if not new:
273 if not new:
273 self.mathjax_url = u''
274 self.mathjax_url = u''
274
275
275 mathjax_url = Unicode("", config=True,
276 mathjax_url = Unicode("", config=True,
276 help="""The url for MathJax.js."""
277 help="""The url for MathJax.js."""
277 )
278 )
278 def _mathjax_url_default(self):
279 def _mathjax_url_default(self):
279 if not self.enable_mathjax:
280 if not self.enable_mathjax:
280 return u''
281 return u''
281 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
282 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
282 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
283 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
283 self.log.info("Using local MathJax")
284 self.log.info("Using local MathJax")
284 return u"static/mathjax/MathJax.js"
285 return u"static/mathjax/MathJax.js"
285 else:
286 else:
286 self.log.info("Using MathJax from CDN")
287 self.log.info("Using MathJax from CDN")
287 return u"http://cdn.mathjax.org/mathjax/latest/MathJax.js"
288 return u"http://cdn.mathjax.org/mathjax/latest/MathJax.js"
288
289
289 def _mathjax_url_changed(self, name, old, new):
290 def _mathjax_url_changed(self, name, old, new):
290 if new and not self.enable_mathjax:
291 if new and not self.enable_mathjax:
291 # enable_mathjax=False overrides mathjax_url
292 # enable_mathjax=False overrides mathjax_url
292 self.mathjax_url = u''
293 self.mathjax_url = u''
293 else:
294 else:
294 self.log.info("Using MathJax: %s", new)
295 self.log.info("Using MathJax: %s", new)
295
296
296 def parse_command_line(self, argv=None):
297 def parse_command_line(self, argv=None):
297 super(NotebookApp, self).parse_command_line(argv)
298 super(NotebookApp, self).parse_command_line(argv)
298 if argv is None:
299 if argv is None:
299 argv = sys.argv[1:]
300 argv = sys.argv[1:]
300
301
301 # Scrub frontend-specific flags
302 # Scrub frontend-specific flags
302 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
303 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
303 # Kernel should inherit default config file from frontend
304 # Kernel should inherit default config file from frontend
304 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
305 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
305
306
306 def init_configurables(self):
307 def init_configurables(self):
307 # Don't let Qt or ZMQ swallow KeyboardInterupts.
308 # Don't let Qt or ZMQ swallow KeyboardInterupts.
308 signal.signal(signal.SIGINT, signal.SIG_DFL)
309 signal.signal(signal.SIGINT, signal.SIG_DFL)
309
310
310 # force Session default to be secure
311 # force Session default to be secure
311 default_secure(self.config)
312 default_secure(self.config)
312 # Create a KernelManager and start a kernel.
313 # Create a KernelManager and start a kernel.
313 self.kernel_manager = MappingKernelManager(
314 self.kernel_manager = MappingKernelManager(
314 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
315 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
315 connection_dir = self.profile_dir.security_dir,
316 connection_dir = self.profile_dir.security_dir,
316 )
317 )
317 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
318 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
318 self.notebook_manager.list_notebooks()
319 self.notebook_manager.list_notebooks()
319
320
320 def init_logging(self):
321 def init_logging(self):
321 super(NotebookApp, self).init_logging()
322 super(NotebookApp, self).init_logging()
322 # This prevents double log messages because tornado use a root logger that
323 # This prevents double log messages because tornado use a root logger that
323 # self.log is a child of. The logging module dipatches log messages to a log
324 # self.log is a child of. The logging module dipatches log messages to a log
324 # and all of its ancenstors until propagate is set to False.
325 # and all of its ancenstors until propagate is set to False.
325 self.log.propagate = False
326 self.log.propagate = False
326
327
327 @catch_config_error
328 @catch_config_error
328 def initialize(self, argv=None):
329 def initialize(self, argv=None):
329 super(NotebookApp, self).initialize(argv)
330 super(NotebookApp, self).initialize(argv)
330 self.init_configurables()
331 self.init_configurables()
331 self.web_app = NotebookWebApplication(
332 self.web_app = NotebookWebApplication(
332 self, self.kernel_manager, self.notebook_manager, self.log,
333 self, self.kernel_manager, self.notebook_manager, self.log,
333 self.webapp_settings
334 self.webapp_settings
334 )
335 )
335 if self.certfile:
336 if self.certfile:
336 ssl_options = dict(certfile=self.certfile)
337 ssl_options = dict(certfile=self.certfile)
337 if self.keyfile:
338 if self.keyfile:
338 ssl_options['keyfile'] = self.keyfile
339 ssl_options['keyfile'] = self.keyfile
339 else:
340 else:
340 ssl_options = None
341 ssl_options = None
341 self.web_app.password = self.password
342 self.web_app.password = self.password
342 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
343 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
343 if ssl_options is None and not self.ip:
344 if ssl_options is None and not self.ip:
344 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
345 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
345 'but not using any encryption or authentication. This is highly '
346 'but not using any encryption or authentication. This is highly '
346 'insecure and not recommended.')
347 'insecure and not recommended.')
347
348
348 # Try random ports centered around the default.
349 # Try random ports centered around the default.
349 from random import randint
350 from random import randint
350 n = 50 # Max number of attempts, keep reasonably large.
351 n = 50 # Max number of attempts, keep reasonably large.
351 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
352 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
352 try:
353 try:
353 self.http_server.listen(port, self.ip)
354 self.http_server.listen(port, self.ip)
354 except socket.error, e:
355 except socket.error, e:
355 if e.errno != errno.EADDRINUSE:
356 if e.errno != errno.EADDRINUSE:
356 raise
357 raise
357 self.log.info('The port %i is already in use, trying another random port.' % port)
358 self.log.info('The port %i is already in use, trying another random port.' % port)
358 else:
359 else:
359 self.port = port
360 self.port = port
360 break
361 break
361
362
362 def start(self):
363 def start(self):
363 ip = self.ip if self.ip else '[all ip addresses on your system]'
364 ip = self.ip if self.ip else '[all ip addresses on your system]'
364 proto = 'https' if self.certfile else 'http'
365 proto = 'https' if self.certfile else 'http'
365 info = self.log.info
366 info = self.log.info
366 info("The IPython Notebook is running at: %s://%s:%i" %
367 info("The IPython Notebook is running at: %s://%s:%i" %
367 (proto, ip, self.port) )
368 (proto, ip, self.port) )
368 info("Use Control-C to stop this server and shut down all kernels.")
369 info("Use Control-C to stop this server and shut down all kernels.")
369
370
370 if self.open_browser:
371 if self.open_browser:
371 ip = self.ip or '127.0.0.1'
372 ip = self.ip or '127.0.0.1'
372 b = lambda : webbrowser.open("%s://%s:%i" % (proto, ip, self.port),
373 b = lambda : webbrowser.open("%s://%s:%i" % (proto, ip, self.port),
373 new=2)
374 new=2)
374 threading.Thread(target=b).start()
375 threading.Thread(target=b).start()
375
376
376 ioloop.IOLoop.instance().start()
377 ioloop.IOLoop.instance().start()
377
378
378 #-----------------------------------------------------------------------------
379 #-----------------------------------------------------------------------------
379 # Main entry point
380 # Main entry point
380 #-----------------------------------------------------------------------------
381 #-----------------------------------------------------------------------------
381
382
382 def launch_new_instance():
383 def launch_new_instance():
383 app = NotebookApp()
384 app = NotebookApp()
384 app.initialize()
385 app.initialize()
385 app.start()
386 app.start()
386
387
General Comments 0
You need to be logged in to leave comments. Login now