##// END OF EJS Templates
Backport PR #2073: allows password and prefix for notebook...
MinRK -
Show More
@@ -1,738 +1,738 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 time
21 import time
22 import uuid
22 import uuid
23
23
24 from tornado import web
24 from tornado import web
25 from tornado import websocket
25 from tornado import websocket
26
26
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
29
29
30 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
33 from IPython.utils.jsonutil import date_default
33 from IPython.utils.jsonutil import date_default
34
34
35 try:
35 try:
36 from docutils.core import publish_string
36 from docutils.core import publish_string
37 except ImportError:
37 except ImportError:
38 publish_string = None
38 publish_string = None
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # parts tornado cares about haven't really changed, so it's OK to continue
45 # parts tornado cares about haven't really changed, so it's OK to continue
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # version as of Oct 30/2011) the version check fails, see the issue report:
47 # version as of Oct 30/2011) the version check fails, see the issue report:
48
48
49 # https://github.com/facebook/tornado/issues/385
49 # https://github.com/facebook/tornado/issues/385
50
50
51 # This issue has been fixed in Tornado post 2.1.1:
51 # This issue has been fixed in Tornado post 2.1.1:
52
52
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54
54
55 # Here we manually apply the same patch as above so that users of IPython can
55 # Here we manually apply the same patch as above so that users of IPython can
56 # continue to work with an officially released Tornado. We make the
56 # continue to work with an officially released Tornado. We make the
57 # monkeypatch version check as narrow as possible to limit its effects; once
57 # monkeypatch version check as narrow as possible to limit its effects; once
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59
59
60 import tornado
60 import tornado
61
61
62 if tornado.version_info <= (2,1,1):
62 if tornado.version_info <= (2,1,1):
63
63
64 def _execute(self, transforms, *args, **kwargs):
64 def _execute(self, transforms, *args, **kwargs):
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66
66
67 self.open_args = args
67 self.open_args = args
68 self.open_kwargs = kwargs
68 self.open_kwargs = kwargs
69
69
70 # The difference between version 8 and 13 is that in 8 the
70 # The difference between version 8 and 13 is that in 8 the
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # simply "Origin".
72 # simply "Origin".
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection.accept_connection()
75 self.ws_connection.accept_connection()
76
76
77 elif self.request.headers.get("Sec-WebSocket-Version"):
77 elif self.request.headers.get("Sec-WebSocket-Version"):
78 self.stream.write(tornado.escape.utf8(
78 self.stream.write(tornado.escape.utf8(
79 "HTTP/1.1 426 Upgrade Required\r\n"
79 "HTTP/1.1 426 Upgrade Required\r\n"
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 self.stream.close()
81 self.stream.close()
82
82
83 else:
83 else:
84 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 websocket.WebSocketHandler._execute = _execute
87 websocket.WebSocketHandler._execute = _execute
88 del _execute
88 del _execute
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Decorator for disabling read-only handlers
91 # Decorator for disabling read-only handlers
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94 @decorator
94 @decorator
95 def not_if_readonly(f, self, *args, **kwargs):
95 def not_if_readonly(f, self, *args, **kwargs):
96 if self.application.read_only:
96 if self.application.read_only:
97 raise web.HTTPError(403, "Notebook server is read-only")
97 raise web.HTTPError(403, "Notebook server is read-only")
98 else:
98 else:
99 return f(self, *args, **kwargs)
99 return f(self, *args, **kwargs)
100
100
101 @decorator
101 @decorator
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 """authenticate this page *unless* readonly view is active.
103 """authenticate this page *unless* readonly view is active.
104
104
105 In read-only mode, the notebook list and print view should
105 In read-only mode, the notebook list and print view should
106 be accessible without authentication.
106 be accessible without authentication.
107 """
107 """
108
108
109 @web.authenticated
109 @web.authenticated
110 def auth_f(self, *args, **kwargs):
110 def auth_f(self, *args, **kwargs):
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112
112
113 if self.application.read_only:
113 if self.application.read_only:
114 return f(self, *args, **kwargs)
114 return f(self, *args, **kwargs)
115 else:
115 else:
116 return auth_f(self, *args, **kwargs)
116 return auth_f(self, *args, **kwargs)
117
117
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119 # Top-level handlers
119 # Top-level handlers
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121
121
122 class RequestHandler(web.RequestHandler):
122 class RequestHandler(web.RequestHandler):
123 """RequestHandler with default variable setting."""
123 """RequestHandler with default variable setting."""
124
124
125 def render(*args, **kwargs):
125 def render(*args, **kwargs):
126 kwargs.setdefault('message', '')
126 kwargs.setdefault('message', '')
127 return web.RequestHandler.render(*args, **kwargs)
127 return web.RequestHandler.render(*args, **kwargs)
128
128
129 class AuthenticatedHandler(RequestHandler):
129 class AuthenticatedHandler(RequestHandler):
130 """A RequestHandler with an authenticated user."""
130 """A RequestHandler with an authenticated user."""
131
131
132 def get_current_user(self):
132 def get_current_user(self):
133 user_id = self.get_secure_cookie("username")
133 user_id = self.get_secure_cookie("username")
134 # For now the user_id should not return empty, but it could eventually
134 # For now the user_id should not return empty, but it could eventually
135 if user_id == '':
135 if user_id == '':
136 user_id = 'anonymous'
136 user_id = 'anonymous'
137 if user_id is None:
137 if user_id is None:
138 # prevent extra Invalid cookie sig warnings:
138 # prevent extra Invalid cookie sig warnings:
139 self.clear_cookie('username')
139 self.clear_cookie('username')
140 if not self.application.password and not self.application.read_only:
140 if not self.application.password and not self.application.read_only:
141 user_id = 'anonymous'
141 user_id = 'anonymous'
142 return user_id
142 return user_id
143
143
144 @property
144 @property
145 def logged_in(self):
145 def logged_in(self):
146 """Is a user currently logged in?
146 """Is a user currently logged in?
147
147
148 """
148 """
149 user = self.get_current_user()
149 user = self.get_current_user()
150 return (user and not user == 'anonymous')
150 return (user and not user == 'anonymous')
151
151
152 @property
152 @property
153 def login_available(self):
153 def login_available(self):
154 """May a user proceed to log in?
154 """May a user proceed to log in?
155
155
156 This returns True if login capability is available, irrespective of
156 This returns True if login capability is available, irrespective of
157 whether the user is already logged in or not.
157 whether the user is already logged in or not.
158
158
159 """
159 """
160 return bool(self.application.password)
160 return bool(self.application.password)
161
161
162 @property
162 @property
163 def read_only(self):
163 def read_only(self):
164 """Is the notebook read-only?
164 """Is the notebook read-only?
165
165
166 """
166 """
167 return self.application.read_only
167 return self.application.read_only
168
168
169 @property
169 @property
170 def ws_url(self):
170 def ws_url(self):
171 """websocket url matching the current request
171 """websocket url matching the current request
172
172
173 turns http[s]://host[:port] into
173 turns http[s]://host[:port] into
174 ws[s]://host[:port]
174 ws[s]://host[:port]
175 """
175 """
176 proto = self.request.protocol.replace('http', 'ws')
176 proto = self.request.protocol.replace('http', 'ws')
177 host = self.application.ipython_app.websocket_host # default to config value
177 host = self.application.ipython_app.websocket_host # default to config value
178 if host == '':
178 if host == '':
179 host = self.request.host # get from request
179 host = self.request.host # get from request
180 return "%s://%s" % (proto, host)
180 return "%s://%s" % (proto, host)
181
181
182
182
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
184 """static files should only be accessible when logged in"""
184 """static files should only be accessible when logged in"""
185
185
186 @authenticate_unless_readonly
186 @authenticate_unless_readonly
187 def get(self, path):
187 def get(self, path):
188 return web.StaticFileHandler.get(self, path)
188 return web.StaticFileHandler.get(self, path)
189
189
190
190
191 class ProjectDashboardHandler(AuthenticatedHandler):
191 class ProjectDashboardHandler(AuthenticatedHandler):
192
192
193 @authenticate_unless_readonly
193 @authenticate_unless_readonly
194 def get(self):
194 def get(self):
195 nbm = self.application.notebook_manager
195 nbm = self.application.notebook_manager
196 project = nbm.notebook_dir
196 project = nbm.notebook_dir
197 self.render(
197 self.render(
198 'projectdashboard.html', project=project,
198 'projectdashboard.html', project=project,
199 base_project_url=self.application.ipython_app.base_project_url,
199 base_project_url=self.application.ipython_app.base_project_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
201 read_only=self.read_only,
201 read_only=self.read_only,
202 logged_in=self.logged_in,
202 logged_in=self.logged_in,
203 login_available=self.login_available
203 login_available=self.login_available
204 )
204 )
205
205
206
206
207 class LoginHandler(AuthenticatedHandler):
207 class LoginHandler(AuthenticatedHandler):
208
208
209 def _render(self, message=None):
209 def _render(self, message=None):
210 self.render('login.html',
210 self.render('login.html',
211 next=self.get_argument('next', default='/'),
211 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
212 read_only=self.read_only,
212 read_only=self.read_only,
213 logged_in=self.logged_in,
213 logged_in=self.logged_in,
214 login_available=self.login_available,
214 login_available=self.login_available,
215 base_project_url=self.application.ipython_app.base_project_url,
215 base_project_url=self.application.ipython_app.base_project_url,
216 message=message
216 message=message
217 )
217 )
218
218
219 def get(self):
219 def get(self):
220 if self.current_user:
220 if self.current_user:
221 self.redirect(self.get_argument('next', default='/'))
221 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
222 else:
222 else:
223 self._render()
223 self._render()
224
224
225 def post(self):
225 def post(self):
226 pwd = self.get_argument('password', default=u'')
226 pwd = self.get_argument('password', default=u'')
227 if self.application.password:
227 if self.application.password:
228 if passwd_check(self.application.password, pwd):
228 if passwd_check(self.application.password, pwd):
229 self.set_secure_cookie('username', str(uuid.uuid4()))
229 self.set_secure_cookie('username', str(uuid.uuid4()))
230 else:
230 else:
231 self._render(message={'error': 'Invalid password'})
231 self._render(message={'error': 'Invalid password'})
232 return
232 return
233
233
234 self.redirect(self.get_argument('next', default='/'))
234 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
235
235
236
236
237 class LogoutHandler(AuthenticatedHandler):
237 class LogoutHandler(AuthenticatedHandler):
238
238
239 def get(self):
239 def get(self):
240 self.clear_cookie('username')
240 self.clear_cookie('username')
241 if self.login_available:
241 if self.login_available:
242 message = {'info': 'Successfully logged out.'}
242 message = {'info': 'Successfully logged out.'}
243 else:
243 else:
244 message = {'warning': 'Cannot log out. Notebook authentication '
244 message = {'warning': 'Cannot log out. Notebook authentication '
245 'is disabled.'}
245 'is disabled.'}
246
246
247 self.render('logout.html',
247 self.render('logout.html',
248 read_only=self.read_only,
248 read_only=self.read_only,
249 logged_in=self.logged_in,
249 logged_in=self.logged_in,
250 login_available=self.login_available,
250 login_available=self.login_available,
251 base_project_url=self.application.ipython_app.base_project_url,
251 base_project_url=self.application.ipython_app.base_project_url,
252 message=message)
252 message=message)
253
253
254
254
255 class NewHandler(AuthenticatedHandler):
255 class NewHandler(AuthenticatedHandler):
256
256
257 @web.authenticated
257 @web.authenticated
258 def get(self):
258 def get(self):
259 nbm = self.application.notebook_manager
259 nbm = self.application.notebook_manager
260 project = nbm.notebook_dir
260 project = nbm.notebook_dir
261 notebook_id = nbm.new_notebook()
261 notebook_id = nbm.new_notebook()
262 self.render(
262 self.render(
263 'notebook.html', project=project,
263 'notebook.html', project=project,
264 notebook_id=notebook_id,
264 notebook_id=notebook_id,
265 base_project_url=self.application.ipython_app.base_project_url,
265 base_project_url=self.application.ipython_app.base_project_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
267 kill_kernel=False,
267 kill_kernel=False,
268 read_only=False,
268 read_only=False,
269 logged_in=self.logged_in,
269 logged_in=self.logged_in,
270 login_available=self.login_available,
270 login_available=self.login_available,
271 mathjax_url=self.application.ipython_app.mathjax_url,
271 mathjax_url=self.application.ipython_app.mathjax_url,
272 )
272 )
273
273
274
274
275 class NamedNotebookHandler(AuthenticatedHandler):
275 class NamedNotebookHandler(AuthenticatedHandler):
276
276
277 @authenticate_unless_readonly
277 @authenticate_unless_readonly
278 def get(self, notebook_id):
278 def get(self, notebook_id):
279 nbm = self.application.notebook_manager
279 nbm = self.application.notebook_manager
280 project = nbm.notebook_dir
280 project = nbm.notebook_dir
281 if not nbm.notebook_exists(notebook_id):
281 if not nbm.notebook_exists(notebook_id):
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
283
283
284 self.render(
284 self.render(
285 'notebook.html', project=project,
285 'notebook.html', project=project,
286 notebook_id=notebook_id,
286 notebook_id=notebook_id,
287 base_project_url=self.application.ipython_app.base_project_url,
287 base_project_url=self.application.ipython_app.base_project_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
289 kill_kernel=False,
289 kill_kernel=False,
290 read_only=self.read_only,
290 read_only=self.read_only,
291 logged_in=self.logged_in,
291 logged_in=self.logged_in,
292 login_available=self.login_available,
292 login_available=self.login_available,
293 mathjax_url=self.application.ipython_app.mathjax_url,
293 mathjax_url=self.application.ipython_app.mathjax_url,
294 )
294 )
295
295
296
296
297 class PrintNotebookHandler(AuthenticatedHandler):
297 class PrintNotebookHandler(AuthenticatedHandler):
298
298
299 @authenticate_unless_readonly
299 @authenticate_unless_readonly
300 def get(self, notebook_id):
300 def get(self, notebook_id):
301 nbm = self.application.notebook_manager
301 nbm = self.application.notebook_manager
302 project = nbm.notebook_dir
302 project = nbm.notebook_dir
303 if not nbm.notebook_exists(notebook_id):
303 if not nbm.notebook_exists(notebook_id):
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
305
305
306 self.render(
306 self.render(
307 'printnotebook.html', project=project,
307 'printnotebook.html', project=project,
308 notebook_id=notebook_id,
308 notebook_id=notebook_id,
309 base_project_url=self.application.ipython_app.base_project_url,
309 base_project_url=self.application.ipython_app.base_project_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
311 kill_kernel=False,
311 kill_kernel=False,
312 read_only=self.read_only,
312 read_only=self.read_only,
313 logged_in=self.logged_in,
313 logged_in=self.logged_in,
314 login_available=self.login_available,
314 login_available=self.login_available,
315 mathjax_url=self.application.ipython_app.mathjax_url,
315 mathjax_url=self.application.ipython_app.mathjax_url,
316 )
316 )
317
317
318 #-----------------------------------------------------------------------------
318 #-----------------------------------------------------------------------------
319 # Kernel handlers
319 # Kernel handlers
320 #-----------------------------------------------------------------------------
320 #-----------------------------------------------------------------------------
321
321
322
322
323 class MainKernelHandler(AuthenticatedHandler):
323 class MainKernelHandler(AuthenticatedHandler):
324
324
325 @web.authenticated
325 @web.authenticated
326 def get(self):
326 def get(self):
327 km = self.application.kernel_manager
327 km = self.application.kernel_manager
328 self.finish(jsonapi.dumps(km.kernel_ids))
328 self.finish(jsonapi.dumps(km.kernel_ids))
329
329
330 @web.authenticated
330 @web.authenticated
331 def post(self):
331 def post(self):
332 km = self.application.kernel_manager
332 km = self.application.kernel_manager
333 nbm = self.application.notebook_manager
333 nbm = self.application.notebook_manager
334 notebook_id = self.get_argument('notebook', default=None)
334 notebook_id = self.get_argument('notebook', default=None)
335 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
335 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
336 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
336 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
337 self.set_header('Location', '/'+kernel_id)
337 self.set_header('Location', '/'+kernel_id)
338 self.finish(jsonapi.dumps(data))
338 self.finish(jsonapi.dumps(data))
339
339
340
340
341 class KernelHandler(AuthenticatedHandler):
341 class KernelHandler(AuthenticatedHandler):
342
342
343 SUPPORTED_METHODS = ('DELETE')
343 SUPPORTED_METHODS = ('DELETE')
344
344
345 @web.authenticated
345 @web.authenticated
346 def delete(self, kernel_id):
346 def delete(self, kernel_id):
347 km = self.application.kernel_manager
347 km = self.application.kernel_manager
348 km.shutdown_kernel(kernel_id)
348 km.shutdown_kernel(kernel_id)
349 self.set_status(204)
349 self.set_status(204)
350 self.finish()
350 self.finish()
351
351
352
352
353 class KernelActionHandler(AuthenticatedHandler):
353 class KernelActionHandler(AuthenticatedHandler):
354
354
355 @web.authenticated
355 @web.authenticated
356 def post(self, kernel_id, action):
356 def post(self, kernel_id, action):
357 km = self.application.kernel_manager
357 km = self.application.kernel_manager
358 if action == 'interrupt':
358 if action == 'interrupt':
359 km.interrupt_kernel(kernel_id)
359 km.interrupt_kernel(kernel_id)
360 self.set_status(204)
360 self.set_status(204)
361 if action == 'restart':
361 if action == 'restart':
362 new_kernel_id = km.restart_kernel(kernel_id)
362 new_kernel_id = km.restart_kernel(kernel_id)
363 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
363 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
364 self.set_header('Location', '/'+new_kernel_id)
364 self.set_header('Location', '/'+new_kernel_id)
365 self.write(jsonapi.dumps(data))
365 self.write(jsonapi.dumps(data))
366 self.finish()
366 self.finish()
367
367
368
368
369 class ZMQStreamHandler(websocket.WebSocketHandler):
369 class ZMQStreamHandler(websocket.WebSocketHandler):
370
370
371 def _reserialize_reply(self, msg_list):
371 def _reserialize_reply(self, msg_list):
372 """Reserialize a reply message using JSON.
372 """Reserialize a reply message using JSON.
373
373
374 This takes the msg list from the ZMQ socket, unserializes it using
374 This takes the msg list from the ZMQ socket, unserializes it using
375 self.session and then serializes the result using JSON. This method
375 self.session and then serializes the result using JSON. This method
376 should be used by self._on_zmq_reply to build messages that can
376 should be used by self._on_zmq_reply to build messages that can
377 be sent back to the browser.
377 be sent back to the browser.
378 """
378 """
379 idents, msg_list = self.session.feed_identities(msg_list)
379 idents, msg_list = self.session.feed_identities(msg_list)
380 msg = self.session.unserialize(msg_list)
380 msg = self.session.unserialize(msg_list)
381 try:
381 try:
382 msg['header'].pop('date')
382 msg['header'].pop('date')
383 except KeyError:
383 except KeyError:
384 pass
384 pass
385 try:
385 try:
386 msg['parent_header'].pop('date')
386 msg['parent_header'].pop('date')
387 except KeyError:
387 except KeyError:
388 pass
388 pass
389 msg.pop('buffers')
389 msg.pop('buffers')
390 return jsonapi.dumps(msg, default=date_default)
390 return jsonapi.dumps(msg, default=date_default)
391
391
392 def _on_zmq_reply(self, msg_list):
392 def _on_zmq_reply(self, msg_list):
393 try:
393 try:
394 msg = self._reserialize_reply(msg_list)
394 msg = self._reserialize_reply(msg_list)
395 except Exception:
395 except Exception:
396 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
396 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
397 else:
397 else:
398 self.write_message(msg)
398 self.write_message(msg)
399
399
400 def allow_draft76(self):
400 def allow_draft76(self):
401 """Allow draft 76, until browsers such as Safari update to RFC 6455.
401 """Allow draft 76, until browsers such as Safari update to RFC 6455.
402
402
403 This has been disabled by default in tornado in release 2.2.0, and
403 This has been disabled by default in tornado in release 2.2.0, and
404 support will be removed in later versions.
404 support will be removed in later versions.
405 """
405 """
406 return True
406 return True
407
407
408
408
409 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
409 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
410
410
411 def open(self, kernel_id):
411 def open(self, kernel_id):
412 self.kernel_id = kernel_id.decode('ascii')
412 self.kernel_id = kernel_id.decode('ascii')
413 try:
413 try:
414 cfg = self.application.ipython_app.config
414 cfg = self.application.ipython_app.config
415 except AttributeError:
415 except AttributeError:
416 # protect from the case where this is run from something other than
416 # protect from the case where this is run from something other than
417 # the notebook app:
417 # the notebook app:
418 cfg = None
418 cfg = None
419 self.session = Session(config=cfg)
419 self.session = Session(config=cfg)
420 self.save_on_message = self.on_message
420 self.save_on_message = self.on_message
421 self.on_message = self.on_first_message
421 self.on_message = self.on_first_message
422
422
423 def get_current_user(self):
423 def get_current_user(self):
424 user_id = self.get_secure_cookie("username")
424 user_id = self.get_secure_cookie("username")
425 if user_id == '' or (user_id is None and not self.application.password):
425 if user_id == '' or (user_id is None and not self.application.password):
426 user_id = 'anonymous'
426 user_id = 'anonymous'
427 return user_id
427 return user_id
428
428
429 def _inject_cookie_message(self, msg):
429 def _inject_cookie_message(self, msg):
430 """Inject the first message, which is the document cookie,
430 """Inject the first message, which is the document cookie,
431 for authentication."""
431 for authentication."""
432 if isinstance(msg, unicode):
432 if isinstance(msg, unicode):
433 # Cookie can't constructor doesn't accept unicode strings for some reason
433 # Cookie can't constructor doesn't accept unicode strings for some reason
434 msg = msg.encode('utf8', 'replace')
434 msg = msg.encode('utf8', 'replace')
435 try:
435 try:
436 self.request._cookies = Cookie.SimpleCookie(msg)
436 self.request._cookies = Cookie.SimpleCookie(msg)
437 except:
437 except:
438 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
438 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
439
439
440 def on_first_message(self, msg):
440 def on_first_message(self, msg):
441 self._inject_cookie_message(msg)
441 self._inject_cookie_message(msg)
442 if self.get_current_user() is None:
442 if self.get_current_user() is None:
443 logging.warn("Couldn't authenticate WebSocket connection")
443 logging.warn("Couldn't authenticate WebSocket connection")
444 raise web.HTTPError(403)
444 raise web.HTTPError(403)
445 self.on_message = self.save_on_message
445 self.on_message = self.save_on_message
446
446
447
447
448 class IOPubHandler(AuthenticatedZMQStreamHandler):
448 class IOPubHandler(AuthenticatedZMQStreamHandler):
449
449
450 def initialize(self, *args, **kwargs):
450 def initialize(self, *args, **kwargs):
451 self._kernel_alive = True
451 self._kernel_alive = True
452 self._beating = False
452 self._beating = False
453 self.iopub_stream = None
453 self.iopub_stream = None
454 self.hb_stream = None
454 self.hb_stream = None
455
455
456 def on_first_message(self, msg):
456 def on_first_message(self, msg):
457 try:
457 try:
458 super(IOPubHandler, self).on_first_message(msg)
458 super(IOPubHandler, self).on_first_message(msg)
459 except web.HTTPError:
459 except web.HTTPError:
460 self.close()
460 self.close()
461 return
461 return
462 km = self.application.kernel_manager
462 km = self.application.kernel_manager
463 self.time_to_dead = km.time_to_dead
463 self.time_to_dead = km.time_to_dead
464 self.first_beat = km.first_beat
464 self.first_beat = km.first_beat
465 kernel_id = self.kernel_id
465 kernel_id = self.kernel_id
466 try:
466 try:
467 self.iopub_stream = km.create_iopub_stream(kernel_id)
467 self.iopub_stream = km.create_iopub_stream(kernel_id)
468 self.hb_stream = km.create_hb_stream(kernel_id)
468 self.hb_stream = km.create_hb_stream(kernel_id)
469 except web.HTTPError:
469 except web.HTTPError:
470 # WebSockets don't response to traditional error codes so we
470 # WebSockets don't response to traditional error codes so we
471 # close the connection.
471 # close the connection.
472 if not self.stream.closed():
472 if not self.stream.closed():
473 self.stream.close()
473 self.stream.close()
474 self.close()
474 self.close()
475 else:
475 else:
476 self.iopub_stream.on_recv(self._on_zmq_reply)
476 self.iopub_stream.on_recv(self._on_zmq_reply)
477 self.start_hb(self.kernel_died)
477 self.start_hb(self.kernel_died)
478
478
479 def on_message(self, msg):
479 def on_message(self, msg):
480 pass
480 pass
481
481
482 def on_close(self):
482 def on_close(self):
483 # This method can be called twice, once by self.kernel_died and once
483 # This method can be called twice, once by self.kernel_died and once
484 # from the WebSocket close event. If the WebSocket connection is
484 # from the WebSocket close event. If the WebSocket connection is
485 # closed before the ZMQ streams are setup, they could be None.
485 # closed before the ZMQ streams are setup, they could be None.
486 self.stop_hb()
486 self.stop_hb()
487 if self.iopub_stream is not None and not self.iopub_stream.closed():
487 if self.iopub_stream is not None and not self.iopub_stream.closed():
488 self.iopub_stream.on_recv(None)
488 self.iopub_stream.on_recv(None)
489 self.iopub_stream.close()
489 self.iopub_stream.close()
490 if self.hb_stream is not None and not self.hb_stream.closed():
490 if self.hb_stream is not None and not self.hb_stream.closed():
491 self.hb_stream.close()
491 self.hb_stream.close()
492
492
493 def start_hb(self, callback):
493 def start_hb(self, callback):
494 """Start the heartbeating and call the callback if the kernel dies."""
494 """Start the heartbeating and call the callback if the kernel dies."""
495 if not self._beating:
495 if not self._beating:
496 self._kernel_alive = True
496 self._kernel_alive = True
497
497
498 def ping_or_dead():
498 def ping_or_dead():
499 self.hb_stream.flush()
499 self.hb_stream.flush()
500 if self._kernel_alive:
500 if self._kernel_alive:
501 self._kernel_alive = False
501 self._kernel_alive = False
502 self.hb_stream.send(b'ping')
502 self.hb_stream.send(b'ping')
503 # flush stream to force immediate socket send
503 # flush stream to force immediate socket send
504 self.hb_stream.flush()
504 self.hb_stream.flush()
505 else:
505 else:
506 try:
506 try:
507 callback()
507 callback()
508 except:
508 except:
509 pass
509 pass
510 finally:
510 finally:
511 self.stop_hb()
511 self.stop_hb()
512
512
513 def beat_received(msg):
513 def beat_received(msg):
514 self._kernel_alive = True
514 self._kernel_alive = True
515
515
516 self.hb_stream.on_recv(beat_received)
516 self.hb_stream.on_recv(beat_received)
517 loop = ioloop.IOLoop.instance()
517 loop = ioloop.IOLoop.instance()
518 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
518 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
519 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
519 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
520 self._beating= True
520 self._beating= True
521
521
522 def _really_start_hb(self):
522 def _really_start_hb(self):
523 """callback for delayed heartbeat start
523 """callback for delayed heartbeat start
524
524
525 Only start the hb loop if we haven't been closed during the wait.
525 Only start the hb loop if we haven't been closed during the wait.
526 """
526 """
527 if self._beating and not self.hb_stream.closed():
527 if self._beating and not self.hb_stream.closed():
528 self._hb_periodic_callback.start()
528 self._hb_periodic_callback.start()
529
529
530 def stop_hb(self):
530 def stop_hb(self):
531 """Stop the heartbeating and cancel all related callbacks."""
531 """Stop the heartbeating and cancel all related callbacks."""
532 if self._beating:
532 if self._beating:
533 self._beating = False
533 self._beating = False
534 self._hb_periodic_callback.stop()
534 self._hb_periodic_callback.stop()
535 if not self.hb_stream.closed():
535 if not self.hb_stream.closed():
536 self.hb_stream.on_recv(None)
536 self.hb_stream.on_recv(None)
537
537
538 def kernel_died(self):
538 def kernel_died(self):
539 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
539 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
540 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
540 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
541 self.write_message(
541 self.write_message(
542 {'header': {'msg_type': 'status'},
542 {'header': {'msg_type': 'status'},
543 'parent_header': {},
543 'parent_header': {},
544 'content': {'execution_state':'dead'}
544 'content': {'execution_state':'dead'}
545 }
545 }
546 )
546 )
547 self.on_close()
547 self.on_close()
548
548
549
549
550 class ShellHandler(AuthenticatedZMQStreamHandler):
550 class ShellHandler(AuthenticatedZMQStreamHandler):
551
551
552 def initialize(self, *args, **kwargs):
552 def initialize(self, *args, **kwargs):
553 self.shell_stream = None
553 self.shell_stream = None
554
554
555 def on_first_message(self, msg):
555 def on_first_message(self, msg):
556 try:
556 try:
557 super(ShellHandler, self).on_first_message(msg)
557 super(ShellHandler, self).on_first_message(msg)
558 except web.HTTPError:
558 except web.HTTPError:
559 self.close()
559 self.close()
560 return
560 return
561 km = self.application.kernel_manager
561 km = self.application.kernel_manager
562 self.max_msg_size = km.max_msg_size
562 self.max_msg_size = km.max_msg_size
563 kernel_id = self.kernel_id
563 kernel_id = self.kernel_id
564 try:
564 try:
565 self.shell_stream = km.create_shell_stream(kernel_id)
565 self.shell_stream = km.create_shell_stream(kernel_id)
566 except web.HTTPError:
566 except web.HTTPError:
567 # WebSockets don't response to traditional error codes so we
567 # WebSockets don't response to traditional error codes so we
568 # close the connection.
568 # close the connection.
569 if not self.stream.closed():
569 if not self.stream.closed():
570 self.stream.close()
570 self.stream.close()
571 self.close()
571 self.close()
572 else:
572 else:
573 self.shell_stream.on_recv(self._on_zmq_reply)
573 self.shell_stream.on_recv(self._on_zmq_reply)
574
574
575 def on_message(self, msg):
575 def on_message(self, msg):
576 if len(msg) < self.max_msg_size:
576 if len(msg) < self.max_msg_size:
577 msg = jsonapi.loads(msg)
577 msg = jsonapi.loads(msg)
578 self.session.send(self.shell_stream, msg)
578 self.session.send(self.shell_stream, msg)
579
579
580 def on_close(self):
580 def on_close(self):
581 # Make sure the stream exists and is not already closed.
581 # Make sure the stream exists and is not already closed.
582 if self.shell_stream is not None and not self.shell_stream.closed():
582 if self.shell_stream is not None and not self.shell_stream.closed():
583 self.shell_stream.close()
583 self.shell_stream.close()
584
584
585
585
586 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
587 # Notebook web service handlers
587 # Notebook web service handlers
588 #-----------------------------------------------------------------------------
588 #-----------------------------------------------------------------------------
589
589
590 class NotebookRootHandler(AuthenticatedHandler):
590 class NotebookRootHandler(AuthenticatedHandler):
591
591
592 @authenticate_unless_readonly
592 @authenticate_unless_readonly
593 def get(self):
593 def get(self):
594 nbm = self.application.notebook_manager
594 nbm = self.application.notebook_manager
595 km = self.application.kernel_manager
595 km = self.application.kernel_manager
596 files = nbm.list_notebooks()
596 files = nbm.list_notebooks()
597 for f in files :
597 for f in files :
598 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
598 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
599 self.finish(jsonapi.dumps(files))
599 self.finish(jsonapi.dumps(files))
600
600
601 @web.authenticated
601 @web.authenticated
602 def post(self):
602 def post(self):
603 nbm = self.application.notebook_manager
603 nbm = self.application.notebook_manager
604 body = self.request.body.strip()
604 body = self.request.body.strip()
605 format = self.get_argument('format', default='json')
605 format = self.get_argument('format', default='json')
606 name = self.get_argument('name', default=None)
606 name = self.get_argument('name', default=None)
607 if body:
607 if body:
608 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
608 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
609 else:
609 else:
610 notebook_id = nbm.new_notebook()
610 notebook_id = nbm.new_notebook()
611 self.set_header('Location', '/'+notebook_id)
611 self.set_header('Location', '/'+notebook_id)
612 self.finish(jsonapi.dumps(notebook_id))
612 self.finish(jsonapi.dumps(notebook_id))
613
613
614
614
615 class NotebookHandler(AuthenticatedHandler):
615 class NotebookHandler(AuthenticatedHandler):
616
616
617 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
617 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
618
618
619 @authenticate_unless_readonly
619 @authenticate_unless_readonly
620 def get(self, notebook_id):
620 def get(self, notebook_id):
621 nbm = self.application.notebook_manager
621 nbm = self.application.notebook_manager
622 format = self.get_argument('format', default='json')
622 format = self.get_argument('format', default='json')
623 last_mod, name, data = nbm.get_notebook(notebook_id, format)
623 last_mod, name, data = nbm.get_notebook(notebook_id, format)
624
624
625 if format == u'json':
625 if format == u'json':
626 self.set_header('Content-Type', 'application/json')
626 self.set_header('Content-Type', 'application/json')
627 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
627 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
628 elif format == u'py':
628 elif format == u'py':
629 self.set_header('Content-Type', 'application/x-python')
629 self.set_header('Content-Type', 'application/x-python')
630 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
630 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
631 self.set_header('Last-Modified', last_mod)
631 self.set_header('Last-Modified', last_mod)
632 self.finish(data)
632 self.finish(data)
633
633
634 @web.authenticated
634 @web.authenticated
635 def put(self, notebook_id):
635 def put(self, notebook_id):
636 nbm = self.application.notebook_manager
636 nbm = self.application.notebook_manager
637 format = self.get_argument('format', default='json')
637 format = self.get_argument('format', default='json')
638 name = self.get_argument('name', default=None)
638 name = self.get_argument('name', default=None)
639 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
639 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
640 self.set_status(204)
640 self.set_status(204)
641 self.finish()
641 self.finish()
642
642
643 @web.authenticated
643 @web.authenticated
644 def delete(self, notebook_id):
644 def delete(self, notebook_id):
645 nbm = self.application.notebook_manager
645 nbm = self.application.notebook_manager
646 nbm.delete_notebook(notebook_id)
646 nbm.delete_notebook(notebook_id)
647 self.set_status(204)
647 self.set_status(204)
648 self.finish()
648 self.finish()
649
649
650
650
651 class NotebookCopyHandler(AuthenticatedHandler):
651 class NotebookCopyHandler(AuthenticatedHandler):
652
652
653 @web.authenticated
653 @web.authenticated
654 def get(self, notebook_id):
654 def get(self, notebook_id):
655 nbm = self.application.notebook_manager
655 nbm = self.application.notebook_manager
656 project = nbm.notebook_dir
656 project = nbm.notebook_dir
657 notebook_id = nbm.copy_notebook(notebook_id)
657 notebook_id = nbm.copy_notebook(notebook_id)
658 self.render(
658 self.render(
659 'notebook.html', project=project,
659 'notebook.html', project=project,
660 notebook_id=notebook_id,
660 notebook_id=notebook_id,
661 base_project_url=self.application.ipython_app.base_project_url,
661 base_project_url=self.application.ipython_app.base_project_url,
662 base_kernel_url=self.application.ipython_app.base_kernel_url,
662 base_kernel_url=self.application.ipython_app.base_kernel_url,
663 kill_kernel=False,
663 kill_kernel=False,
664 read_only=False,
664 read_only=False,
665 logged_in=self.logged_in,
665 logged_in=self.logged_in,
666 login_available=self.login_available,
666 login_available=self.login_available,
667 mathjax_url=self.application.ipython_app.mathjax_url,
667 mathjax_url=self.application.ipython_app.mathjax_url,
668 )
668 )
669
669
670
670
671 #-----------------------------------------------------------------------------
671 #-----------------------------------------------------------------------------
672 # Cluster handlers
672 # Cluster handlers
673 #-----------------------------------------------------------------------------
673 #-----------------------------------------------------------------------------
674
674
675
675
676 class MainClusterHandler(AuthenticatedHandler):
676 class MainClusterHandler(AuthenticatedHandler):
677
677
678 @web.authenticated
678 @web.authenticated
679 def get(self):
679 def get(self):
680 cm = self.application.cluster_manager
680 cm = self.application.cluster_manager
681 self.finish(jsonapi.dumps(cm.list_profiles()))
681 self.finish(jsonapi.dumps(cm.list_profiles()))
682
682
683
683
684 class ClusterProfileHandler(AuthenticatedHandler):
684 class ClusterProfileHandler(AuthenticatedHandler):
685
685
686 @web.authenticated
686 @web.authenticated
687 def get(self, profile):
687 def get(self, profile):
688 cm = self.application.cluster_manager
688 cm = self.application.cluster_manager
689 self.finish(jsonapi.dumps(cm.profile_info(profile)))
689 self.finish(jsonapi.dumps(cm.profile_info(profile)))
690
690
691
691
692 class ClusterActionHandler(AuthenticatedHandler):
692 class ClusterActionHandler(AuthenticatedHandler):
693
693
694 @web.authenticated
694 @web.authenticated
695 def post(self, profile, action):
695 def post(self, profile, action):
696 cm = self.application.cluster_manager
696 cm = self.application.cluster_manager
697 if action == 'start':
697 if action == 'start':
698 n = self.get_argument('n',default=None)
698 n = self.get_argument('n',default=None)
699 if n is None:
699 if n is None:
700 data = cm.start_cluster(profile)
700 data = cm.start_cluster(profile)
701 else:
701 else:
702 data = cm.start_cluster(profile,int(n))
702 data = cm.start_cluster(profile,int(n))
703 if action == 'stop':
703 if action == 'stop':
704 data = cm.stop_cluster(profile)
704 data = cm.stop_cluster(profile)
705 self.finish(jsonapi.dumps(data))
705 self.finish(jsonapi.dumps(data))
706
706
707
707
708 #-----------------------------------------------------------------------------
708 #-----------------------------------------------------------------------------
709 # RST web service handlers
709 # RST web service handlers
710 #-----------------------------------------------------------------------------
710 #-----------------------------------------------------------------------------
711
711
712
712
713 class RSTHandler(AuthenticatedHandler):
713 class RSTHandler(AuthenticatedHandler):
714
714
715 @web.authenticated
715 @web.authenticated
716 def post(self):
716 def post(self):
717 if publish_string is None:
717 if publish_string is None:
718 raise web.HTTPError(503, u'docutils not available')
718 raise web.HTTPError(503, u'docutils not available')
719 body = self.request.body.strip()
719 body = self.request.body.strip()
720 source = body
720 source = body
721 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
721 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
722 defaults = {'file_insertion_enabled': 0,
722 defaults = {'file_insertion_enabled': 0,
723 'raw_enabled': 0,
723 'raw_enabled': 0,
724 '_disable_config': 1,
724 '_disable_config': 1,
725 'stylesheet_path': 0
725 'stylesheet_path': 0
726 # 'template': template_path
726 # 'template': template_path
727 }
727 }
728 try:
728 try:
729 html = publish_string(source, writer_name='html',
729 html = publish_string(source, writer_name='html',
730 settings_overrides=defaults
730 settings_overrides=defaults
731 )
731 )
732 except:
732 except:
733 raise web.HTTPError(400, u'Invalid RST')
733 raise web.HTTPError(400, u'Invalid RST')
734 print html
734 print html
735 self.set_header('Content-Type', 'text/html')
735 self.set_header('Content-Type', 'text/html')
736 self.finish(html)
736 self.finish(html)
737
737
738
738
@@ -1,592 +1,593 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 #-----------------------------------------------------------------------------
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 random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import webbrowser
31 import webbrowser
32
32
33 # Third party
33 # Third party
34 import zmq
34 import zmq
35
35
36 # Install the pyzmq ioloop. This has to be done before anything else from
36 # Install the pyzmq ioloop. This has to be done before anything else from
37 # tornado is imported.
37 # tornado is imported.
38 from zmq.eventloop import ioloop
38 from zmq.eventloop import ioloop
39 ioloop.install()
39 ioloop.install()
40
40
41 from tornado import httpserver
41 from tornado import httpserver
42 from tornado import web
42 from tornado import web
43
43
44 # Our own libraries
44 # Our own libraries
45 from .kernelmanager import MappingKernelManager
45 from .kernelmanager import MappingKernelManager
46 from .handlers import (LoginHandler, LogoutHandler,
46 from .handlers import (LoginHandler, LogoutHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
52 )
52 )
53 from .notebookmanager import NotebookManager
53 from .notebookmanager import NotebookManager
54 from .clustermanager import ClusterManager
54 from .clustermanager import ClusterManager
55
55
56 from IPython.config.application import catch_config_error, boolean_flag
56 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.core.application import BaseIPythonApplication
57 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.profiledir import ProfileDir
58 from IPython.core.profiledir import ProfileDir
59 from IPython.frontend.consoleapp import IPythonConsoleApp
59 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.lib.kernel import swallow_argv
60 from IPython.lib.kernel import swallow_argv
61 from IPython.zmq.session import Session, default_secure
61 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.ipkernel import (
63 from IPython.zmq.ipkernel import (
64 flags as ipkernel_flags,
64 flags as ipkernel_flags,
65 aliases as ipkernel_aliases,
65 aliases as ipkernel_aliases,
66 IPKernelApp
66 IPKernelApp
67 )
67 )
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils import py3compat
69 from IPython.utils import py3compat
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Module globals
72 # Module globals
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
78 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
79 _cluster_action_regex = r"(?P<action>start|stop)"
79 _cluster_action_regex = r"(?P<action>start|stop)"
80
80
81
81
82 LOCALHOST = '127.0.0.1'
82 LOCALHOST = '127.0.0.1'
83
83
84 _examples = """
84 _examples = """
85 ipython notebook # start the notebook
85 ipython notebook # start the notebook
86 ipython notebook --profile=sympy # use the sympy profile
86 ipython notebook --profile=sympy # use the sympy profile
87 ipython notebook --pylab=inline # pylab in inline plotting mode
87 ipython notebook --pylab=inline # pylab in inline plotting mode
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 """
90 """
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Helper functions
93 # Helper functions
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95
95
96 def url_path_join(a,b):
96 def url_path_join(a,b):
97 if a.endswith('/') and b.startswith('/'):
97 if a.endswith('/') and b.startswith('/'):
98 return a[:-1]+b
98 return a[:-1]+b
99 else:
99 else:
100 return a+b
100 return a+b
101
101
102 def random_ports(port, n):
102 def random_ports(port, n):
103 """Generate a list of n random ports near the given port.
103 """Generate a list of n random ports near the given port.
104
104
105 The first 5 ports will be sequential, and the remaining n-5 will be
105 The first 5 ports will be sequential, and the remaining n-5 will be
106 randomly selected in the range [port-2*n, port+2*n].
106 randomly selected in the range [port-2*n, port+2*n].
107 """
107 """
108 for i in range(min(5, n)):
108 for i in range(min(5, n)):
109 yield port + i
109 yield port + i
110 for i in range(n-5):
110 for i in range(n-5):
111 yield port + random.randint(-2*n, 2*n)
111 yield port + random.randint(-2*n, 2*n)
112
112
113 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
114 # The Tornado web application
114 # The Tornado web application
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116
116
117 class NotebookWebApplication(web.Application):
117 class NotebookWebApplication(web.Application):
118
118
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
120 cluster_manager, log,
120 cluster_manager, log,
121 base_project_url, settings_overrides):
121 base_project_url, settings_overrides):
122 handlers = [
122 handlers = [
123 (r"/", ProjectDashboardHandler),
123 (r"/", ProjectDashboardHandler),
124 (r"/login", LoginHandler),
124 (r"/login", LoginHandler),
125 (r"/logout", LogoutHandler),
125 (r"/logout", LogoutHandler),
126 (r"/new", NewHandler),
126 (r"/new", NewHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
130 (r"/kernels", MainKernelHandler),
130 (r"/kernels", MainKernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
135 (r"/notebooks", NotebookRootHandler),
135 (r"/notebooks", NotebookRootHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
137 (r"/rstservice/render", RSTHandler),
137 (r"/rstservice/render", RSTHandler),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
139 (r"/clusters", MainClusterHandler),
139 (r"/clusters", MainClusterHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
142 ]
142 ]
143 settings = dict(
144 template_path=os.path.join(os.path.dirname(__file__), "templates"),
145 static_path=os.path.join(os.path.dirname(__file__), "static"),
146 cookie_secret=os.urandom(1024),
147 login_url="/login",
148 )
149
150 # allow custom overrides for the tornado web app.
151 settings.update(settings_overrides)
152
143
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
144 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
145 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
146 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
147 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
148 # This enforces that base_project_url be ascii in that situation.
158 #
149 #
159 # Note that the URLs these patterns check against are escaped,
150 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
151 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
152 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
153
154 settings = dict(
155 template_path=os.path.join(os.path.dirname(__file__), "templates"),
156 static_path=os.path.join(os.path.dirname(__file__), "static"),
157 cookie_secret=os.urandom(1024),
158 login_url="%s/login"%(base_project_url.rstrip('/')),
159 )
160
161 # allow custom overrides for the tornado web app.
162 settings.update(settings_overrides)
163
163 # prepend base_project_url onto the patterns that we match
164 # prepend base_project_url onto the patterns that we match
164 new_handlers = []
165 new_handlers = []
165 for handler in handlers:
166 for handler in handlers:
166 pattern = url_path_join(base_project_url, handler[0])
167 pattern = url_path_join(base_project_url, handler[0])
167 new_handler = tuple([pattern]+list(handler[1:]))
168 new_handler = tuple([pattern]+list(handler[1:]))
168 new_handlers.append( new_handler )
169 new_handlers.append( new_handler )
169
170
170 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
171 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
171
172
172 self.kernel_manager = kernel_manager
173 self.kernel_manager = kernel_manager
173 self.notebook_manager = notebook_manager
174 self.notebook_manager = notebook_manager
174 self.cluster_manager = cluster_manager
175 self.cluster_manager = cluster_manager
175 self.ipython_app = ipython_app
176 self.ipython_app = ipython_app
176 self.read_only = self.ipython_app.read_only
177 self.read_only = self.ipython_app.read_only
177 self.log = log
178 self.log = log
178
179
179
180
180 #-----------------------------------------------------------------------------
181 #-----------------------------------------------------------------------------
181 # Aliases and Flags
182 # Aliases and Flags
182 #-----------------------------------------------------------------------------
183 #-----------------------------------------------------------------------------
183
184
184 flags = dict(ipkernel_flags)
185 flags = dict(ipkernel_flags)
185 flags['no-browser']=(
186 flags['no-browser']=(
186 {'NotebookApp' : {'open_browser' : False}},
187 {'NotebookApp' : {'open_browser' : False}},
187 "Don't open the notebook in a browser after startup."
188 "Don't open the notebook in a browser after startup."
188 )
189 )
189 flags['no-mathjax']=(
190 flags['no-mathjax']=(
190 {'NotebookApp' : {'enable_mathjax' : False}},
191 {'NotebookApp' : {'enable_mathjax' : False}},
191 """Disable MathJax
192 """Disable MathJax
192
193
193 MathJax is the javascript library IPython uses to render math/LaTeX. It is
194 MathJax is the javascript library IPython uses to render math/LaTeX. It is
194 very large, so you may want to disable it if you have a slow internet
195 very large, so you may want to disable it if you have a slow internet
195 connection, or for offline use of the notebook.
196 connection, or for offline use of the notebook.
196
197
197 When disabled, equations etc. will appear as their untransformed TeX source.
198 When disabled, equations etc. will appear as their untransformed TeX source.
198 """
199 """
199 )
200 )
200 flags['read-only'] = (
201 flags['read-only'] = (
201 {'NotebookApp' : {'read_only' : True}},
202 {'NotebookApp' : {'read_only' : True}},
202 """Allow read-only access to notebooks.
203 """Allow read-only access to notebooks.
203
204
204 When using a password to protect the notebook server, this flag
205 When using a password to protect the notebook server, this flag
205 allows unauthenticated clients to view the notebook list, and
206 allows unauthenticated clients to view the notebook list, and
206 individual notebooks, but not edit them, start kernels, or run
207 individual notebooks, but not edit them, start kernels, or run
207 code.
208 code.
208
209
209 If no password is set, the server will be entirely read-only.
210 If no password is set, the server will be entirely read-only.
210 """
211 """
211 )
212 )
212
213
213 # Add notebook manager flags
214 # Add notebook manager flags
214 flags.update(boolean_flag('script', 'NotebookManager.save_script',
215 flags.update(boolean_flag('script', 'NotebookManager.save_script',
215 'Auto-save a .py script everytime the .ipynb notebook is saved',
216 'Auto-save a .py script everytime the .ipynb notebook is saved',
216 'Do not auto-save .py scripts for every notebook'))
217 'Do not auto-save .py scripts for every notebook'))
217
218
218 # the flags that are specific to the frontend
219 # the flags that are specific to the frontend
219 # these must be scrubbed before being passed to the kernel,
220 # these must be scrubbed before being passed to the kernel,
220 # or it will raise an error on unrecognized flags
221 # or it will raise an error on unrecognized flags
221 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
222 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
222
223
223 aliases = dict(ipkernel_aliases)
224 aliases = dict(ipkernel_aliases)
224
225
225 aliases.update({
226 aliases.update({
226 'ip': 'NotebookApp.ip',
227 'ip': 'NotebookApp.ip',
227 'port': 'NotebookApp.port',
228 'port': 'NotebookApp.port',
228 'port-retries': 'NotebookApp.port_retries',
229 'port-retries': 'NotebookApp.port_retries',
229 'keyfile': 'NotebookApp.keyfile',
230 'keyfile': 'NotebookApp.keyfile',
230 'certfile': 'NotebookApp.certfile',
231 'certfile': 'NotebookApp.certfile',
231 'notebook-dir': 'NotebookManager.notebook_dir',
232 'notebook-dir': 'NotebookManager.notebook_dir',
232 'browser': 'NotebookApp.browser',
233 'browser': 'NotebookApp.browser',
233 })
234 })
234
235
235 # remove ipkernel flags that are singletons, and don't make sense in
236 # remove ipkernel flags that are singletons, and don't make sense in
236 # multi-kernel evironment:
237 # multi-kernel evironment:
237 aliases.pop('f', None)
238 aliases.pop('f', None)
238
239
239 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
240 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
240 u'notebook-dir']
241 u'notebook-dir']
241
242
242 #-----------------------------------------------------------------------------
243 #-----------------------------------------------------------------------------
243 # NotebookApp
244 # NotebookApp
244 #-----------------------------------------------------------------------------
245 #-----------------------------------------------------------------------------
245
246
246 class NotebookApp(BaseIPythonApplication):
247 class NotebookApp(BaseIPythonApplication):
247
248
248 name = 'ipython-notebook'
249 name = 'ipython-notebook'
249 default_config_file_name='ipython_notebook_config.py'
250 default_config_file_name='ipython_notebook_config.py'
250
251
251 description = """
252 description = """
252 The IPython HTML Notebook.
253 The IPython HTML Notebook.
253
254
254 This launches a Tornado based HTML Notebook Server that serves up an
255 This launches a Tornado based HTML Notebook Server that serves up an
255 HTML5/Javascript Notebook client.
256 HTML5/Javascript Notebook client.
256 """
257 """
257 examples = _examples
258 examples = _examples
258
259
259 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
260 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
260 flags = Dict(flags)
261 flags = Dict(flags)
261 aliases = Dict(aliases)
262 aliases = Dict(aliases)
262
263
263 kernel_argv = List(Unicode)
264 kernel_argv = List(Unicode)
264
265
265 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
266 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
266 default_value=logging.INFO,
267 default_value=logging.INFO,
267 config=True,
268 config=True,
268 help="Set the log level by value or name.")
269 help="Set the log level by value or name.")
269
270
270 # create requested profiles by default, if they don't exist:
271 # create requested profiles by default, if they don't exist:
271 auto_create = Bool(True)
272 auto_create = Bool(True)
272
273
273 # file to be opened in the notebook server
274 # file to be opened in the notebook server
274 file_to_run = Unicode('')
275 file_to_run = Unicode('')
275
276
276 # Network related information.
277 # Network related information.
277
278
278 ip = Unicode(LOCALHOST, config=True,
279 ip = Unicode(LOCALHOST, config=True,
279 help="The IP address the notebook server will listen on."
280 help="The IP address the notebook server will listen on."
280 )
281 )
281
282
282 def _ip_changed(self, name, old, new):
283 def _ip_changed(self, name, old, new):
283 if new == u'*': self.ip = u''
284 if new == u'*': self.ip = u''
284
285
285 port = Integer(8888, config=True,
286 port = Integer(8888, config=True,
286 help="The port the notebook server will listen on."
287 help="The port the notebook server will listen on."
287 )
288 )
288 port_retries = Integer(50, config=True,
289 port_retries = Integer(50, config=True,
289 help="The number of additional ports to try if the specified port is not available."
290 help="The number of additional ports to try if the specified port is not available."
290 )
291 )
291
292
292 certfile = Unicode(u'', config=True,
293 certfile = Unicode(u'', config=True,
293 help="""The full path to an SSL/TLS certificate file."""
294 help="""The full path to an SSL/TLS certificate file."""
294 )
295 )
295
296
296 keyfile = Unicode(u'', config=True,
297 keyfile = Unicode(u'', config=True,
297 help="""The full path to a private key file for usage with SSL/TLS."""
298 help="""The full path to a private key file for usage with SSL/TLS."""
298 )
299 )
299
300
300 password = Unicode(u'', config=True,
301 password = Unicode(u'', config=True,
301 help="""Hashed password to use for web authentication.
302 help="""Hashed password to use for web authentication.
302
303
303 To generate, type in a python/IPython shell:
304 To generate, type in a python/IPython shell:
304
305
305 from IPython.lib import passwd; passwd()
306 from IPython.lib import passwd; passwd()
306
307
307 The string should be of the form type:salt:hashed-password.
308 The string should be of the form type:salt:hashed-password.
308 """
309 """
309 )
310 )
310
311
311 open_browser = Bool(True, config=True,
312 open_browser = Bool(True, config=True,
312 help="""Whether to open in a browser after starting.
313 help="""Whether to open in a browser after starting.
313 The specific browser used is platform dependent and
314 The specific browser used is platform dependent and
314 determined by the python standard library `webbrowser`
315 determined by the python standard library `webbrowser`
315 module, unless it is overridden using the --browser
316 module, unless it is overridden using the --browser
316 (NotebookApp.browser) configuration option.
317 (NotebookApp.browser) configuration option.
317 """)
318 """)
318
319
319 browser = Unicode(u'', config=True,
320 browser = Unicode(u'', config=True,
320 help="""Specify what command to use to invoke a web
321 help="""Specify what command to use to invoke a web
321 browser when opening the notebook. If not specified, the
322 browser when opening the notebook. If not specified, the
322 default browser will be determined by the `webbrowser`
323 default browser will be determined by the `webbrowser`
323 standard library module, which allows setting of the
324 standard library module, which allows setting of the
324 BROWSER environment variable to override it.
325 BROWSER environment variable to override it.
325 """)
326 """)
326
327
327 read_only = Bool(False, config=True,
328 read_only = Bool(False, config=True,
328 help="Whether to prevent editing/execution of notebooks."
329 help="Whether to prevent editing/execution of notebooks."
329 )
330 )
330
331
331 webapp_settings = Dict(config=True,
332 webapp_settings = Dict(config=True,
332 help="Supply overrides for the tornado.web.Application that the "
333 help="Supply overrides for the tornado.web.Application that the "
333 "IPython notebook uses.")
334 "IPython notebook uses.")
334
335
335 enable_mathjax = Bool(True, config=True,
336 enable_mathjax = Bool(True, config=True,
336 help="""Whether to enable MathJax for typesetting math/TeX
337 help="""Whether to enable MathJax for typesetting math/TeX
337
338
338 MathJax is the javascript library IPython uses to render math/LaTeX. It is
339 MathJax is the javascript library IPython uses to render math/LaTeX. It is
339 very large, so you may want to disable it if you have a slow internet
340 very large, so you may want to disable it if you have a slow internet
340 connection, or for offline use of the notebook.
341 connection, or for offline use of the notebook.
341
342
342 When disabled, equations etc. will appear as their untransformed TeX source.
343 When disabled, equations etc. will appear as their untransformed TeX source.
343 """
344 """
344 )
345 )
345 def _enable_mathjax_changed(self, name, old, new):
346 def _enable_mathjax_changed(self, name, old, new):
346 """set mathjax url to empty if mathjax is disabled"""
347 """set mathjax url to empty if mathjax is disabled"""
347 if not new:
348 if not new:
348 self.mathjax_url = u''
349 self.mathjax_url = u''
349
350
350 base_project_url = Unicode('/', config=True,
351 base_project_url = Unicode('/', config=True,
351 help='''The base URL for the notebook server''')
352 help='''The base URL for the notebook server''')
352 base_kernel_url = Unicode('/', config=True,
353 base_kernel_url = Unicode('/', config=True,
353 help='''The base URL for the kernel server''')
354 help='''The base URL for the kernel server''')
354 websocket_host = Unicode("", config=True,
355 websocket_host = Unicode("", config=True,
355 help="""The hostname for the websocket server."""
356 help="""The hostname for the websocket server."""
356 )
357 )
357
358
358 mathjax_url = Unicode("", config=True,
359 mathjax_url = Unicode("", config=True,
359 help="""The url for MathJax.js."""
360 help="""The url for MathJax.js."""
360 )
361 )
361 def _mathjax_url_default(self):
362 def _mathjax_url_default(self):
362 if not self.enable_mathjax:
363 if not self.enable_mathjax:
363 return u''
364 return u''
364 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
365 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
365 static_url_prefix = self.webapp_settings.get("static_url_prefix",
366 static_url_prefix = self.webapp_settings.get("static_url_prefix",
366 "/static/")
367 "/static/")
367 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
368 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
368 self.log.info("Using local MathJax")
369 self.log.info("Using local MathJax")
369 return static_url_prefix+u"mathjax/MathJax.js"
370 return static_url_prefix+u"mathjax/MathJax.js"
370 else:
371 else:
371 if self.certfile:
372 if self.certfile:
372 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
373 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
373 base = u"https://c328740.ssl.cf1.rackcdn.com"
374 base = u"https://c328740.ssl.cf1.rackcdn.com"
374 else:
375 else:
375 base = u"http://cdn.mathjax.org"
376 base = u"http://cdn.mathjax.org"
376
377
377 url = base + u"/mathjax/latest/MathJax.js"
378 url = base + u"/mathjax/latest/MathJax.js"
378 self.log.info("Using MathJax from CDN: %s", url)
379 self.log.info("Using MathJax from CDN: %s", url)
379 return url
380 return url
380
381
381 def _mathjax_url_changed(self, name, old, new):
382 def _mathjax_url_changed(self, name, old, new):
382 if new and not self.enable_mathjax:
383 if new and not self.enable_mathjax:
383 # enable_mathjax=False overrides mathjax_url
384 # enable_mathjax=False overrides mathjax_url
384 self.mathjax_url = u''
385 self.mathjax_url = u''
385 else:
386 else:
386 self.log.info("Using MathJax: %s", new)
387 self.log.info("Using MathJax: %s", new)
387
388
388 def parse_command_line(self, argv=None):
389 def parse_command_line(self, argv=None):
389 super(NotebookApp, self).parse_command_line(argv)
390 super(NotebookApp, self).parse_command_line(argv)
390 if argv is None:
391 if argv is None:
391 argv = sys.argv[1:]
392 argv = sys.argv[1:]
392
393
393 # Scrub frontend-specific flags
394 # Scrub frontend-specific flags
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
395 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
395 # Kernel should inherit default config file from frontend
396 # Kernel should inherit default config file from frontend
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
397 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
397
398
398 if self.extra_args:
399 if self.extra_args:
399 f = os.path.abspath(self.extra_args[0])
400 f = os.path.abspath(self.extra_args[0])
400 if os.path.isdir(f):
401 if os.path.isdir(f):
401 nbdir = f
402 nbdir = f
402 else:
403 else:
403 self.file_to_run = f
404 self.file_to_run = f
404 nbdir = os.path.dirname(f)
405 nbdir = os.path.dirname(f)
405 self.config.NotebookManager.notebook_dir = nbdir
406 self.config.NotebookManager.notebook_dir = nbdir
406
407
407 def init_configurables(self):
408 def init_configurables(self):
408 # force Session default to be secure
409 # force Session default to be secure
409 default_secure(self.config)
410 default_secure(self.config)
410 self.kernel_manager = MappingKernelManager(
411 self.kernel_manager = MappingKernelManager(
411 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
412 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
412 connection_dir = self.profile_dir.security_dir,
413 connection_dir = self.profile_dir.security_dir,
413 )
414 )
414 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
415 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
415 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
416 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
416 self.notebook_manager.list_notebooks()
417 self.notebook_manager.list_notebooks()
417 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
418 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
418 self.cluster_manager.update_profiles()
419 self.cluster_manager.update_profiles()
419
420
420 def init_logging(self):
421 def init_logging(self):
421 # This prevents double log messages because tornado use a root logger that
422 # This prevents double log messages because tornado use a root logger that
422 # self.log is a child of. The logging module dipatches log messages to a log
423 # self.log is a child of. The logging module dipatches log messages to a log
423 # and all of its ancenstors until propagate is set to False.
424 # and all of its ancenstors until propagate is set to False.
424 self.log.propagate = False
425 self.log.propagate = False
425
426
426 def init_webapp(self):
427 def init_webapp(self):
427 """initialize tornado webapp and httpserver"""
428 """initialize tornado webapp and httpserver"""
428 self.web_app = NotebookWebApplication(
429 self.web_app = NotebookWebApplication(
429 self, self.kernel_manager, self.notebook_manager,
430 self, self.kernel_manager, self.notebook_manager,
430 self.cluster_manager, self.log,
431 self.cluster_manager, self.log,
431 self.base_project_url, self.webapp_settings
432 self.base_project_url, self.webapp_settings
432 )
433 )
433 if self.certfile:
434 if self.certfile:
434 ssl_options = dict(certfile=self.certfile)
435 ssl_options = dict(certfile=self.certfile)
435 if self.keyfile:
436 if self.keyfile:
436 ssl_options['keyfile'] = self.keyfile
437 ssl_options['keyfile'] = self.keyfile
437 else:
438 else:
438 ssl_options = None
439 ssl_options = None
439 self.web_app.password = self.password
440 self.web_app.password = self.password
440 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
441 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
441 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
442 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
442 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
443 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
443 'but not using any encryption or authentication. This is highly '
444 'but not using any encryption or authentication. This is highly '
444 'insecure and not recommended.')
445 'insecure and not recommended.')
445
446
446 success = None
447 success = None
447 for port in random_ports(self.port, self.port_retries+1):
448 for port in random_ports(self.port, self.port_retries+1):
448 try:
449 try:
449 self.http_server.listen(port, self.ip)
450 self.http_server.listen(port, self.ip)
450 except socket.error, e:
451 except socket.error, e:
451 if e.errno != errno.EADDRINUSE:
452 if e.errno != errno.EADDRINUSE:
452 raise
453 raise
453 self.log.info('The port %i is already in use, trying another random port.' % port)
454 self.log.info('The port %i is already in use, trying another random port.' % port)
454 else:
455 else:
455 self.port = port
456 self.port = port
456 success = True
457 success = True
457 break
458 break
458 if not success:
459 if not success:
459 self.log.critical('ERROR: the notebook server could not be started because '
460 self.log.critical('ERROR: the notebook server could not be started because '
460 'no available port could be found.')
461 'no available port could be found.')
461 self.exit(1)
462 self.exit(1)
462
463
463 def init_signal(self):
464 def init_signal(self):
464 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
465 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
465 # safely extract zmq version info:
466 # safely extract zmq version info:
466 try:
467 try:
467 zmq_v = zmq.pyzmq_version_info()
468 zmq_v = zmq.pyzmq_version_info()
468 except AttributeError:
469 except AttributeError:
469 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
470 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
470 if 'dev' in zmq.__version__:
471 if 'dev' in zmq.__version__:
471 zmq_v.append(999)
472 zmq_v.append(999)
472 zmq_v = tuple(zmq_v)
473 zmq_v = tuple(zmq_v)
473 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
474 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
474 # This won't work with 2.1.7 and
475 # This won't work with 2.1.7 and
475 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
476 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
476 # but it will work
477 # but it will work
477 signal.signal(signal.SIGINT, self._handle_sigint)
478 signal.signal(signal.SIGINT, self._handle_sigint)
478 signal.signal(signal.SIGTERM, self._signal_stop)
479 signal.signal(signal.SIGTERM, self._signal_stop)
479
480
480 def _handle_sigint(self, sig, frame):
481 def _handle_sigint(self, sig, frame):
481 """SIGINT handler spawns confirmation dialog"""
482 """SIGINT handler spawns confirmation dialog"""
482 # register more forceful signal handler for ^C^C case
483 # register more forceful signal handler for ^C^C case
483 signal.signal(signal.SIGINT, self._signal_stop)
484 signal.signal(signal.SIGINT, self._signal_stop)
484 # request confirmation dialog in bg thread, to avoid
485 # request confirmation dialog in bg thread, to avoid
485 # blocking the App
486 # blocking the App
486 thread = threading.Thread(target=self._confirm_exit)
487 thread = threading.Thread(target=self._confirm_exit)
487 thread.daemon = True
488 thread.daemon = True
488 thread.start()
489 thread.start()
489
490
490 def _restore_sigint_handler(self):
491 def _restore_sigint_handler(self):
491 """callback for restoring original SIGINT handler"""
492 """callback for restoring original SIGINT handler"""
492 signal.signal(signal.SIGINT, self._handle_sigint)
493 signal.signal(signal.SIGINT, self._handle_sigint)
493
494
494 def _confirm_exit(self):
495 def _confirm_exit(self):
495 """confirm shutdown on ^C
496 """confirm shutdown on ^C
496
497
497 A second ^C, or answering 'y' within 5s will cause shutdown,
498 A second ^C, or answering 'y' within 5s will cause shutdown,
498 otherwise original SIGINT handler will be restored.
499 otherwise original SIGINT handler will be restored.
499
500
500 This doesn't work on Windows.
501 This doesn't work on Windows.
501 """
502 """
502 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
503 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
503 time.sleep(0.1)
504 time.sleep(0.1)
504 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
505 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
505 sys.stdout.flush()
506 sys.stdout.flush()
506 r,w,x = select.select([sys.stdin], [], [], 5)
507 r,w,x = select.select([sys.stdin], [], [], 5)
507 if r:
508 if r:
508 line = sys.stdin.readline()
509 line = sys.stdin.readline()
509 if line.lower().startswith('y'):
510 if line.lower().startswith('y'):
510 self.log.critical("Shutdown confirmed")
511 self.log.critical("Shutdown confirmed")
511 ioloop.IOLoop.instance().stop()
512 ioloop.IOLoop.instance().stop()
512 return
513 return
513 else:
514 else:
514 print "No answer for 5s:",
515 print "No answer for 5s:",
515 print "resuming operation..."
516 print "resuming operation..."
516 # no answer, or answer is no:
517 # no answer, or answer is no:
517 # set it back to original SIGINT handler
518 # set it back to original SIGINT handler
518 # use IOLoop.add_callback because signal.signal must be called
519 # use IOLoop.add_callback because signal.signal must be called
519 # from main thread
520 # from main thread
520 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
521 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
521
522
522 def _signal_stop(self, sig, frame):
523 def _signal_stop(self, sig, frame):
523 self.log.critical("received signal %s, stopping", sig)
524 self.log.critical("received signal %s, stopping", sig)
524 ioloop.IOLoop.instance().stop()
525 ioloop.IOLoop.instance().stop()
525
526
526 @catch_config_error
527 @catch_config_error
527 def initialize(self, argv=None):
528 def initialize(self, argv=None):
528 self.init_logging()
529 self.init_logging()
529 super(NotebookApp, self).initialize(argv)
530 super(NotebookApp, self).initialize(argv)
530 self.init_configurables()
531 self.init_configurables()
531 self.init_webapp()
532 self.init_webapp()
532 self.init_signal()
533 self.init_signal()
533
534
534 def cleanup_kernels(self):
535 def cleanup_kernels(self):
535 """shutdown all kernels
536 """shutdown all kernels
536
537
537 The kernels will shutdown themselves when this process no longer exists,
538 The kernels will shutdown themselves when this process no longer exists,
538 but explicit shutdown allows the KernelManagers to cleanup the connection files.
539 but explicit shutdown allows the KernelManagers to cleanup the connection files.
539 """
540 """
540 self.log.info('Shutting down kernels')
541 self.log.info('Shutting down kernels')
541 km = self.kernel_manager
542 km = self.kernel_manager
542 # copy list, since shutdown_kernel deletes keys
543 # copy list, since shutdown_kernel deletes keys
543 for kid in list(km.kernel_ids):
544 for kid in list(km.kernel_ids):
544 km.shutdown_kernel(kid)
545 km.shutdown_kernel(kid)
545
546
546 def start(self):
547 def start(self):
547 ip = self.ip if self.ip else '[all ip addresses on your system]'
548 ip = self.ip if self.ip else '[all ip addresses on your system]'
548 proto = 'https' if self.certfile else 'http'
549 proto = 'https' if self.certfile else 'http'
549 info = self.log.info
550 info = self.log.info
550 info("The IPython Notebook is running at: %s://%s:%i%s" %
551 info("The IPython Notebook is running at: %s://%s:%i%s" %
551 (proto, ip, self.port,self.base_project_url) )
552 (proto, ip, self.port,self.base_project_url) )
552 info("Use Control-C to stop this server and shut down all kernels.")
553 info("Use Control-C to stop this server and shut down all kernels.")
553
554
554 if self.open_browser or self.file_to_run:
555 if self.open_browser or self.file_to_run:
555 ip = self.ip or '127.0.0.1'
556 ip = self.ip or '127.0.0.1'
556 try:
557 try:
557 browser = webbrowser.get(self.browser or None)
558 browser = webbrowser.get(self.browser or None)
558 except webbrowser.Error as e:
559 except webbrowser.Error as e:
559 self.log.warn('No web browser found: %s.' % e)
560 self.log.warn('No web browser found: %s.' % e)
560 browser = None
561 browser = None
561
562
562 if self.file_to_run:
563 if self.file_to_run:
563 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
564 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
564 for nb in self.notebook_manager.list_notebooks():
565 for nb in self.notebook_manager.list_notebooks():
565 if filename == nb['name']:
566 if filename == nb['name']:
566 url = nb['notebook_id']
567 url = nb['notebook_id']
567 break
568 break
568 else:
569 else:
569 url = ''
570 url = ''
570 else:
571 else:
571 url = ''
572 url = ''
572 if browser:
573 if browser:
573 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
574 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
574 self.port, self.base_project_url, url), new=2)
575 self.port, self.base_project_url, url), new=2)
575 threading.Thread(target=b).start()
576 threading.Thread(target=b).start()
576 try:
577 try:
577 ioloop.IOLoop.instance().start()
578 ioloop.IOLoop.instance().start()
578 except KeyboardInterrupt:
579 except KeyboardInterrupt:
579 info("Interrupted...")
580 info("Interrupted...")
580 finally:
581 finally:
581 self.cleanup_kernels()
582 self.cleanup_kernels()
582
583
583
584
584 #-----------------------------------------------------------------------------
585 #-----------------------------------------------------------------------------
585 # Main entry point
586 # Main entry point
586 #-----------------------------------------------------------------------------
587 #-----------------------------------------------------------------------------
587
588
588 def launch_new_instance():
589 def launch_new_instance():
589 app = NotebookApp.instance()
590 app = NotebookApp.instance()
590 app.initialize()
591 app.initialize()
591 app.start()
592 app.start()
592
593
@@ -1,44 +1,45 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Login button
9 // Login button
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 var base_url = $('body').data('baseProjectUrl');
13
14
14 var LoginWidget = function (selector) {
15 var LoginWidget = function (selector) {
15 this.selector = selector;
16 this.selector = selector;
16 if (this.selector !== undefined) {
17 if (this.selector !== undefined) {
17 this.element = $(selector);
18 this.element = $(selector);
18 this.style();
19 this.style();
19 this.bind_events();
20 this.bind_events();
20 }
21 }
21 };
22 };
22
23
23 LoginWidget.prototype.style = function () {
24 LoginWidget.prototype.style = function () {
24 this.element.find('button#logout').button();
25 this.element.find('button#logout').button();
25 this.element.find('button#login').button();
26 this.element.find('button#login').button();
26 };
27 };
27
28
28
29
29 LoginWidget.prototype.bind_events = function () {
30 LoginWidget.prototype.bind_events = function () {
30 var that = this;
31 var that = this;
31 this.element.find("button#logout").click(function () {
32 this.element.find("button#logout").click(function () {
32 window.location = "/logout";
33 window.location = base_url+"logout";
33 });
34 });
34 this.element.find("button#login").click(function () {
35 this.element.find("button#login").click(function () {
35 window.location = "/login";
36 window.location = base_url+"login";
36 });
37 });
37 };
38 };
38
39
39 // Set module variables
40 // Set module variables
40 IPython.LoginWidget = LoginWidget;
41 IPython.LoginWidget = LoginWidget;
41
42
42 return IPython;
43 return IPython;
43
44
44 }(IPython));
45 }(IPython));
@@ -1,42 +1,42 b''
1 {% extends page.html %}
1 {% extends page.html %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4
4
5 <link rel="stylesheet" href="{{static_url("css/login.css") }}" type="text/css"/>
5 <link rel="stylesheet" href="{{static_url("css/login.css") }}" type="text/css"/>
6
6
7 {% end %}
7 {% end %}
8
8
9
9
10 {% block login_widget %}
10 {% block login_widget %}
11 {% end %}
11 {% end %}
12
12
13
13
14 {% block site %}
14 {% block site %}
15
15
16 <div id="main_app">
16 <div id="main_app">
17
17
18 {% if login_available %}
18 {% if login_available %}
19 <form action="/login?next={{url_escape(next)}}" method="post">
19 <form action="{{base_project_url}}login?next={{url_escape(next)}}" method="post">
20 Password: <input type="password" class='ui-widget ui-widget-content' name="password" id="password_input">
20 Password: <input type="password" class='ui-widget ui-widget-content' name="password" id="password_input">
21 <input type="submit" value="Log in" id="login_submit">
21 <input type="submit" value="Log in" id="login_submit">
22 </form>
22 </form>
23 {% end %}
23 {% end %}
24
24
25 {% if message %}
25 {% if message %}
26 {% for key in message %}
26 {% for key in message %}
27 <div class="message {{key}}">
27 <div class="message {{key}}">
28 {{message[key]}}
28 {{message[key]}}
29 </div>
29 </div>
30 {% end %}
30 {% end %}
31 {% end %}
31 {% end %}
32
32
33 <div/>
33 <div/>
34
34
35 {% end %}
35 {% end %}
36
36
37
37
38 {% block script %}
38 {% block script %}
39
39
40 <script src="{{static_url("js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
40 <script src="{{static_url("js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
41
41
42 {% end %}
42 {% end %}
@@ -1,40 +1,40 b''
1 {% extends page.html %}
1 {% extends page.html %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4
4
5 <link rel="stylesheet" href="{{static_url("css/logout.css") }}" type="text/css"/>
5 <link rel="stylesheet" href="{{static_url("css/logout.css") }}" type="text/css"/>
6
6
7 {% end %}
7 {% end %}
8
8
9
9
10 {% block login_widget %}
10 {% block login_widget %}
11 {% end %}
11 {% end %}
12
12
13 {% block site %}
13 {% block site %}
14
14
15 <div id="main_app">
15 <div id="main_app">
16
16
17 {% if message %}
17 {% if message %}
18 {% for key in message %}
18 {% for key in message %}
19 <div class="message {{key}}">
19 <div class="message {{key}}">
20 {{message[key]}}
20 {{message[key]}}
21 </div>
21 </div>
22 {% end %}
22 {% end %}
23 {% end %}
23 {% end %}
24
24
25 {% if read_only or not login_available %}
25 {% if read_only or not login_available %}
26 Proceed to the <a href="/">dashboard</a>.
26 Proceed to the <a href="{{base_project_url}}">dashboard</a>.
27 {% else %}
27 {% else %}
28 Proceed to the <a href="/login">login page</a>.
28 Proceed to the <a href="{{base_project_url}}login">login page</a>.
29 {% end %}
29 {% end %}
30
30
31
31
32 <div/>
32 <div/>
33
33
34 {% end %}
34 {% end %}
35
35
36 {% block script %}
36 {% block script %}
37
37
38 <script src="{{static_url("js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
38 <script src="{{static_url("js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
39
39
40 {% end %}
40 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now