##// END OF EJS Templates
allow draft76 websockets (Safari)...
MinRK -
Show More
@@ -1,630 +1,638
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 if self.application.read_only:
111 if self.application.read_only:
112 return f(self, *args, **kwargs)
112 return f(self, *args, **kwargs)
113 else:
113 else:
114 return auth_f(self, *args, **kwargs)
114 return auth_f(self, *args, **kwargs)
115
115
116 #-----------------------------------------------------------------------------
116 #-----------------------------------------------------------------------------
117 # Top-level handlers
117 # Top-level handlers
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119
119
120 class RequestHandler(web.RequestHandler):
120 class RequestHandler(web.RequestHandler):
121 """RequestHandler with default variable setting."""
121 """RequestHandler with default variable setting."""
122
122
123 def render(*args, **kwargs):
123 def render(*args, **kwargs):
124 kwargs.setdefault('message', '')
124 kwargs.setdefault('message', '')
125 return web.RequestHandler.render(*args, **kwargs)
125 return web.RequestHandler.render(*args, **kwargs)
126
126
127 class AuthenticatedHandler(RequestHandler):
127 class AuthenticatedHandler(RequestHandler):
128 """A RequestHandler with an authenticated user."""
128 """A RequestHandler with an authenticated user."""
129
129
130 def get_current_user(self):
130 def get_current_user(self):
131 user_id = self.get_secure_cookie("username")
131 user_id = self.get_secure_cookie("username")
132 # For now the user_id should not return empty, but it could eventually
132 # For now the user_id should not return empty, but it could eventually
133 if user_id == '':
133 if user_id == '':
134 user_id = 'anonymous'
134 user_id = 'anonymous'
135 if user_id is None:
135 if user_id is None:
136 # prevent extra Invalid cookie sig warnings:
136 # prevent extra Invalid cookie sig warnings:
137 self.clear_cookie('username')
137 self.clear_cookie('username')
138 if not self.application.password and not self.application.read_only:
138 if not self.application.password and not self.application.read_only:
139 user_id = 'anonymous'
139 user_id = 'anonymous'
140 return user_id
140 return user_id
141
141
142 @property
142 @property
143 def logged_in(self):
143 def logged_in(self):
144 """Is a user currently logged in?
144 """Is a user currently logged in?
145
145
146 """
146 """
147 user = self.get_current_user()
147 user = self.get_current_user()
148 return (user and not user == 'anonymous')
148 return (user and not user == 'anonymous')
149
149
150 @property
150 @property
151 def login_available(self):
151 def login_available(self):
152 """May a user proceed to log in?
152 """May a user proceed to log in?
153
153
154 This returns True if login capability is available, irrespective of
154 This returns True if login capability is available, irrespective of
155 whether the user is already logged in or not.
155 whether the user is already logged in or not.
156
156
157 """
157 """
158 return bool(self.application.password)
158 return bool(self.application.password)
159
159
160 @property
160 @property
161 def read_only(self):
161 def read_only(self):
162 """Is the notebook read-only?
162 """Is the notebook read-only?
163
163
164 """
164 """
165 return self.application.read_only
165 return self.application.read_only
166
166
167 @property
167 @property
168 def ws_url(self):
168 def ws_url(self):
169 """websocket url matching the current request
169 """websocket url matching the current request
170
170
171 turns http[s]://host[:port] into
171 turns http[s]://host[:port] into
172 ws[s]://host[:port]
172 ws[s]://host[:port]
173 """
173 """
174 proto = self.request.protocol.replace('http', 'ws')
174 proto = self.request.protocol.replace('http', 'ws')
175 return "%s://%s" % (proto, self.request.host)
175 return "%s://%s" % (proto, self.request.host)
176
176
177
177
178 class ProjectDashboardHandler(AuthenticatedHandler):
178 class ProjectDashboardHandler(AuthenticatedHandler):
179
179
180 @authenticate_unless_readonly
180 @authenticate_unless_readonly
181 def get(self):
181 def get(self):
182 nbm = self.application.notebook_manager
182 nbm = self.application.notebook_manager
183 project = nbm.notebook_dir
183 project = nbm.notebook_dir
184 self.render(
184 self.render(
185 'projectdashboard.html', project=project,
185 'projectdashboard.html', project=project,
186 base_project_url=u'/', base_kernel_url=u'/',
186 base_project_url=u'/', base_kernel_url=u'/',
187 read_only=self.read_only,
187 read_only=self.read_only,
188 logged_in=self.logged_in,
188 logged_in=self.logged_in,
189 login_available=self.login_available
189 login_available=self.login_available
190 )
190 )
191
191
192
192
193 class LoginHandler(AuthenticatedHandler):
193 class LoginHandler(AuthenticatedHandler):
194
194
195 def _render(self, message=None):
195 def _render(self, message=None):
196 self.render('login.html',
196 self.render('login.html',
197 next=self.get_argument('next', default='/'),
197 next=self.get_argument('next', default='/'),
198 read_only=self.read_only,
198 read_only=self.read_only,
199 logged_in=self.logged_in,
199 logged_in=self.logged_in,
200 login_available=self.login_available,
200 login_available=self.login_available,
201 message=message
201 message=message
202 )
202 )
203
203
204 def get(self):
204 def get(self):
205 if self.current_user:
205 if self.current_user:
206 self.redirect(self.get_argument('next', default='/'))
206 self.redirect(self.get_argument('next', default='/'))
207 else:
207 else:
208 self._render()
208 self._render()
209
209
210 def post(self):
210 def post(self):
211 pwd = self.get_argument('password', default=u'')
211 pwd = self.get_argument('password', default=u'')
212 if self.application.password:
212 if self.application.password:
213 if passwd_check(self.application.password, pwd):
213 if passwd_check(self.application.password, pwd):
214 self.set_secure_cookie('username', str(uuid.uuid4()))
214 self.set_secure_cookie('username', str(uuid.uuid4()))
215 else:
215 else:
216 self._render(message={'error': 'Invalid password'})
216 self._render(message={'error': 'Invalid password'})
217 return
217 return
218
218
219 self.redirect(self.get_argument('next', default='/'))
219 self.redirect(self.get_argument('next', default='/'))
220
220
221
221
222 class LogoutHandler(AuthenticatedHandler):
222 class LogoutHandler(AuthenticatedHandler):
223
223
224 def get(self):
224 def get(self):
225 self.clear_cookie('username')
225 self.clear_cookie('username')
226 if self.login_available:
226 if self.login_available:
227 message = {'info': 'Successfully logged out.'}
227 message = {'info': 'Successfully logged out.'}
228 else:
228 else:
229 message = {'warning': 'Cannot log out. Notebook authentication '
229 message = {'warning': 'Cannot log out. Notebook authentication '
230 'is disabled.'}
230 'is disabled.'}
231
231
232 self.render('logout.html',
232 self.render('logout.html',
233 read_only=self.read_only,
233 read_only=self.read_only,
234 logged_in=self.logged_in,
234 logged_in=self.logged_in,
235 login_available=self.login_available,
235 login_available=self.login_available,
236 message=message)
236 message=message)
237
237
238
238
239 class NewHandler(AuthenticatedHandler):
239 class NewHandler(AuthenticatedHandler):
240
240
241 @web.authenticated
241 @web.authenticated
242 def get(self):
242 def get(self):
243 nbm = self.application.notebook_manager
243 nbm = self.application.notebook_manager
244 project = nbm.notebook_dir
244 project = nbm.notebook_dir
245 notebook_id = nbm.new_notebook()
245 notebook_id = nbm.new_notebook()
246 self.render(
246 self.render(
247 'notebook.html', project=project,
247 'notebook.html', project=project,
248 notebook_id=notebook_id,
248 notebook_id=notebook_id,
249 base_project_url=u'/', base_kernel_url=u'/',
249 base_project_url=u'/', base_kernel_url=u'/',
250 kill_kernel=False,
250 kill_kernel=False,
251 read_only=False,
251 read_only=False,
252 logged_in=self.logged_in,
252 logged_in=self.logged_in,
253 login_available=self.login_available,
253 login_available=self.login_available,
254 mathjax_url=self.application.ipython_app.mathjax_url,
254 mathjax_url=self.application.ipython_app.mathjax_url,
255 )
255 )
256
256
257
257
258 class NamedNotebookHandler(AuthenticatedHandler):
258 class NamedNotebookHandler(AuthenticatedHandler):
259
259
260 @authenticate_unless_readonly
260 @authenticate_unless_readonly
261 def get(self, notebook_id):
261 def get(self, notebook_id):
262 nbm = self.application.notebook_manager
262 nbm = self.application.notebook_manager
263 project = nbm.notebook_dir
263 project = nbm.notebook_dir
264 if not nbm.notebook_exists(notebook_id):
264 if not nbm.notebook_exists(notebook_id):
265 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
265 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
266
266
267 self.render(
267 self.render(
268 'notebook.html', project=project,
268 'notebook.html', project=project,
269 notebook_id=notebook_id,
269 notebook_id=notebook_id,
270 base_project_url=u'/', base_kernel_url=u'/',
270 base_project_url=u'/', base_kernel_url=u'/',
271 kill_kernel=False,
271 kill_kernel=False,
272 read_only=self.read_only,
272 read_only=self.read_only,
273 logged_in=self.logged_in,
273 logged_in=self.logged_in,
274 login_available=self.login_available,
274 login_available=self.login_available,
275 mathjax_url=self.application.ipython_app.mathjax_url,
275 mathjax_url=self.application.ipython_app.mathjax_url,
276 )
276 )
277
277
278
278
279 #-----------------------------------------------------------------------------
279 #-----------------------------------------------------------------------------
280 # Kernel handlers
280 # Kernel handlers
281 #-----------------------------------------------------------------------------
281 #-----------------------------------------------------------------------------
282
282
283
283
284 class MainKernelHandler(AuthenticatedHandler):
284 class MainKernelHandler(AuthenticatedHandler):
285
285
286 @web.authenticated
286 @web.authenticated
287 def get(self):
287 def get(self):
288 km = self.application.kernel_manager
288 km = self.application.kernel_manager
289 self.finish(jsonapi.dumps(km.kernel_ids))
289 self.finish(jsonapi.dumps(km.kernel_ids))
290
290
291 @web.authenticated
291 @web.authenticated
292 def post(self):
292 def post(self):
293 km = self.application.kernel_manager
293 km = self.application.kernel_manager
294 notebook_id = self.get_argument('notebook', default=None)
294 notebook_id = self.get_argument('notebook', default=None)
295 kernel_id = km.start_kernel(notebook_id)
295 kernel_id = km.start_kernel(notebook_id)
296 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
296 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
297 self.set_header('Location', '/'+kernel_id)
297 self.set_header('Location', '/'+kernel_id)
298 self.finish(jsonapi.dumps(data))
298 self.finish(jsonapi.dumps(data))
299
299
300
300
301 class KernelHandler(AuthenticatedHandler):
301 class KernelHandler(AuthenticatedHandler):
302
302
303 SUPPORTED_METHODS = ('DELETE')
303 SUPPORTED_METHODS = ('DELETE')
304
304
305 @web.authenticated
305 @web.authenticated
306 def delete(self, kernel_id):
306 def delete(self, kernel_id):
307 km = self.application.kernel_manager
307 km = self.application.kernel_manager
308 km.kill_kernel(kernel_id)
308 km.kill_kernel(kernel_id)
309 self.set_status(204)
309 self.set_status(204)
310 self.finish()
310 self.finish()
311
311
312
312
313 class KernelActionHandler(AuthenticatedHandler):
313 class KernelActionHandler(AuthenticatedHandler):
314
314
315 @web.authenticated
315 @web.authenticated
316 def post(self, kernel_id, action):
316 def post(self, kernel_id, action):
317 km = self.application.kernel_manager
317 km = self.application.kernel_manager
318 if action == 'interrupt':
318 if action == 'interrupt':
319 km.interrupt_kernel(kernel_id)
319 km.interrupt_kernel(kernel_id)
320 self.set_status(204)
320 self.set_status(204)
321 if action == 'restart':
321 if action == 'restart':
322 new_kernel_id = km.restart_kernel(kernel_id)
322 new_kernel_id = km.restart_kernel(kernel_id)
323 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
323 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
324 self.set_header('Location', '/'+new_kernel_id)
324 self.set_header('Location', '/'+new_kernel_id)
325 self.write(jsonapi.dumps(data))
325 self.write(jsonapi.dumps(data))
326 self.finish()
326 self.finish()
327
327
328
328
329 class ZMQStreamHandler(websocket.WebSocketHandler):
329 class ZMQStreamHandler(websocket.WebSocketHandler):
330
330
331 def _reserialize_reply(self, msg_list):
331 def _reserialize_reply(self, msg_list):
332 """Reserialize a reply message using JSON.
332 """Reserialize a reply message using JSON.
333
333
334 This takes the msg list from the ZMQ socket, unserializes it using
334 This takes the msg list from the ZMQ socket, unserializes it using
335 self.session and then serializes the result using JSON. This method
335 self.session and then serializes the result using JSON. This method
336 should be used by self._on_zmq_reply to build messages that can
336 should be used by self._on_zmq_reply to build messages that can
337 be sent back to the browser.
337 be sent back to the browser.
338 """
338 """
339 idents, msg_list = self.session.feed_identities(msg_list)
339 idents, msg_list = self.session.feed_identities(msg_list)
340 msg = self.session.unserialize(msg_list)
340 msg = self.session.unserialize(msg_list)
341 try:
341 try:
342 msg['header'].pop('date')
342 msg['header'].pop('date')
343 except KeyError:
343 except KeyError:
344 pass
344 pass
345 try:
345 try:
346 msg['parent_header'].pop('date')
346 msg['parent_header'].pop('date')
347 except KeyError:
347 except KeyError:
348 pass
348 pass
349 msg.pop('buffers')
349 msg.pop('buffers')
350 return jsonapi.dumps(msg)
350 return jsonapi.dumps(msg)
351
351
352 def _on_zmq_reply(self, msg_list):
352 def _on_zmq_reply(self, msg_list):
353 try:
353 try:
354 msg = self._reserialize_reply(msg_list)
354 msg = self._reserialize_reply(msg_list)
355 except:
355 except:
356 self.application.log.critical("Malformed message: %r" % msg_list)
356 self.application.log.critical("Malformed message: %r" % msg_list)
357 else:
357 else:
358 self.write_message(msg)
358 self.write_message(msg)
359
359
360 def allow_draft76(self):
361 """Allow draft 76, until browsers such as Safari update to RFC 6455.
362
363 This has been disabled by default in tornado in release 2.2.0, and
364 support will be removed in later versions.
365 """
366 return True
367
360
368
361 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
369 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
362
370
363 def open(self, kernel_id):
371 def open(self, kernel_id):
364 self.kernel_id = kernel_id.decode('ascii')
372 self.kernel_id = kernel_id.decode('ascii')
365 try:
373 try:
366 cfg = self.application.ipython_app.config
374 cfg = self.application.ipython_app.config
367 except AttributeError:
375 except AttributeError:
368 # protect from the case where this is run from something other than
376 # protect from the case where this is run from something other than
369 # the notebook app:
377 # the notebook app:
370 cfg = None
378 cfg = None
371 self.session = Session(config=cfg)
379 self.session = Session(config=cfg)
372 self.save_on_message = self.on_message
380 self.save_on_message = self.on_message
373 self.on_message = self.on_first_message
381 self.on_message = self.on_first_message
374
382
375 def get_current_user(self):
383 def get_current_user(self):
376 user_id = self.get_secure_cookie("username")
384 user_id = self.get_secure_cookie("username")
377 if user_id == '' or (user_id is None and not self.application.password):
385 if user_id == '' or (user_id is None and not self.application.password):
378 user_id = 'anonymous'
386 user_id = 'anonymous'
379 return user_id
387 return user_id
380
388
381 def _inject_cookie_message(self, msg):
389 def _inject_cookie_message(self, msg):
382 """Inject the first message, which is the document cookie,
390 """Inject the first message, which is the document cookie,
383 for authentication."""
391 for authentication."""
384 if isinstance(msg, unicode):
392 if isinstance(msg, unicode):
385 # Cookie can't constructor doesn't accept unicode strings for some reason
393 # Cookie can't constructor doesn't accept unicode strings for some reason
386 msg = msg.encode('utf8', 'replace')
394 msg = msg.encode('utf8', 'replace')
387 try:
395 try:
388 self.request._cookies = Cookie.SimpleCookie(msg)
396 self.request._cookies = Cookie.SimpleCookie(msg)
389 except:
397 except:
390 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
398 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
391
399
392 def on_first_message(self, msg):
400 def on_first_message(self, msg):
393 self._inject_cookie_message(msg)
401 self._inject_cookie_message(msg)
394 if self.get_current_user() is None:
402 if self.get_current_user() is None:
395 logging.warn("Couldn't authenticate WebSocket connection")
403 logging.warn("Couldn't authenticate WebSocket connection")
396 raise web.HTTPError(403)
404 raise web.HTTPError(403)
397 self.on_message = self.save_on_message
405 self.on_message = self.save_on_message
398
406
399
407
400 class IOPubHandler(AuthenticatedZMQStreamHandler):
408 class IOPubHandler(AuthenticatedZMQStreamHandler):
401
409
402 def initialize(self, *args, **kwargs):
410 def initialize(self, *args, **kwargs):
403 self._kernel_alive = True
411 self._kernel_alive = True
404 self._beating = False
412 self._beating = False
405 self.iopub_stream = None
413 self.iopub_stream = None
406 self.hb_stream = None
414 self.hb_stream = None
407
415
408 def on_first_message(self, msg):
416 def on_first_message(self, msg):
409 try:
417 try:
410 super(IOPubHandler, self).on_first_message(msg)
418 super(IOPubHandler, self).on_first_message(msg)
411 except web.HTTPError:
419 except web.HTTPError:
412 self.close()
420 self.close()
413 return
421 return
414 km = self.application.kernel_manager
422 km = self.application.kernel_manager
415 self.time_to_dead = km.time_to_dead
423 self.time_to_dead = km.time_to_dead
416 self.first_beat = km.first_beat
424 self.first_beat = km.first_beat
417 kernel_id = self.kernel_id
425 kernel_id = self.kernel_id
418 try:
426 try:
419 self.iopub_stream = km.create_iopub_stream(kernel_id)
427 self.iopub_stream = km.create_iopub_stream(kernel_id)
420 self.hb_stream = km.create_hb_stream(kernel_id)
428 self.hb_stream = km.create_hb_stream(kernel_id)
421 except web.HTTPError:
429 except web.HTTPError:
422 # WebSockets don't response to traditional error codes so we
430 # WebSockets don't response to traditional error codes so we
423 # close the connection.
431 # close the connection.
424 if not self.stream.closed():
432 if not self.stream.closed():
425 self.stream.close()
433 self.stream.close()
426 self.close()
434 self.close()
427 else:
435 else:
428 self.iopub_stream.on_recv(self._on_zmq_reply)
436 self.iopub_stream.on_recv(self._on_zmq_reply)
429 self.start_hb(self.kernel_died)
437 self.start_hb(self.kernel_died)
430
438
431 def on_message(self, msg):
439 def on_message(self, msg):
432 pass
440 pass
433
441
434 def on_close(self):
442 def on_close(self):
435 # This method can be called twice, once by self.kernel_died and once
443 # This method can be called twice, once by self.kernel_died and once
436 # from the WebSocket close event. If the WebSocket connection is
444 # from the WebSocket close event. If the WebSocket connection is
437 # closed before the ZMQ streams are setup, they could be None.
445 # closed before the ZMQ streams are setup, they could be None.
438 self.stop_hb()
446 self.stop_hb()
439 if self.iopub_stream is not None and not self.iopub_stream.closed():
447 if self.iopub_stream is not None and not self.iopub_stream.closed():
440 self.iopub_stream.on_recv(None)
448 self.iopub_stream.on_recv(None)
441 self.iopub_stream.close()
449 self.iopub_stream.close()
442 if self.hb_stream is not None and not self.hb_stream.closed():
450 if self.hb_stream is not None and not self.hb_stream.closed():
443 self.hb_stream.close()
451 self.hb_stream.close()
444
452
445 def start_hb(self, callback):
453 def start_hb(self, callback):
446 """Start the heartbeating and call the callback if the kernel dies."""
454 """Start the heartbeating and call the callback if the kernel dies."""
447 if not self._beating:
455 if not self._beating:
448 self._kernel_alive = True
456 self._kernel_alive = True
449
457
450 def ping_or_dead():
458 def ping_or_dead():
451 self.hb_stream.flush()
459 self.hb_stream.flush()
452 if self._kernel_alive:
460 if self._kernel_alive:
453 self._kernel_alive = False
461 self._kernel_alive = False
454 self.hb_stream.send(b'ping')
462 self.hb_stream.send(b'ping')
455 # flush stream to force immediate socket send
463 # flush stream to force immediate socket send
456 self.hb_stream.flush()
464 self.hb_stream.flush()
457 else:
465 else:
458 try:
466 try:
459 callback()
467 callback()
460 except:
468 except:
461 pass
469 pass
462 finally:
470 finally:
463 self.stop_hb()
471 self.stop_hb()
464
472
465 def beat_received(msg):
473 def beat_received(msg):
466 self._kernel_alive = True
474 self._kernel_alive = True
467
475
468 self.hb_stream.on_recv(beat_received)
476 self.hb_stream.on_recv(beat_received)
469 loop = ioloop.IOLoop.instance()
477 loop = ioloop.IOLoop.instance()
470 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
478 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
471 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
479 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
472 self._beating= True
480 self._beating= True
473
481
474 def _really_start_hb(self):
482 def _really_start_hb(self):
475 """callback for delayed heartbeat start
483 """callback for delayed heartbeat start
476
484
477 Only start the hb loop if we haven't been closed during the wait.
485 Only start the hb loop if we haven't been closed during the wait.
478 """
486 """
479 if self._beating and not self.hb_stream.closed():
487 if self._beating and not self.hb_stream.closed():
480 self._hb_periodic_callback.start()
488 self._hb_periodic_callback.start()
481
489
482 def stop_hb(self):
490 def stop_hb(self):
483 """Stop the heartbeating and cancel all related callbacks."""
491 """Stop the heartbeating and cancel all related callbacks."""
484 if self._beating:
492 if self._beating:
485 self._beating = False
493 self._beating = False
486 self._hb_periodic_callback.stop()
494 self._hb_periodic_callback.stop()
487 if not self.hb_stream.closed():
495 if not self.hb_stream.closed():
488 self.hb_stream.on_recv(None)
496 self.hb_stream.on_recv(None)
489
497
490 def kernel_died(self):
498 def kernel_died(self):
491 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
499 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
492 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
500 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
493 self.write_message(
501 self.write_message(
494 {'header': {'msg_type': 'status'},
502 {'header': {'msg_type': 'status'},
495 'parent_header': {},
503 'parent_header': {},
496 'content': {'execution_state':'dead'}
504 'content': {'execution_state':'dead'}
497 }
505 }
498 )
506 )
499 self.on_close()
507 self.on_close()
500
508
501
509
502 class ShellHandler(AuthenticatedZMQStreamHandler):
510 class ShellHandler(AuthenticatedZMQStreamHandler):
503
511
504 def initialize(self, *args, **kwargs):
512 def initialize(self, *args, **kwargs):
505 self.shell_stream = None
513 self.shell_stream = None
506
514
507 def on_first_message(self, msg):
515 def on_first_message(self, msg):
508 try:
516 try:
509 super(ShellHandler, self).on_first_message(msg)
517 super(ShellHandler, self).on_first_message(msg)
510 except web.HTTPError:
518 except web.HTTPError:
511 self.close()
519 self.close()
512 return
520 return
513 km = self.application.kernel_manager
521 km = self.application.kernel_manager
514 self.max_msg_size = km.max_msg_size
522 self.max_msg_size = km.max_msg_size
515 kernel_id = self.kernel_id
523 kernel_id = self.kernel_id
516 try:
524 try:
517 self.shell_stream = km.create_shell_stream(kernel_id)
525 self.shell_stream = km.create_shell_stream(kernel_id)
518 except web.HTTPError:
526 except web.HTTPError:
519 # WebSockets don't response to traditional error codes so we
527 # WebSockets don't response to traditional error codes so we
520 # close the connection.
528 # close the connection.
521 if not self.stream.closed():
529 if not self.stream.closed():
522 self.stream.close()
530 self.stream.close()
523 self.close()
531 self.close()
524 else:
532 else:
525 self.shell_stream.on_recv(self._on_zmq_reply)
533 self.shell_stream.on_recv(self._on_zmq_reply)
526
534
527 def on_message(self, msg):
535 def on_message(self, msg):
528 if len(msg) < self.max_msg_size:
536 if len(msg) < self.max_msg_size:
529 msg = jsonapi.loads(msg)
537 msg = jsonapi.loads(msg)
530 self.session.send(self.shell_stream, msg)
538 self.session.send(self.shell_stream, msg)
531
539
532 def on_close(self):
540 def on_close(self):
533 # Make sure the stream exists and is not already closed.
541 # Make sure the stream exists and is not already closed.
534 if self.shell_stream is not None and not self.shell_stream.closed():
542 if self.shell_stream is not None and not self.shell_stream.closed():
535 self.shell_stream.close()
543 self.shell_stream.close()
536
544
537
545
538 #-----------------------------------------------------------------------------
546 #-----------------------------------------------------------------------------
539 # Notebook web service handlers
547 # Notebook web service handlers
540 #-----------------------------------------------------------------------------
548 #-----------------------------------------------------------------------------
541
549
542 class NotebookRootHandler(AuthenticatedHandler):
550 class NotebookRootHandler(AuthenticatedHandler):
543
551
544 @authenticate_unless_readonly
552 @authenticate_unless_readonly
545 def get(self):
553 def get(self):
546
554
547 nbm = self.application.notebook_manager
555 nbm = self.application.notebook_manager
548 files = nbm.list_notebooks()
556 files = nbm.list_notebooks()
549 self.finish(jsonapi.dumps(files))
557 self.finish(jsonapi.dumps(files))
550
558
551 @web.authenticated
559 @web.authenticated
552 def post(self):
560 def post(self):
553 nbm = self.application.notebook_manager
561 nbm = self.application.notebook_manager
554 body = self.request.body.strip()
562 body = self.request.body.strip()
555 format = self.get_argument('format', default='json')
563 format = self.get_argument('format', default='json')
556 name = self.get_argument('name', default=None)
564 name = self.get_argument('name', default=None)
557 if body:
565 if body:
558 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
566 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
559 else:
567 else:
560 notebook_id = nbm.new_notebook()
568 notebook_id = nbm.new_notebook()
561 self.set_header('Location', '/'+notebook_id)
569 self.set_header('Location', '/'+notebook_id)
562 self.finish(jsonapi.dumps(notebook_id))
570 self.finish(jsonapi.dumps(notebook_id))
563
571
564
572
565 class NotebookHandler(AuthenticatedHandler):
573 class NotebookHandler(AuthenticatedHandler):
566
574
567 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
575 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
568
576
569 @authenticate_unless_readonly
577 @authenticate_unless_readonly
570 def get(self, notebook_id):
578 def get(self, notebook_id):
571 nbm = self.application.notebook_manager
579 nbm = self.application.notebook_manager
572 format = self.get_argument('format', default='json')
580 format = self.get_argument('format', default='json')
573 last_mod, name, data = nbm.get_notebook(notebook_id, format)
581 last_mod, name, data = nbm.get_notebook(notebook_id, format)
574
582
575 if format == u'json':
583 if format == u'json':
576 self.set_header('Content-Type', 'application/json')
584 self.set_header('Content-Type', 'application/json')
577 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
585 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
578 elif format == u'py':
586 elif format == u'py':
579 self.set_header('Content-Type', 'application/x-python')
587 self.set_header('Content-Type', 'application/x-python')
580 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
588 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
581 self.set_header('Last-Modified', last_mod)
589 self.set_header('Last-Modified', last_mod)
582 self.finish(data)
590 self.finish(data)
583
591
584 @web.authenticated
592 @web.authenticated
585 def put(self, notebook_id):
593 def put(self, notebook_id):
586 nbm = self.application.notebook_manager
594 nbm = self.application.notebook_manager
587 format = self.get_argument('format', default='json')
595 format = self.get_argument('format', default='json')
588 name = self.get_argument('name', default=None)
596 name = self.get_argument('name', default=None)
589 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
597 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
590 self.set_status(204)
598 self.set_status(204)
591 self.finish()
599 self.finish()
592
600
593 @web.authenticated
601 @web.authenticated
594 def delete(self, notebook_id):
602 def delete(self, notebook_id):
595 nbm = self.application.notebook_manager
603 nbm = self.application.notebook_manager
596 nbm.delete_notebook(notebook_id)
604 nbm.delete_notebook(notebook_id)
597 self.set_status(204)
605 self.set_status(204)
598 self.finish()
606 self.finish()
599
607
600 #-----------------------------------------------------------------------------
608 #-----------------------------------------------------------------------------
601 # RST web service handlers
609 # RST web service handlers
602 #-----------------------------------------------------------------------------
610 #-----------------------------------------------------------------------------
603
611
604
612
605 class RSTHandler(AuthenticatedHandler):
613 class RSTHandler(AuthenticatedHandler):
606
614
607 @web.authenticated
615 @web.authenticated
608 def post(self):
616 def post(self):
609 if publish_string is None:
617 if publish_string is None:
610 raise web.HTTPError(503, u'docutils not available')
618 raise web.HTTPError(503, u'docutils not available')
611 body = self.request.body.strip()
619 body = self.request.body.strip()
612 source = body
620 source = body
613 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
621 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
614 defaults = {'file_insertion_enabled': 0,
622 defaults = {'file_insertion_enabled': 0,
615 'raw_enabled': 0,
623 'raw_enabled': 0,
616 '_disable_config': 1,
624 '_disable_config': 1,
617 'stylesheet_path': 0
625 'stylesheet_path': 0
618 # 'template': template_path
626 # 'template': template_path
619 }
627 }
620 try:
628 try:
621 html = publish_string(source, writer_name='html',
629 html = publish_string(source, writer_name='html',
622 settings_overrides=defaults
630 settings_overrides=defaults
623 )
631 )
624 except:
632 except:
625 raise web.HTTPError(400, u'Invalid RST')
633 raise web.HTTPError(400, u'Invalid RST')
626 print html
634 print html
627 self.set_header('Content-Type', 'text/html')
635 self.set_header('Content-Type', 'text/html')
628 self.finish(html)
636 self.finish(html)
629
637
630
638
General Comments 0
You need to be logged in to leave comments. Login now