##// END OF EJS Templates
clean unused command
Matthias BUSSONNIER -
Show More
@@ -1,744 +1,741 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
33
34 try:
34 try:
35 from docutils.core import publish_string
35 from docutils.core import publish_string
36 except ImportError:
36 except ImportError:
37 publish_string = None
37 publish_string = None
38
38
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42
42
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # parts tornado cares about haven't really changed, so it's OK to continue
44 # parts tornado cares about haven't really changed, so it's OK to continue
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # version as of Oct 30/2011) the version check fails, see the issue report:
46 # version as of Oct 30/2011) the version check fails, see the issue report:
47
47
48 # https://github.com/facebook/tornado/issues/385
48 # https://github.com/facebook/tornado/issues/385
49
49
50 # This issue has been fixed in Tornado post 2.1.1:
50 # This issue has been fixed in Tornado post 2.1.1:
51
51
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53
53
54 # Here we manually apply the same patch as above so that users of IPython can
54 # Here we manually apply the same patch as above so that users of IPython can
55 # continue to work with an officially released Tornado. We make the
55 # continue to work with an officially released Tornado. We make the
56 # monkeypatch version check as narrow as possible to limit its effects; once
56 # monkeypatch version check as narrow as possible to limit its effects; once
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58
58
59 import tornado
59 import tornado
60
60
61 if tornado.version_info <= (2,1,1):
61 if tornado.version_info <= (2,1,1):
62
62
63 def _execute(self, transforms, *args, **kwargs):
63 def _execute(self, transforms, *args, **kwargs):
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65
65
66 self.open_args = args
66 self.open_args = args
67 self.open_kwargs = kwargs
67 self.open_kwargs = kwargs
68
68
69 # The difference between version 8 and 13 is that in 8 the
69 # The difference between version 8 and 13 is that in 8 the
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # simply "Origin".
71 # simply "Origin".
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection.accept_connection()
74 self.ws_connection.accept_connection()
75
75
76 elif self.request.headers.get("Sec-WebSocket-Version"):
76 elif self.request.headers.get("Sec-WebSocket-Version"):
77 self.stream.write(tornado.escape.utf8(
77 self.stream.write(tornado.escape.utf8(
78 "HTTP/1.1 426 Upgrade Required\r\n"
78 "HTTP/1.1 426 Upgrade Required\r\n"
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 self.stream.close()
80 self.stream.close()
81
81
82 else:
82 else:
83 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection.accept_connection()
84 self.ws_connection.accept_connection()
85
85
86 websocket.WebSocketHandler._execute = _execute
86 websocket.WebSocketHandler._execute = _execute
87 del _execute
87 del _execute
88
88
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90 # Decorator for disabling read-only handlers
90 # Decorator for disabling read-only handlers
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92
92
93 @decorator
93 @decorator
94 def not_if_readonly(f, self, *args, **kwargs):
94 def not_if_readonly(f, self, *args, **kwargs):
95 if self.application.read_only:
95 if self.application.read_only:
96 raise web.HTTPError(403, "Notebook server is read-only")
96 raise web.HTTPError(403, "Notebook server is read-only")
97 else:
97 else:
98 return f(self, *args, **kwargs)
98 return f(self, *args, **kwargs)
99
99
100 @decorator
100 @decorator
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 """authenticate this page *unless* readonly view is active.
102 """authenticate this page *unless* readonly view is active.
103
103
104 In read-only mode, the notebook list and print view should
104 In read-only mode, the notebook list and print view should
105 be accessible without authentication.
105 be accessible without authentication.
106 """
106 """
107
107
108 @web.authenticated
108 @web.authenticated
109 def auth_f(self, *args, **kwargs):
109 def auth_f(self, *args, **kwargs):
110 return f(self, *args, **kwargs)
110 return f(self, *args, **kwargs)
111
111
112 if self.application.read_only:
112 if self.application.read_only:
113 return f(self, *args, **kwargs)
113 return f(self, *args, **kwargs)
114 else:
114 else:
115 return auth_f(self, *args, **kwargs)
115 return auth_f(self, *args, **kwargs)
116
116
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118 # Top-level handlers
118 # Top-level handlers
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120
120
121 class RequestHandler(web.RequestHandler):
121 class RequestHandler(web.RequestHandler):
122 """RequestHandler with default variable setting."""
122 """RequestHandler with default variable setting."""
123
123
124 def render(*args, **kwargs):
124 def render(*args, **kwargs):
125 kwargs.setdefault('message', '')
125 kwargs.setdefault('message', '')
126 return web.RequestHandler.render(*args, **kwargs)
126 return web.RequestHandler.render(*args, **kwargs)
127
127
128 class AuthenticatedHandler(RequestHandler):
128 class AuthenticatedHandler(RequestHandler):
129 """A RequestHandler with an authenticated user."""
129 """A RequestHandler with an authenticated user."""
130
130
131 def get_current_user(self):
131 def get_current_user(self):
132 user_id = self.get_secure_cookie("username")
132 user_id = self.get_secure_cookie("username")
133 # For now the user_id should not return empty, but it could eventually
133 # For now the user_id should not return empty, but it could eventually
134 if user_id == '':
134 if user_id == '':
135 user_id = 'anonymous'
135 user_id = 'anonymous'
136 if user_id is None:
136 if user_id is None:
137 # prevent extra Invalid cookie sig warnings:
137 # prevent extra Invalid cookie sig warnings:
138 self.clear_cookie('username')
138 self.clear_cookie('username')
139 if not self.application.password and not self.application.read_only:
139 if not self.application.password and not self.application.read_only:
140 user_id = 'anonymous'
140 user_id = 'anonymous'
141 return user_id
141 return user_id
142
142
143 @property
143 @property
144 def logged_in(self):
144 def logged_in(self):
145 """Is a user currently logged in?
145 """Is a user currently logged in?
146
146
147 """
147 """
148 user = self.get_current_user()
148 user = self.get_current_user()
149 return (user and not user == 'anonymous')
149 return (user and not user == 'anonymous')
150
150
151 @property
151 @property
152 def login_available(self):
152 def login_available(self):
153 """May a user proceed to log in?
153 """May a user proceed to log in?
154
154
155 This returns True if login capability is available, irrespective of
155 This returns True if login capability is available, irrespective of
156 whether the user is already logged in or not.
156 whether the user is already logged in or not.
157
157
158 """
158 """
159 return bool(self.application.password)
159 return bool(self.application.password)
160
160
161 @property
161 @property
162 def read_only(self):
162 def read_only(self):
163 """Is the notebook read-only?
163 """Is the notebook read-only?
164
164
165 """
165 """
166 return self.application.read_only
166 return self.application.read_only
167
167
168 @property
168 @property
169 def ws_url(self):
169 def ws_url(self):
170 """websocket url matching the current request
170 """websocket url matching the current request
171
171
172 turns http[s]://host[:port] into
172 turns http[s]://host[:port] into
173 ws[s]://host[:port]
173 ws[s]://host[:port]
174 """
174 """
175 proto = self.request.protocol.replace('http', 'ws')
175 proto = self.request.protocol.replace('http', 'ws')
176 host = self.application.ipython_app.websocket_host # default to config value
176 host = self.application.ipython_app.websocket_host # default to config value
177 if host == '':
177 if host == '':
178 host = self.request.host # get from request
178 host = self.request.host # get from request
179 return "%s://%s" % (proto, host)
179 return "%s://%s" % (proto, host)
180
180
181
181
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 """static files should only be accessible when logged in"""
183 """static files should only be accessible when logged in"""
184
184
185 @authenticate_unless_readonly
185 @authenticate_unless_readonly
186 def get(self, path):
186 def get(self, path):
187 return web.StaticFileHandler.get(self, path)
187 return web.StaticFileHandler.get(self, path)
188
188
189
189
190 class ProjectDashboardHandler(AuthenticatedHandler):
190 class ProjectDashboardHandler(AuthenticatedHandler):
191
191
192 @authenticate_unless_readonly
192 @authenticate_unless_readonly
193 def get(self):
193 def get(self):
194 nbm = self.application.notebook_manager
194 nbm = self.application.notebook_manager
195 project = nbm.notebook_dir
195 project = nbm.notebook_dir
196 self.render(
196 self.render(
197 'projectdashboard.html', project=project,
197 'projectdashboard.html', project=project,
198 base_project_url=self.application.ipython_app.base_project_url,
198 base_project_url=self.application.ipython_app.base_project_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 read_only=self.read_only,
200 read_only=self.read_only,
201 logged_in=self.logged_in,
201 logged_in=self.logged_in,
202 login_available=self.login_available
202 login_available=self.login_available
203 )
203 )
204
204
205
205
206 class LoginHandler(AuthenticatedHandler):
206 class LoginHandler(AuthenticatedHandler):
207
207
208 def _render(self, message=None):
208 def _render(self, message=None):
209 self.render('login.html',
209 self.render('login.html',
210 next=self.get_argument('next', default='/'),
210 next=self.get_argument('next', default='/'),
211 read_only=self.read_only,
211 read_only=self.read_only,
212 logged_in=self.logged_in,
212 logged_in=self.logged_in,
213 login_available=self.login_available,
213 login_available=self.login_available,
214 base_project_url=self.application.ipython_app.base_project_url,
214 base_project_url=self.application.ipython_app.base_project_url,
215 message=message
215 message=message
216 )
216 )
217
217
218 def get(self):
218 def get(self):
219 if self.current_user:
219 if self.current_user:
220 self.redirect(self.get_argument('next', default='/'))
220 self.redirect(self.get_argument('next', default='/'))
221 else:
221 else:
222 self._render()
222 self._render()
223
223
224 def post(self):
224 def post(self):
225 pwd = self.get_argument('password', default=u'')
225 pwd = self.get_argument('password', default=u'')
226 if self.application.password:
226 if self.application.password:
227 if passwd_check(self.application.password, pwd):
227 if passwd_check(self.application.password, pwd):
228 self.set_secure_cookie('username', str(uuid.uuid4()))
228 self.set_secure_cookie('username', str(uuid.uuid4()))
229 else:
229 else:
230 self._render(message={'error': 'Invalid password'})
230 self._render(message={'error': 'Invalid password'})
231 return
231 return
232
232
233 self.redirect(self.get_argument('next', default='/'))
233 self.redirect(self.get_argument('next', default='/'))
234
234
235
235
236 class LogoutHandler(AuthenticatedHandler):
236 class LogoutHandler(AuthenticatedHandler):
237
237
238 def get(self):
238 def get(self):
239 self.clear_cookie('username')
239 self.clear_cookie('username')
240 if self.login_available:
240 if self.login_available:
241 message = {'info': 'Successfully logged out.'}
241 message = {'info': 'Successfully logged out.'}
242 else:
242 else:
243 message = {'warning': 'Cannot log out. Notebook authentication '
243 message = {'warning': 'Cannot log out. Notebook authentication '
244 'is disabled.'}
244 'is disabled.'}
245
245
246 self.render('logout.html',
246 self.render('logout.html',
247 read_only=self.read_only,
247 read_only=self.read_only,
248 logged_in=self.logged_in,
248 logged_in=self.logged_in,
249 login_available=self.login_available,
249 login_available=self.login_available,
250 base_project_url=self.application.ipython_app.base_project_url,
250 base_project_url=self.application.ipython_app.base_project_url,
251 message=message)
251 message=message)
252
252
253
253
254 class NewHandler(AuthenticatedHandler):
254 class NewHandler(AuthenticatedHandler):
255
255
256 @web.authenticated
256 @web.authenticated
257 def get(self):
257 def get(self):
258 nbm = self.application.notebook_manager
258 nbm = self.application.notebook_manager
259 project = nbm.notebook_dir
259 project = nbm.notebook_dir
260 notebook_id = nbm.new_notebook()
260 notebook_id = nbm.new_notebook()
261 self.render(
261 self.render(
262 'notebook.html', project=project,
262 'notebook.html', project=project,
263 notebook_id=notebook_id,
263 notebook_id=notebook_id,
264 base_project_url=self.application.ipython_app.base_project_url,
264 base_project_url=self.application.ipython_app.base_project_url,
265 base_kernel_url=self.application.ipython_app.base_kernel_url,
265 base_kernel_url=self.application.ipython_app.base_kernel_url,
266 kill_kernel=False,
266 kill_kernel=False,
267 read_only=False,
267 read_only=False,
268 logged_in=self.logged_in,
268 logged_in=self.logged_in,
269 login_available=self.login_available,
269 login_available=self.login_available,
270 mathjax_url=self.application.ipython_app.mathjax_url,
270 mathjax_url=self.application.ipython_app.mathjax_url,
271 )
271 )
272
272
273
273
274 class NamedNotebookHandler(AuthenticatedHandler):
274 class NamedNotebookHandler(AuthenticatedHandler):
275
275
276 @authenticate_unless_readonly
276 @authenticate_unless_readonly
277 def get(self, notebook_id):
277 def get(self, notebook_id):
278 nbm = self.application.notebook_manager
278 nbm = self.application.notebook_manager
279 project = nbm.notebook_dir
279 project = nbm.notebook_dir
280 if not nbm.notebook_exists(notebook_id):
280 if not nbm.notebook_exists(notebook_id):
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
282
282
283 self.render(
283 self.render(
284 'notebook.html', project=project,
284 'notebook.html', project=project,
285 notebook_id=notebook_id,
285 notebook_id=notebook_id,
286 base_project_url=self.application.ipython_app.base_project_url,
286 base_project_url=self.application.ipython_app.base_project_url,
287 base_kernel_url=self.application.ipython_app.base_kernel_url,
287 base_kernel_url=self.application.ipython_app.base_kernel_url,
288 kill_kernel=False,
288 kill_kernel=False,
289 read_only=self.read_only,
289 read_only=self.read_only,
290 logged_in=self.logged_in,
290 logged_in=self.logged_in,
291 login_available=self.login_available,
291 login_available=self.login_available,
292 mathjax_url=self.application.ipython_app.mathjax_url,
292 mathjax_url=self.application.ipython_app.mathjax_url,
293 )
293 )
294
294
295
295
296 class PrintNotebookHandler(AuthenticatedHandler):
296 class PrintNotebookHandler(AuthenticatedHandler):
297
297
298 @authenticate_unless_readonly
298 @authenticate_unless_readonly
299 def get(self, notebook_id):
299 def get(self, notebook_id):
300 nbm = self.application.notebook_manager
300 nbm = self.application.notebook_manager
301 project = nbm.notebook_dir
301 project = nbm.notebook_dir
302 if not nbm.notebook_exists(notebook_id):
302 if not nbm.notebook_exists(notebook_id):
303 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
303 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
304
304
305 self.render(
305 self.render(
306 'printnotebook.html', project=project,
306 'printnotebook.html', project=project,
307 notebook_id=notebook_id,
307 notebook_id=notebook_id,
308 base_project_url=self.application.ipython_app.base_project_url,
308 base_project_url=self.application.ipython_app.base_project_url,
309 base_kernel_url=self.application.ipython_app.base_kernel_url,
309 base_kernel_url=self.application.ipython_app.base_kernel_url,
310 kill_kernel=False,
310 kill_kernel=False,
311 read_only=self.read_only,
311 read_only=self.read_only,
312 logged_in=self.logged_in,
312 logged_in=self.logged_in,
313 login_available=self.login_available,
313 login_available=self.login_available,
314 mathjax_url=self.application.ipython_app.mathjax_url,
314 mathjax_url=self.application.ipython_app.mathjax_url,
315 )
315 )
316
316
317 #-----------------------------------------------------------------------------
317 #-----------------------------------------------------------------------------
318 # Kernel handlers
318 # Kernel handlers
319 #-----------------------------------------------------------------------------
319 #-----------------------------------------------------------------------------
320
320
321
321
322 class MainKernelHandler(AuthenticatedHandler):
322 class MainKernelHandler(AuthenticatedHandler):
323
323
324 @web.authenticated
324 @web.authenticated
325 def get(self):
325 def get(self):
326 km = self.application.kernel_manager
326 km = self.application.kernel_manager
327 self.finish(jsonapi.dumps(km.kernel_ids))
327 self.finish(jsonapi.dumps(km.kernel_ids))
328
328
329 @web.authenticated
329 @web.authenticated
330 def post(self):
330 def post(self):
331 km = self.application.kernel_manager
331 km = self.application.kernel_manager
332 notebook_id = self.get_argument('notebook', default=None)
332 notebook_id = self.get_argument('notebook', default=None)
333 kernel_id = km.start_kernel(notebook_id)
333 kernel_id = km.start_kernel(notebook_id)
334 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
334 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
335 self.set_header('Location', '/'+kernel_id)
335 self.set_header('Location', '/'+kernel_id)
336 self.finish(jsonapi.dumps(data))
336 self.finish(jsonapi.dumps(data))
337
337
338
338
339 class KernelHandler(AuthenticatedHandler):
339 class KernelHandler(AuthenticatedHandler):
340
340
341 SUPPORTED_METHODS = ('POST','DELETE')
341 SUPPORTED_METHODS = ('DELETE')
342
342
343 @web.authenticated
343 @web.authenticated
344 def delete(self, kernel_id):
344 def delete(self, kernel_id):
345 km = self.application.kernel_manager
345 km = self.application.kernel_manager
346 km.kill_kernel(kernel_id)
346 km.kill_kernel(kernel_id)
347 self.set_status(204)
347 self.set_status(204)
348 self.finish()
348 self.finish()
349
349
350
350 class KernelActionHandler(AuthenticatedHandler):
351 class KernelActionHandler(AuthenticatedHandler):
351
352
352 @web.authenticated
353 @web.authenticated
353 def post(self, kernel_id, action):
354 def post(self, kernel_id, action):
354 km = self.application.kernel_manager
355 km = self.application.kernel_manager
355 if action == 'interrupt':
356 if action == 'interrupt':
356 km.interrupt_kernel(kernel_id)
357 km.interrupt_kernel(kernel_id)
357 self.set_status(204)
358 self.set_status(204)
358 if action == 'restart':
359 if action == 'restart':
359 new_kernel_id = km.restart_kernel(kernel_id)
360 new_kernel_id = km.restart_kernel(kernel_id)
360 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
361 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
361 self.set_header('Location', '/'+new_kernel_id)
362 self.set_header('Location', '/'+new_kernel_id)
362 self.write(jsonapi.dumps(data))
363 self.write(jsonapi.dumps(data))
363 self.finish()
364 self.finish()
364
365
365
366
366 class ZMQStreamHandler(websocket.WebSocketHandler):
367 class ZMQStreamHandler(websocket.WebSocketHandler):
367
368
368 def _reserialize_reply(self, msg_list):
369 def _reserialize_reply(self, msg_list):
369 """Reserialize a reply message using JSON.
370 """Reserialize a reply message using JSON.
370
371
371 This takes the msg list from the ZMQ socket, unserializes it using
372 This takes the msg list from the ZMQ socket, unserializes it using
372 self.session and then serializes the result using JSON. This method
373 self.session and then serializes the result using JSON. This method
373 should be used by self._on_zmq_reply to build messages that can
374 should be used by self._on_zmq_reply to build messages that can
374 be sent back to the browser.
375 be sent back to the browser.
375 """
376 """
376 idents, msg_list = self.session.feed_identities(msg_list)
377 idents, msg_list = self.session.feed_identities(msg_list)
377 msg = self.session.unserialize(msg_list)
378 msg = self.session.unserialize(msg_list)
378 try:
379 try:
379 msg['header'].pop('date')
380 msg['header'].pop('date')
380 except KeyError:
381 except KeyError:
381 pass
382 pass
382 try:
383 try:
383 msg['parent_header'].pop('date')
384 msg['parent_header'].pop('date')
384 except KeyError:
385 except KeyError:
385 pass
386 pass
386 msg.pop('buffers')
387 msg.pop('buffers')
387 return jsonapi.dumps(msg)
388 return jsonapi.dumps(msg)
388
389
389 def _on_zmq_reply(self, msg_list):
390 def _on_zmq_reply(self, msg_list):
390 try:
391 try:
391 msg = self._reserialize_reply(msg_list)
392 msg = self._reserialize_reply(msg_list)
392 except:
393 except:
393 self.application.log.critical("Malformed message: %r" % msg_list)
394 self.application.log.critical("Malformed message: %r" % msg_list)
394 else:
395 else:
395 self.write_message(msg)
396 self.write_message(msg)
396
397
397 def allow_draft76(self):
398 def allow_draft76(self):
398 """Allow draft 76, until browsers such as Safari update to RFC 6455.
399 """Allow draft 76, until browsers such as Safari update to RFC 6455.
399
400
400 This has been disabled by default in tornado in release 2.2.0, and
401 This has been disabled by default in tornado in release 2.2.0, and
401 support will be removed in later versions.
402 support will be removed in later versions.
402 """
403 """
403 return True
404 return True
404
405
405
406
406 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
407 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
407
408
408 def open(self, kernel_id):
409 def open(self, kernel_id):
409 self.kernel_id = kernel_id.decode('ascii')
410 self.kernel_id = kernel_id.decode('ascii')
410 try:
411 try:
411 cfg = self.application.ipython_app.config
412 cfg = self.application.ipython_app.config
412 except AttributeError:
413 except AttributeError:
413 # protect from the case where this is run from something other than
414 # protect from the case where this is run from something other than
414 # the notebook app:
415 # the notebook app:
415 cfg = None
416 cfg = None
416 self.session = Session(config=cfg)
417 self.session = Session(config=cfg)
417 self.save_on_message = self.on_message
418 self.save_on_message = self.on_message
418 self.on_message = self.on_first_message
419 self.on_message = self.on_first_message
419
420
420 def get_current_user(self):
421 def get_current_user(self):
421 user_id = self.get_secure_cookie("username")
422 user_id = self.get_secure_cookie("username")
422 if user_id == '' or (user_id is None and not self.application.password):
423 if user_id == '' or (user_id is None and not self.application.password):
423 user_id = 'anonymous'
424 user_id = 'anonymous'
424 return user_id
425 return user_id
425
426
426 def _inject_cookie_message(self, msg):
427 def _inject_cookie_message(self, msg):
427 """Inject the first message, which is the document cookie,
428 """Inject the first message, which is the document cookie,
428 for authentication."""
429 for authentication."""
429 if isinstance(msg, unicode):
430 if isinstance(msg, unicode):
430 # Cookie can't constructor doesn't accept unicode strings for some reason
431 # Cookie can't constructor doesn't accept unicode strings for some reason
431 msg = msg.encode('utf8', 'replace')
432 msg = msg.encode('utf8', 'replace')
432 try:
433 try:
433 self.request._cookies = Cookie.SimpleCookie(msg)
434 self.request._cookies = Cookie.SimpleCookie(msg)
434 except:
435 except:
435 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
436 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
436
437
437 def on_first_message(self, msg):
438 def on_first_message(self, msg):
438 self._inject_cookie_message(msg)
439 self._inject_cookie_message(msg)
439 if self.get_current_user() is None:
440 if self.get_current_user() is None:
440 logging.warn("Couldn't authenticate WebSocket connection")
441 logging.warn("Couldn't authenticate WebSocket connection")
441 raise web.HTTPError(403)
442 raise web.HTTPError(403)
442 self.on_message = self.save_on_message
443 self.on_message = self.save_on_message
443
444
444
445
445 class IOPubHandler(AuthenticatedZMQStreamHandler):
446 class IOPubHandler(AuthenticatedZMQStreamHandler):
446
447
447 def initialize(self, *args, **kwargs):
448 def initialize(self, *args, **kwargs):
448 self._kernel_alive = True
449 self._kernel_alive = True
449 self._beating = False
450 self._beating = False
450 self.iopub_stream = None
451 self.iopub_stream = None
451 self.hb_stream = None
452 self.hb_stream = None
452
453
453 def on_first_message(self, msg):
454 def on_first_message(self, msg):
454 try:
455 try:
455 super(IOPubHandler, self).on_first_message(msg)
456 super(IOPubHandler, self).on_first_message(msg)
456 except web.HTTPError:
457 except web.HTTPError:
457 self.close()
458 self.close()
458 return
459 return
459 km = self.application.kernel_manager
460 km = self.application.kernel_manager
460 self.time_to_dead = km.time_to_dead
461 self.time_to_dead = km.time_to_dead
461 self.first_beat = km.first_beat
462 self.first_beat = km.first_beat
462 kernel_id = self.kernel_id
463 kernel_id = self.kernel_id
463 try:
464 try:
464 self.iopub_stream = km.create_iopub_stream(kernel_id)
465 self.iopub_stream = km.create_iopub_stream(kernel_id)
465 self.hb_stream = km.create_hb_stream(kernel_id)
466 self.hb_stream = km.create_hb_stream(kernel_id)
466 except web.HTTPError:
467 except web.HTTPError:
467 # WebSockets don't response to traditional error codes so we
468 # WebSockets don't response to traditional error codes so we
468 # close the connection.
469 # close the connection.
469 if not self.stream.closed():
470 if not self.stream.closed():
470 self.stream.close()
471 self.stream.close()
471 self.close()
472 self.close()
472 else:
473 else:
473 self.iopub_stream.on_recv(self._on_zmq_reply)
474 self.iopub_stream.on_recv(self._on_zmq_reply)
474 self.start_hb(self.kernel_died)
475 self.start_hb(self.kernel_died)
475
476
476 def on_message(self, msg):
477 def on_message(self, msg):
477 pass
478 pass
478
479
479 def on_close(self):
480 def on_close(self):
480 # This method can be called twice, once by self.kernel_died and once
481 # This method can be called twice, once by self.kernel_died and once
481 # from the WebSocket close event. If the WebSocket connection is
482 # from the WebSocket close event. If the WebSocket connection is
482 # closed before the ZMQ streams are setup, they could be None.
483 # closed before the ZMQ streams are setup, they could be None.
483 self.stop_hb()
484 self.stop_hb()
484 if self.iopub_stream is not None and not self.iopub_stream.closed():
485 if self.iopub_stream is not None and not self.iopub_stream.closed():
485 self.iopub_stream.on_recv(None)
486 self.iopub_stream.on_recv(None)
486 self.iopub_stream.close()
487 self.iopub_stream.close()
487 if self.hb_stream is not None and not self.hb_stream.closed():
488 if self.hb_stream is not None and not self.hb_stream.closed():
488 self.hb_stream.close()
489 self.hb_stream.close()
489
490
490 def start_hb(self, callback):
491 def start_hb(self, callback):
491 """Start the heartbeating and call the callback if the kernel dies."""
492 """Start the heartbeating and call the callback if the kernel dies."""
492 if not self._beating:
493 if not self._beating:
493 self._kernel_alive = True
494 self._kernel_alive = True
494
495
495 def ping_or_dead():
496 def ping_or_dead():
496 self.hb_stream.flush()
497 self.hb_stream.flush()
497 if self._kernel_alive:
498 if self._kernel_alive:
498 self._kernel_alive = False
499 self._kernel_alive = False
499 self.hb_stream.send(b'ping')
500 self.hb_stream.send(b'ping')
500 # flush stream to force immediate socket send
501 # flush stream to force immediate socket send
501 self.hb_stream.flush()
502 self.hb_stream.flush()
502 else:
503 else:
503 try:
504 try:
504 callback()
505 callback()
505 except:
506 except:
506 pass
507 pass
507 finally:
508 finally:
508 self.stop_hb()
509 self.stop_hb()
509
510
510 def beat_received(msg):
511 def beat_received(msg):
511 self._kernel_alive = True
512 self._kernel_alive = True
512
513
513 self.hb_stream.on_recv(beat_received)
514 self.hb_stream.on_recv(beat_received)
514 loop = ioloop.IOLoop.instance()
515 loop = ioloop.IOLoop.instance()
515 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
516 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
516 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
517 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
517 self._beating= True
518 self._beating= True
518
519
519 def _really_start_hb(self):
520 def _really_start_hb(self):
520 """callback for delayed heartbeat start
521 """callback for delayed heartbeat start
521
522
522 Only start the hb loop if we haven't been closed during the wait.
523 Only start the hb loop if we haven't been closed during the wait.
523 """
524 """
524 if self._beating and not self.hb_stream.closed():
525 if self._beating and not self.hb_stream.closed():
525 self._hb_periodic_callback.start()
526 self._hb_periodic_callback.start()
526
527
527 def stop_hb(self):
528 def stop_hb(self):
528 """Stop the heartbeating and cancel all related callbacks."""
529 """Stop the heartbeating and cancel all related callbacks."""
529 if self._beating:
530 if self._beating:
530 self._beating = False
531 self._beating = False
531 self._hb_periodic_callback.stop()
532 self._hb_periodic_callback.stop()
532 if not self.hb_stream.closed():
533 if not self.hb_stream.closed():
533 self.hb_stream.on_recv(None)
534 self.hb_stream.on_recv(None)
534
535
535 def kernel_died(self):
536 def kernel_died(self):
536 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
537 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
537 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
538 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
538 self.write_message(
539 self.write_message(
539 {'header': {'msg_type': 'status'},
540 {'header': {'msg_type': 'status'},
540 'parent_header': {},
541 'parent_header': {},
541 'content': {'execution_state':'dead'}
542 'content': {'execution_state':'dead'}
542 }
543 }
543 )
544 )
544 self.on_close()
545 self.on_close()
545
546
546
547
547 class ShellHandler(AuthenticatedZMQStreamHandler):
548 class ShellHandler(AuthenticatedZMQStreamHandler):
548
549
549 def initialize(self, *args, **kwargs):
550 def initialize(self, *args, **kwargs):
550 self.shell_stream = None
551 self.shell_stream = None
551
552
552 def on_first_message(self, msg):
553 def on_first_message(self, msg):
553 try:
554 try:
554 super(ShellHandler, self).on_first_message(msg)
555 super(ShellHandler, self).on_first_message(msg)
555 except web.HTTPError:
556 except web.HTTPError:
556 self.close()
557 self.close()
557 return
558 return
558 km = self.application.kernel_manager
559 km = self.application.kernel_manager
559 self.max_msg_size = km.max_msg_size
560 self.max_msg_size = km.max_msg_size
560 kernel_id = self.kernel_id
561 kernel_id = self.kernel_id
561 try:
562 try:
562 self.shell_stream = km.create_shell_stream(kernel_id)
563 self.shell_stream = km.create_shell_stream(kernel_id)
563 except web.HTTPError:
564 except web.HTTPError:
564 # WebSockets don't response to traditional error codes so we
565 # WebSockets don't response to traditional error codes so we
565 # close the connection.
566 # close the connection.
566 if not self.stream.closed():
567 if not self.stream.closed():
567 self.stream.close()
568 self.stream.close()
568 self.close()
569 self.close()
569 else:
570 else:
570 self.shell_stream.on_recv(self._on_zmq_reply)
571 self.shell_stream.on_recv(self._on_zmq_reply)
571
572
572 def on_message(self, msg):
573 def on_message(self, msg):
573 if len(msg) < self.max_msg_size:
574 if len(msg) < self.max_msg_size:
574 msg = jsonapi.loads(msg)
575 msg = jsonapi.loads(msg)
575 self.session.send(self.shell_stream, msg)
576 self.session.send(self.shell_stream, msg)
576
577
577 def on_close(self):
578 def on_close(self):
578 # Make sure the stream exists and is not already closed.
579 # Make sure the stream exists and is not already closed.
579 if self.shell_stream is not None and not self.shell_stream.closed():
580 if self.shell_stream is not None and not self.shell_stream.closed():
580 self.shell_stream.close()
581 self.shell_stream.close()
581
582
582
583
583 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
584 # Notebook web service handlers
585 # Notebook web service handlers
585 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
586
587
587 class NotebookRootHandler(AuthenticatedHandler):
588 class NotebookRootHandler(AuthenticatedHandler):
588
589
589 @authenticate_unless_readonly
590 @authenticate_unless_readonly
590 def get(self):
591 def get(self):
591 nbm = self.application.notebook_manager
592 nbm = self.application.notebook_manager
592 km = self.application.kernel_manager
593 km = self.application.kernel_manager
593 files = nbm.list_notebooks()
594 files = nbm.list_notebooks()
594 #kernel_for_notebook
595 nlist=[]
596 for f in files :
595 for f in files :
597 nid = f['notebook_id']
596 nid = f['notebook_id']
598 kid = km.kernel_for_notebook(nid)
597 kid = km.kernel_for_notebook(nid)
599 if kid is not None:
598 if kid is not None:
600 f['kernel_status']=kid
599 f['kernel_status']=kid
601 else:
600 else:
602 f['kernel_status']='off'
601 f['kernel_status']='off'
603 nlist.append(f)
602 self.finish(jsonapi.dumps(files))
604 print(files)
605 self.finish(jsonapi.dumps(nlist))
606
603
607 @web.authenticated
604 @web.authenticated
608 def post(self):
605 def post(self):
609 nbm = self.application.notebook_manager
606 nbm = self.application.notebook_manager
610 body = self.request.body.strip()
607 body = self.request.body.strip()
611 format = self.get_argument('format', default='json')
608 format = self.get_argument('format', default='json')
612 name = self.get_argument('name', default=None)
609 name = self.get_argument('name', default=None)
613 if body:
610 if body:
614 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
611 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
615 else:
612 else:
616 notebook_id = nbm.new_notebook()
613 notebook_id = nbm.new_notebook()
617 self.set_header('Location', '/'+notebook_id)
614 self.set_header('Location', '/'+notebook_id)
618 self.finish(jsonapi.dumps(notebook_id))
615 self.finish(jsonapi.dumps(notebook_id))
619
616
620
617
621 class NotebookHandler(AuthenticatedHandler):
618 class NotebookHandler(AuthenticatedHandler):
622
619
623 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
620 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
624
621
625 @authenticate_unless_readonly
622 @authenticate_unless_readonly
626 def get(self, notebook_id):
623 def get(self, notebook_id):
627 nbm = self.application.notebook_manager
624 nbm = self.application.notebook_manager
628 format = self.get_argument('format', default='json')
625 format = self.get_argument('format', default='json')
629 last_mod, name, data = nbm.get_notebook(notebook_id, format)
626 last_mod, name, data = nbm.get_notebook(notebook_id, format)
630
627
631 if format == u'json':
628 if format == u'json':
632 self.set_header('Content-Type', 'application/json')
629 self.set_header('Content-Type', 'application/json')
633 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
630 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
634 elif format == u'py':
631 elif format == u'py':
635 self.set_header('Content-Type', 'application/x-python')
632 self.set_header('Content-Type', 'application/x-python')
636 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
633 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
637 self.set_header('Last-Modified', last_mod)
634 self.set_header('Last-Modified', last_mod)
638 self.finish(data)
635 self.finish(data)
639
636
640 @web.authenticated
637 @web.authenticated
641 def put(self, notebook_id):
638 def put(self, notebook_id):
642 nbm = self.application.notebook_manager
639 nbm = self.application.notebook_manager
643 format = self.get_argument('format', default='json')
640 format = self.get_argument('format', default='json')
644 name = self.get_argument('name', default=None)
641 name = self.get_argument('name', default=None)
645 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
642 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
646 self.set_status(204)
643 self.set_status(204)
647 self.finish()
644 self.finish()
648
645
649 @web.authenticated
646 @web.authenticated
650 def delete(self, notebook_id):
647 def delete(self, notebook_id):
651 nbm = self.application.notebook_manager
648 nbm = self.application.notebook_manager
652 nbm.delete_notebook(notebook_id)
649 nbm.delete_notebook(notebook_id)
653 self.set_status(204)
650 self.set_status(204)
654 self.finish()
651 self.finish()
655
652
656
653
657 class NotebookCopyHandler(AuthenticatedHandler):
654 class NotebookCopyHandler(AuthenticatedHandler):
658
655
659 @web.authenticated
656 @web.authenticated
660 def get(self, notebook_id):
657 def get(self, notebook_id):
661 nbm = self.application.notebook_manager
658 nbm = self.application.notebook_manager
662 project = nbm.notebook_dir
659 project = nbm.notebook_dir
663 notebook_id = nbm.copy_notebook(notebook_id)
660 notebook_id = nbm.copy_notebook(notebook_id)
664 self.render(
661 self.render(
665 'notebook.html', project=project,
662 'notebook.html', project=project,
666 notebook_id=notebook_id,
663 notebook_id=notebook_id,
667 base_project_url=self.application.ipython_app.base_project_url,
664 base_project_url=self.application.ipython_app.base_project_url,
668 base_kernel_url=self.application.ipython_app.base_kernel_url,
665 base_kernel_url=self.application.ipython_app.base_kernel_url,
669 kill_kernel=False,
666 kill_kernel=False,
670 read_only=False,
667 read_only=False,
671 logged_in=self.logged_in,
668 logged_in=self.logged_in,
672 login_available=self.login_available,
669 login_available=self.login_available,
673 mathjax_url=self.application.ipython_app.mathjax_url,
670 mathjax_url=self.application.ipython_app.mathjax_url,
674 )
671 )
675
672
676
673
677 #-----------------------------------------------------------------------------
674 #-----------------------------------------------------------------------------
678 # Cluster handlers
675 # Cluster handlers
679 #-----------------------------------------------------------------------------
676 #-----------------------------------------------------------------------------
680
677
681
678
682 class MainClusterHandler(AuthenticatedHandler):
679 class MainClusterHandler(AuthenticatedHandler):
683
680
684 @web.authenticated
681 @web.authenticated
685 def get(self):
682 def get(self):
686 cm = self.application.cluster_manager
683 cm = self.application.cluster_manager
687 self.finish(jsonapi.dumps(cm.list_profiles()))
684 self.finish(jsonapi.dumps(cm.list_profiles()))
688
685
689
686
690 class ClusterProfileHandler(AuthenticatedHandler):
687 class ClusterProfileHandler(AuthenticatedHandler):
691
688
692 @web.authenticated
689 @web.authenticated
693 def get(self, profile):
690 def get(self, profile):
694 cm = self.application.cluster_manager
691 cm = self.application.cluster_manager
695 self.finish(jsonapi.dumps(cm.profile_info(profile)))
692 self.finish(jsonapi.dumps(cm.profile_info(profile)))
696
693
697
694
698 class ClusterActionHandler(AuthenticatedHandler):
695 class ClusterActionHandler(AuthenticatedHandler):
699
696
700 @web.authenticated
697 @web.authenticated
701 def post(self, profile, action):
698 def post(self, profile, action):
702 cm = self.application.cluster_manager
699 cm = self.application.cluster_manager
703 if action == 'start':
700 if action == 'start':
704 n = self.get_argument('n',default=None)
701 n = self.get_argument('n',default=None)
705 if n is None:
702 if n is None:
706 data = cm.start_cluster(profile)
703 data = cm.start_cluster(profile)
707 else:
704 else:
708 data = cm.start_cluster(profile,int(n))
705 data = cm.start_cluster(profile,int(n))
709 if action == 'stop':
706 if action == 'stop':
710 data = cm.stop_cluster(profile)
707 data = cm.stop_cluster(profile)
711 self.finish(jsonapi.dumps(data))
708 self.finish(jsonapi.dumps(data))
712
709
713
710
714 #-----------------------------------------------------------------------------
711 #-----------------------------------------------------------------------------
715 # RST web service handlers
712 # RST web service handlers
716 #-----------------------------------------------------------------------------
713 #-----------------------------------------------------------------------------
717
714
718
715
719 class RSTHandler(AuthenticatedHandler):
716 class RSTHandler(AuthenticatedHandler):
720
717
721 @web.authenticated
718 @web.authenticated
722 def post(self):
719 def post(self):
723 if publish_string is None:
720 if publish_string is None:
724 raise web.HTTPError(503, u'docutils not available')
721 raise web.HTTPError(503, u'docutils not available')
725 body = self.request.body.strip()
722 body = self.request.body.strip()
726 source = body
723 source = body
727 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
724 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
728 defaults = {'file_insertion_enabled': 0,
725 defaults = {'file_insertion_enabled': 0,
729 'raw_enabled': 0,
726 'raw_enabled': 0,
730 '_disable_config': 1,
727 '_disable_config': 1,
731 'stylesheet_path': 0
728 'stylesheet_path': 0
732 # 'template': template_path
729 # 'template': template_path
733 }
730 }
734 try:
731 try:
735 html = publish_string(source, writer_name='html',
732 html = publish_string(source, writer_name='html',
736 settings_overrides=defaults
733 settings_overrides=defaults
737 )
734 )
738 except:
735 except:
739 raise web.HTTPError(400, u'Invalid RST')
736 raise web.HTTPError(400, u'Invalid RST')
740 print html
737 print html
741 self.set_header('Content-Type', 'text/html')
738 self.set_header('Content-Type', 'text/html')
742 self.finish(html)
739 self.finish(html)
743
740
744
741
General Comments 0
You need to be logged in to leave comments. Login now