##// END OF EJS Templates
Monkeypatch Tornado 2.1.1 so it works with Google Chrome 16....
Fernando Perez -
Show More
@@ -1,496 +1,545 b''
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import logging
20 20 import Cookie
21 21 import uuid
22 22
23 23 from tornado import web
24 24 from tornado import websocket
25 25
26 26 from zmq.eventloop import ioloop
27 27 from zmq.utils import jsonapi
28 28
29 29 from IPython.external.decorator import decorator
30 30 from IPython.zmq.session import Session
31 31
32 32 try:
33 33 from docutils.core import publish_string
34 34 except ImportError:
35 35 publish_string = None
36 36
37 #-----------------------------------------------------------------------------
38 # Monkeypatch for Tornado 2.1.1 - Remove when no longer necessary!
39 #-----------------------------------------------------------------------------
40
41 # Google Chrome, as of release 16, changed its websocket protocol number. The
42 # parts tornado cares about haven't really changed, so it's OK to continue
43 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
44 # version as of Oct 30/2011) the version check fails, see the issue report:
45
46 # https://github.com/facebook/tornado/issues/385
47
48 # This issue has been fixed in Tornado post 2.1.1:
49
50 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
51
52 # Here we manually apply the same patch as above so that users of IPython can
53 # continue to work with an officially released Tornado. We make the
54 # monkeypatch version check as narrow as possible to limit its effects; once
55 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
37 56
57 import tornado
58
59 if tornado.version == '2.1.1':
60
61 def _execute(self, transforms, *args, **kwargs):
62 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
63
64 self.open_args = args
65 self.open_kwargs = kwargs
66
67 # The difference between version 8 and 13 is that in 8 the
68 # client sends a "Sec-Websocket-Origin" header and in 13 it's
69 # simply "Origin".
70 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
71 self.ws_connection = WebSocketProtocol8(self)
72 self.ws_connection.accept_connection()
73
74 elif self.request.headers.get("Sec-WebSocket-Version"):
75 self.stream.write(tornado.escape.utf8(
76 "HTTP/1.1 426 Upgrade Required\r\n"
77 "Sec-WebSocket-Version: 8\r\n\r\n"))
78 self.stream.close()
79
80 else:
81 self.ws_connection = WebSocketProtocol76(self)
82 self.ws_connection.accept_connection()
83
84 websocket.WebSocketHandler._execute = _execute
85 del _execute
86
38 87 #-----------------------------------------------------------------------------
39 88 # Decorator for disabling read-only handlers
40 89 #-----------------------------------------------------------------------------
41 90
42 91 @decorator
43 92 def not_if_readonly(f, self, *args, **kwargs):
44 93 if self.application.read_only:
45 94 raise web.HTTPError(403, "Notebook server is read-only")
46 95 else:
47 96 return f(self, *args, **kwargs)
48 97
49 98 @decorator
50 99 def authenticate_unless_readonly(f, self, *args, **kwargs):
51 100 """authenticate this page *unless* readonly view is active.
52 101
53 102 In read-only mode, the notebook list and print view should
54 103 be accessible without authentication.
55 104 """
56 105
57 106 @web.authenticated
58 107 def auth_f(self, *args, **kwargs):
59 108 return f(self, *args, **kwargs)
60 109 if self.application.read_only:
61 110 return f(self, *args, **kwargs)
62 111 else:
63 112 return auth_f(self, *args, **kwargs)
64 113
65 114 #-----------------------------------------------------------------------------
66 115 # Top-level handlers
67 116 #-----------------------------------------------------------------------------
68 117
69 118 class AuthenticatedHandler(web.RequestHandler):
70 119 """A RequestHandler with an authenticated user."""
71 120
72 121 def get_current_user(self):
73 122 user_id = self.get_secure_cookie("username")
74 123 # For now the user_id should not return empty, but it could eventually
75 124 if user_id == '':
76 125 user_id = 'anonymous'
77 126 if user_id is None:
78 127 # prevent extra Invalid cookie sig warnings:
79 128 self.clear_cookie('username')
80 129 if not self.application.password and not self.application.read_only:
81 130 user_id = 'anonymous'
82 131 return user_id
83 132
84 133 @property
85 134 def read_only(self):
86 135 if self.application.read_only:
87 136 if self.application.password:
88 137 return self.get_current_user() is None
89 138 else:
90 139 return True
91 140 else:
92 141 return False
93 142
94 143
95 144
96 145 class ProjectDashboardHandler(AuthenticatedHandler):
97 146
98 147 @authenticate_unless_readonly
99 148 def get(self):
100 149 nbm = self.application.notebook_manager
101 150 project = nbm.notebook_dir
102 151 self.render(
103 152 'projectdashboard.html', project=project,
104 153 base_project_url=u'/', base_kernel_url=u'/',
105 154 read_only=self.read_only,
106 155 )
107 156
108 157
109 158 class LoginHandler(AuthenticatedHandler):
110 159
111 160 def get(self):
112 161 self.render('login.html',
113 162 next=self.get_argument('next', default='/'),
114 163 read_only=self.read_only,
115 164 )
116 165
117 166 def post(self):
118 167 pwd = self.get_argument('password', default=u'')
119 168 if self.application.password and pwd == self.application.password:
120 169 self.set_secure_cookie('username', str(uuid.uuid4()))
121 170 self.redirect(self.get_argument('next', default='/'))
122 171
123 172
124 173 class NewHandler(AuthenticatedHandler):
125 174
126 175 @web.authenticated
127 176 def get(self):
128 177 nbm = self.application.notebook_manager
129 178 project = nbm.notebook_dir
130 179 notebook_id = nbm.new_notebook()
131 180 self.render(
132 181 'notebook.html', project=project,
133 182 notebook_id=notebook_id,
134 183 base_project_url=u'/', base_kernel_url=u'/',
135 184 kill_kernel=False,
136 185 read_only=False,
137 186 )
138 187
139 188
140 189 class NamedNotebookHandler(AuthenticatedHandler):
141 190
142 191 @authenticate_unless_readonly
143 192 def get(self, notebook_id):
144 193 nbm = self.application.notebook_manager
145 194 project = nbm.notebook_dir
146 195 if not nbm.notebook_exists(notebook_id):
147 196 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
148 197
149 198 self.render(
150 199 'notebook.html', project=project,
151 200 notebook_id=notebook_id,
152 201 base_project_url=u'/', base_kernel_url=u'/',
153 202 kill_kernel=False,
154 203 read_only=self.read_only,
155 204 )
156 205
157 206
158 207 #-----------------------------------------------------------------------------
159 208 # Kernel handlers
160 209 #-----------------------------------------------------------------------------
161 210
162 211
163 212 class MainKernelHandler(AuthenticatedHandler):
164 213
165 214 @web.authenticated
166 215 def get(self):
167 216 km = self.application.kernel_manager
168 217 self.finish(jsonapi.dumps(km.kernel_ids))
169 218
170 219 @web.authenticated
171 220 def post(self):
172 221 km = self.application.kernel_manager
173 222 notebook_id = self.get_argument('notebook', default=None)
174 223 kernel_id = km.start_kernel(notebook_id)
175 224 ws_url = self.application.ipython_app.get_ws_url()
176 225 data = {'ws_url':ws_url,'kernel_id':kernel_id}
177 226 self.set_header('Location', '/'+kernel_id)
178 227 self.finish(jsonapi.dumps(data))
179 228
180 229
181 230 class KernelHandler(AuthenticatedHandler):
182 231
183 232 SUPPORTED_METHODS = ('DELETE')
184 233
185 234 @web.authenticated
186 235 def delete(self, kernel_id):
187 236 km = self.application.kernel_manager
188 237 km.kill_kernel(kernel_id)
189 238 self.set_status(204)
190 239 self.finish()
191 240
192 241
193 242 class KernelActionHandler(AuthenticatedHandler):
194 243
195 244 @web.authenticated
196 245 def post(self, kernel_id, action):
197 246 km = self.application.kernel_manager
198 247 if action == 'interrupt':
199 248 km.interrupt_kernel(kernel_id)
200 249 self.set_status(204)
201 250 if action == 'restart':
202 251 new_kernel_id = km.restart_kernel(kernel_id)
203 252 ws_url = self.application.ipython_app.get_ws_url()
204 253 data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
205 254 self.set_header('Location', '/'+new_kernel_id)
206 255 self.write(jsonapi.dumps(data))
207 256 self.finish()
208 257
209 258
210 259 class ZMQStreamHandler(websocket.WebSocketHandler):
211 260
212 261 def _reserialize_reply(self, msg_list):
213 262 """Reserialize a reply message using JSON.
214 263
215 264 This takes the msg list from the ZMQ socket, unserializes it using
216 265 self.session and then serializes the result using JSON. This method
217 266 should be used by self._on_zmq_reply to build messages that can
218 267 be sent back to the browser.
219 268 """
220 269 idents, msg_list = self.session.feed_identities(msg_list)
221 270 msg = self.session.unserialize(msg_list)
222 271 try:
223 272 msg['header'].pop('date')
224 273 except KeyError:
225 274 pass
226 275 try:
227 276 msg['parent_header'].pop('date')
228 277 except KeyError:
229 278 pass
230 279 msg.pop('buffers')
231 280 return jsonapi.dumps(msg)
232 281
233 282 def _on_zmq_reply(self, msg_list):
234 283 try:
235 284 msg = self._reserialize_reply(msg_list)
236 285 except:
237 286 self.application.log.critical("Malformed message: %r" % msg_list)
238 287 else:
239 288 self.write_message(msg)
240 289
241 290
242 291 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
243 292
244 293 def open(self, kernel_id):
245 294 self.kernel_id = kernel_id.decode('ascii')
246 295 try:
247 296 cfg = self.application.ipython_app.config
248 297 except AttributeError:
249 298 # protect from the case where this is run from something other than
250 299 # the notebook app:
251 300 cfg = None
252 301 self.session = Session(config=cfg)
253 302 self.save_on_message = self.on_message
254 303 self.on_message = self.on_first_message
255 304
256 305 def get_current_user(self):
257 306 user_id = self.get_secure_cookie("username")
258 307 if user_id == '' or (user_id is None and not self.application.password):
259 308 user_id = 'anonymous'
260 309 return user_id
261 310
262 311 def _inject_cookie_message(self, msg):
263 312 """Inject the first message, which is the document cookie,
264 313 for authentication."""
265 314 if isinstance(msg, unicode):
266 315 # Cookie can't constructor doesn't accept unicode strings for some reason
267 316 msg = msg.encode('utf8', 'replace')
268 317 try:
269 318 self.request._cookies = Cookie.SimpleCookie(msg)
270 319 except:
271 320 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
272 321
273 322 def on_first_message(self, msg):
274 323 self._inject_cookie_message(msg)
275 324 if self.get_current_user() is None:
276 325 logging.warn("Couldn't authenticate WebSocket connection")
277 326 raise web.HTTPError(403)
278 327 self.on_message = self.save_on_message
279 328
280 329
281 330 class IOPubHandler(AuthenticatedZMQStreamHandler):
282 331
283 332 def initialize(self, *args, **kwargs):
284 333 self._kernel_alive = True
285 334 self._beating = False
286 335 self.iopub_stream = None
287 336 self.hb_stream = None
288 337
289 338 def on_first_message(self, msg):
290 339 try:
291 340 super(IOPubHandler, self).on_first_message(msg)
292 341 except web.HTTPError:
293 342 self.close()
294 343 return
295 344 km = self.application.kernel_manager
296 345 self.time_to_dead = km.time_to_dead
297 346 kernel_id = self.kernel_id
298 347 try:
299 348 self.iopub_stream = km.create_iopub_stream(kernel_id)
300 349 self.hb_stream = km.create_hb_stream(kernel_id)
301 350 except web.HTTPError:
302 351 # WebSockets don't response to traditional error codes so we
303 352 # close the connection.
304 353 if not self.stream.closed():
305 354 self.stream.close()
306 355 self.close()
307 356 else:
308 357 self.iopub_stream.on_recv(self._on_zmq_reply)
309 358 self.start_hb(self.kernel_died)
310 359
311 360 def on_message(self, msg):
312 361 pass
313 362
314 363 def on_close(self):
315 364 # This method can be called twice, once by self.kernel_died and once
316 365 # from the WebSocket close event. If the WebSocket connection is
317 366 # closed before the ZMQ streams are setup, they could be None.
318 367 self.stop_hb()
319 368 if self.iopub_stream is not None and not self.iopub_stream.closed():
320 369 self.iopub_stream.on_recv(None)
321 370 self.iopub_stream.close()
322 371 if self.hb_stream is not None and not self.hb_stream.closed():
323 372 self.hb_stream.close()
324 373
325 374 def start_hb(self, callback):
326 375 """Start the heartbeating and call the callback if the kernel dies."""
327 376 if not self._beating:
328 377 self._kernel_alive = True
329 378
330 379 def ping_or_dead():
331 380 if self._kernel_alive:
332 381 self._kernel_alive = False
333 382 self.hb_stream.send(b'ping')
334 383 else:
335 384 try:
336 385 callback()
337 386 except:
338 387 pass
339 388 finally:
340 389 self._hb_periodic_callback.stop()
341 390
342 391 def beat_received(msg):
343 392 self._kernel_alive = True
344 393
345 394 self.hb_stream.on_recv(beat_received)
346 395 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
347 396 self._hb_periodic_callback.start()
348 397 self._beating= True
349 398
350 399 def stop_hb(self):
351 400 """Stop the heartbeating and cancel all related callbacks."""
352 401 if self._beating:
353 402 self._hb_periodic_callback.stop()
354 403 if not self.hb_stream.closed():
355 404 self.hb_stream.on_recv(None)
356 405
357 406 def kernel_died(self):
358 407 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
359 408 self.write_message(
360 409 {'header': {'msg_type': 'status'},
361 410 'parent_header': {},
362 411 'content': {'execution_state':'dead'}
363 412 }
364 413 )
365 414 self.on_close()
366 415
367 416
368 417 class ShellHandler(AuthenticatedZMQStreamHandler):
369 418
370 419 def initialize(self, *args, **kwargs):
371 420 self.shell_stream = None
372 421
373 422 def on_first_message(self, msg):
374 423 try:
375 424 super(ShellHandler, self).on_first_message(msg)
376 425 except web.HTTPError:
377 426 self.close()
378 427 return
379 428 km = self.application.kernel_manager
380 429 self.max_msg_size = km.max_msg_size
381 430 kernel_id = self.kernel_id
382 431 try:
383 432 self.shell_stream = km.create_shell_stream(kernel_id)
384 433 except web.HTTPError:
385 434 # WebSockets don't response to traditional error codes so we
386 435 # close the connection.
387 436 if not self.stream.closed():
388 437 self.stream.close()
389 438 self.close()
390 439 else:
391 440 self.shell_stream.on_recv(self._on_zmq_reply)
392 441
393 442 def on_message(self, msg):
394 443 if len(msg) < self.max_msg_size:
395 444 msg = jsonapi.loads(msg)
396 445 self.session.send(self.shell_stream, msg)
397 446
398 447 def on_close(self):
399 448 # Make sure the stream exists and is not already closed.
400 449 if self.shell_stream is not None and not self.shell_stream.closed():
401 450 self.shell_stream.close()
402 451
403 452
404 453 #-----------------------------------------------------------------------------
405 454 # Notebook web service handlers
406 455 #-----------------------------------------------------------------------------
407 456
408 457 class NotebookRootHandler(AuthenticatedHandler):
409 458
410 459 @authenticate_unless_readonly
411 460 def get(self):
412 461
413 462 nbm = self.application.notebook_manager
414 463 files = nbm.list_notebooks()
415 464 self.finish(jsonapi.dumps(files))
416 465
417 466 @web.authenticated
418 467 def post(self):
419 468 nbm = self.application.notebook_manager
420 469 body = self.request.body.strip()
421 470 format = self.get_argument('format', default='json')
422 471 name = self.get_argument('name', default=None)
423 472 if body:
424 473 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
425 474 else:
426 475 notebook_id = nbm.new_notebook()
427 476 self.set_header('Location', '/'+notebook_id)
428 477 self.finish(jsonapi.dumps(notebook_id))
429 478
430 479
431 480 class NotebookHandler(AuthenticatedHandler):
432 481
433 482 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
434 483
435 484 @authenticate_unless_readonly
436 485 def get(self, notebook_id):
437 486 nbm = self.application.notebook_manager
438 487 format = self.get_argument('format', default='json')
439 488 last_mod, name, data = nbm.get_notebook(notebook_id, format)
440 489
441 490 if format == u'json':
442 491 self.set_header('Content-Type', 'application/json')
443 492 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
444 493 elif format == u'py':
445 494 self.set_header('Content-Type', 'application/x-python')
446 495 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
447 496 self.set_header('Last-Modified', last_mod)
448 497 self.finish(data)
449 498
450 499 @web.authenticated
451 500 def put(self, notebook_id):
452 501 nbm = self.application.notebook_manager
453 502 format = self.get_argument('format', default='json')
454 503 name = self.get_argument('name', default=None)
455 504 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
456 505 self.set_status(204)
457 506 self.finish()
458 507
459 508 @web.authenticated
460 509 def delete(self, notebook_id):
461 510 nbm = self.application.notebook_manager
462 511 nbm.delete_notebook(notebook_id)
463 512 self.set_status(204)
464 513 self.finish()
465 514
466 515 #-----------------------------------------------------------------------------
467 516 # RST web service handlers
468 517 #-----------------------------------------------------------------------------
469 518
470 519
471 520 class RSTHandler(AuthenticatedHandler):
472 521
473 522 @web.authenticated
474 523 def post(self):
475 524 if publish_string is None:
476 525 raise web.HTTPError(503, u'docutils not available')
477 526 body = self.request.body.strip()
478 527 source = body
479 528 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
480 529 defaults = {'file_insertion_enabled': 0,
481 530 'raw_enabled': 0,
482 531 '_disable_config': 1,
483 532 'stylesheet_path': 0
484 533 # 'template': template_path
485 534 }
486 535 try:
487 536 html = publish_string(source, writer_name='html',
488 537 settings_overrides=defaults
489 538 )
490 539 except:
491 540 raise web.HTTPError(400, u'Invalid RST')
492 541 print html
493 542 self.set_header('Content-Type', 'text/html')
494 543 self.finish(html)
495 544
496 545
General Comments 0
You need to be logged in to leave comments. Login now