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