##// END OF EJS Templates
Use the correct tornado import as suggested by @ivanov
Cameron Bates -
Show More
@@ -1,907 +1,907 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 Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30 import os
31 31
32 import tornado
32 from tornado.escape import url_escape
33 33 from tornado import web
34 34 from tornado import websocket
35 35
36 36 from zmq.eventloop import ioloop
37 37 from zmq.utils import jsonapi
38 38
39 39 from IPython.external.decorator import decorator
40 40 from IPython.zmq.session import Session
41 41 from IPython.lib.security import passwd_check
42 42 from IPython.utils.jsonutil import date_default
43 43 from IPython.utils.path import filefind
44 44
45 45 try:
46 46 from docutils.core import publish_string
47 47 except ImportError:
48 48 publish_string = None
49 49
50 50 #-----------------------------------------------------------------------------
51 51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 52 #-----------------------------------------------------------------------------
53 53
54 54 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 55 # parts tornado cares about haven't really changed, so it's OK to continue
56 56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 57 # version as of Oct 30/2011) the version check fails, see the issue report:
58 58
59 59 # https://github.com/facebook/tornado/issues/385
60 60
61 61 # This issue has been fixed in Tornado post 2.1.1:
62 62
63 63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64 64
65 65 # Here we manually apply the same patch as above so that users of IPython can
66 66 # continue to work with an officially released Tornado. We make the
67 67 # monkeypatch version check as narrow as possible to limit its effects; once
68 68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69 69
70 70 import tornado
71 71
72 72 if tornado.version_info <= (2,1,1):
73 73
74 74 def _execute(self, transforms, *args, **kwargs):
75 75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76 76
77 77 self.open_args = args
78 78 self.open_kwargs = kwargs
79 79
80 80 # The difference between version 8 and 13 is that in 8 the
81 81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 82 # simply "Origin".
83 83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 84 self.ws_connection = WebSocketProtocol8(self)
85 85 self.ws_connection.accept_connection()
86 86
87 87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 88 self.stream.write(tornado.escape.utf8(
89 89 "HTTP/1.1 426 Upgrade Required\r\n"
90 90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 91 self.stream.close()
92 92
93 93 else:
94 94 self.ws_connection = WebSocketProtocol76(self)
95 95 self.ws_connection.accept_connection()
96 96
97 97 websocket.WebSocketHandler._execute = _execute
98 98 del _execute
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Decorator for disabling read-only handlers
102 102 #-----------------------------------------------------------------------------
103 103
104 104 @decorator
105 105 def not_if_readonly(f, self, *args, **kwargs):
106 106 if self.application.read_only:
107 107 raise web.HTTPError(403, "Notebook server is read-only")
108 108 else:
109 109 return f(self, *args, **kwargs)
110 110
111 111 @decorator
112 112 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 113 """authenticate this page *unless* readonly view is active.
114 114
115 115 In read-only mode, the notebook list and print view should
116 116 be accessible without authentication.
117 117 """
118 118
119 119 @web.authenticated
120 120 def auth_f(self, *args, **kwargs):
121 121 return f(self, *args, **kwargs)
122 122
123 123 if self.application.read_only:
124 124 return f(self, *args, **kwargs)
125 125 else:
126 126 return auth_f(self, *args, **kwargs)
127 127
128 128 def urljoin(*pieces):
129 129 """Join componenet of url into a relative url
130 130
131 131 Use to prevent double slash when joining subpath
132 132 """
133 133 striped = [s.strip('/') for s in pieces]
134 134 return '/'.join(s for s in striped if s)
135 135
136 136 #-----------------------------------------------------------------------------
137 137 # Top-level handlers
138 138 #-----------------------------------------------------------------------------
139 139
140 140 class RequestHandler(web.RequestHandler):
141 141 """RequestHandler with default variable setting."""
142 142
143 143 def render(*args, **kwargs):
144 144 kwargs.setdefault('message', '')
145 145 return web.RequestHandler.render(*args, **kwargs)
146 146
147 147 class AuthenticatedHandler(RequestHandler):
148 148 """A RequestHandler with an authenticated user."""
149 149
150 150 def get_current_user(self):
151 151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
152 152 # For now the user_id should not return empty, but it could eventually
153 153 if user_id == '':
154 154 user_id = 'anonymous'
155 155 if user_id is None:
156 156 # prevent extra Invalid cookie sig warnings:
157 157 self.clear_cookie(self.settings['cookie_name'])
158 158 if not self.application.password and not self.application.read_only:
159 159 user_id = 'anonymous'
160 160 return user_id
161 161
162 162 @property
163 163 def logged_in(self):
164 164 """Is a user currently logged in?
165 165
166 166 """
167 167 user = self.get_current_user()
168 168 return (user and not user == 'anonymous')
169 169
170 170 @property
171 171 def login_available(self):
172 172 """May a user proceed to log in?
173 173
174 174 This returns True if login capability is available, irrespective of
175 175 whether the user is already logged in or not.
176 176
177 177 """
178 178 return bool(self.application.password)
179 179
180 180 @property
181 181 def read_only(self):
182 182 """Is the notebook read-only?
183 183
184 184 """
185 185 return self.application.read_only
186 186
187 187 @property
188 188 def ws_url(self):
189 189 """websocket url matching the current request
190 190
191 191 turns http[s]://host[:port] into
192 192 ws[s]://host[:port]
193 193 """
194 194 proto = self.request.protocol.replace('http', 'ws')
195 195 host = self.application.ipython_app.websocket_host # default to config value
196 196 if host == '':
197 197 host = self.request.host # get from request
198 198 return "%s://%s" % (proto, host)
199 199
200 200
201 201 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
202 202 """static files should only be accessible when logged in"""
203 203
204 204 @authenticate_unless_readonly
205 205 def get(self, path):
206 206 return web.StaticFileHandler.get(self, path)
207 207
208 208
209 209 class ProjectDashboardHandler(AuthenticatedHandler):
210 210
211 211 @authenticate_unless_readonly
212 212 def get(self):
213 213 nbm = self.application.notebook_manager
214 214 project = nbm.notebook_dir
215 215 template = self.application.jinja2_env.get_template('projectdashboard.html')
216 216 self.write( template.render(project=project,
217 217 base_project_url=self.application.ipython_app.base_project_url,
218 218 base_kernel_url=self.application.ipython_app.base_kernel_url,
219 219 read_only=self.read_only,
220 220 logged_in=self.logged_in,
221 221 login_available=self.login_available))
222 222
223 223
224 224 class LoginHandler(AuthenticatedHandler):
225 225
226 226 def _render(self, message=None):
227 227 template = self.application.jinja2_env.get_template('login.html')
228 228 self.write( template.render(
229 next=tornado.escape.url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
229 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
230 230 read_only=self.read_only,
231 231 logged_in=self.logged_in,
232 232 login_available=self.login_available,
233 233 base_project_url=self.application.ipython_app.base_project_url,
234 234 message=message
235 235 ))
236 236
237 237 def get(self):
238 238 if self.current_user:
239 239 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
240 240 else:
241 241 self._render()
242 242
243 243 def post(self):
244 244 pwd = self.get_argument('password', default=u'')
245 245 if self.application.password:
246 246 if passwd_check(self.application.password, pwd):
247 247 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
248 248 else:
249 249 self._render(message={'error': 'Invalid password'})
250 250 return
251 251
252 252 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
253 253
254 254
255 255 class LogoutHandler(AuthenticatedHandler):
256 256
257 257 def get(self):
258 258 self.clear_cookie(self.settings['cookie_name'])
259 259 if self.login_available:
260 260 message = {'info': 'Successfully logged out.'}
261 261 else:
262 262 message = {'warning': 'Cannot log out. Notebook authentication '
263 263 'is disabled.'}
264 264 template = self.application.jinja2_env.get_template('logout.html')
265 265 self.write( template.render(
266 266 read_only=self.read_only,
267 267 logged_in=self.logged_in,
268 268 login_available=self.login_available,
269 269 base_project_url=self.application.ipython_app.base_project_url,
270 270 message=message))
271 271
272 272
273 273 class NewHandler(AuthenticatedHandler):
274 274
275 275 @web.authenticated
276 276 def get(self):
277 277 nbm = self.application.notebook_manager
278 278 project = nbm.notebook_dir
279 279 notebook_id = nbm.new_notebook()
280 280 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
281 281
282 282 class NamedNotebookHandler(AuthenticatedHandler):
283 283
284 284 @authenticate_unless_readonly
285 285 def get(self, notebook_id):
286 286 nbm = self.application.notebook_manager
287 287 project = nbm.notebook_dir
288 288 if not nbm.notebook_exists(notebook_id):
289 289 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
290 290 template = self.application.jinja2_env.get_template('notebook.html')
291 291 self.write( template.render(project=project,
292 292 notebook_id=notebook_id,
293 293 base_project_url=self.application.ipython_app.base_project_url,
294 294 base_kernel_url=self.application.ipython_app.base_kernel_url,
295 295 kill_kernel=False,
296 296 read_only=self.read_only,
297 297 logged_in=self.logged_in,
298 298 login_available=self.login_available,
299 299 mathjax_url=self.application.ipython_app.mathjax_url,))
300 300
301 301
302 302 class PrintNotebookHandler(AuthenticatedHandler):
303 303
304 304 @authenticate_unless_readonly
305 305 def get(self, notebook_id):
306 306 nbm = self.application.notebook_manager
307 307 project = nbm.notebook_dir
308 308 if not nbm.notebook_exists(notebook_id):
309 309 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
310 310 template = self.application.jinja2_env.get_template('printnotebook.html')
311 311 self.write( template.render(
312 312 project=project,
313 313 notebook_id=notebook_id,
314 314 base_project_url=self.application.ipython_app.base_project_url,
315 315 base_kernel_url=self.application.ipython_app.base_kernel_url,
316 316 kill_kernel=False,
317 317 read_only=self.read_only,
318 318 logged_in=self.logged_in,
319 319 login_available=self.login_available,
320 320 mathjax_url=self.application.ipython_app.mathjax_url,
321 321 ))
322 322
323 323 #-----------------------------------------------------------------------------
324 324 # Kernel handlers
325 325 #-----------------------------------------------------------------------------
326 326
327 327
328 328 class MainKernelHandler(AuthenticatedHandler):
329 329
330 330 @web.authenticated
331 331 def get(self):
332 332 km = self.application.kernel_manager
333 333 self.finish(jsonapi.dumps(km.kernel_ids))
334 334
335 335 @web.authenticated
336 336 def post(self):
337 337 km = self.application.kernel_manager
338 338 nbm = self.application.notebook_manager
339 339 notebook_id = self.get_argument('notebook', default=None)
340 340 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
341 341 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
342 342 self.set_header('Location', '/'+kernel_id)
343 343 self.finish(jsonapi.dumps(data))
344 344
345 345
346 346 class KernelHandler(AuthenticatedHandler):
347 347
348 348 SUPPORTED_METHODS = ('DELETE')
349 349
350 350 @web.authenticated
351 351 def delete(self, kernel_id):
352 352 km = self.application.kernel_manager
353 353 km.shutdown_kernel(kernel_id)
354 354 self.set_status(204)
355 355 self.finish()
356 356
357 357
358 358 class KernelActionHandler(AuthenticatedHandler):
359 359
360 360 @web.authenticated
361 361 def post(self, kernel_id, action):
362 362 km = self.application.kernel_manager
363 363 if action == 'interrupt':
364 364 km.interrupt_kernel(kernel_id)
365 365 self.set_status(204)
366 366 if action == 'restart':
367 367 new_kernel_id = km.restart_kernel(kernel_id)
368 368 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
369 369 self.set_header('Location', '/'+new_kernel_id)
370 370 self.write(jsonapi.dumps(data))
371 371 self.finish()
372 372
373 373
374 374 class ZMQStreamHandler(websocket.WebSocketHandler):
375 375
376 376 def _reserialize_reply(self, msg_list):
377 377 """Reserialize a reply message using JSON.
378 378
379 379 This takes the msg list from the ZMQ socket, unserializes it using
380 380 self.session and then serializes the result using JSON. This method
381 381 should be used by self._on_zmq_reply to build messages that can
382 382 be sent back to the browser.
383 383 """
384 384 idents, msg_list = self.session.feed_identities(msg_list)
385 385 msg = self.session.unserialize(msg_list)
386 386 try:
387 387 msg['header'].pop('date')
388 388 except KeyError:
389 389 pass
390 390 try:
391 391 msg['parent_header'].pop('date')
392 392 except KeyError:
393 393 pass
394 394 msg.pop('buffers')
395 395 return jsonapi.dumps(msg, default=date_default)
396 396
397 397 def _on_zmq_reply(self, msg_list):
398 398 try:
399 399 msg = self._reserialize_reply(msg_list)
400 400 except Exception:
401 401 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
402 402 else:
403 403 self.write_message(msg)
404 404
405 405 def allow_draft76(self):
406 406 """Allow draft 76, until browsers such as Safari update to RFC 6455.
407 407
408 408 This has been disabled by default in tornado in release 2.2.0, and
409 409 support will be removed in later versions.
410 410 """
411 411 return True
412 412
413 413
414 414 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
415 415
416 416 def open(self, kernel_id):
417 417 self.kernel_id = kernel_id.decode('ascii')
418 418 try:
419 419 cfg = self.application.ipython_app.config
420 420 except AttributeError:
421 421 # protect from the case where this is run from something other than
422 422 # the notebook app:
423 423 cfg = None
424 424 self.session = Session(config=cfg)
425 425 self.save_on_message = self.on_message
426 426 self.on_message = self.on_first_message
427 427
428 428 def get_current_user(self):
429 429 user_id = self.get_secure_cookie(self.settings['cookie_name'])
430 430 if user_id == '' or (user_id is None and not self.application.password):
431 431 user_id = 'anonymous'
432 432 return user_id
433 433
434 434 def _inject_cookie_message(self, msg):
435 435 """Inject the first message, which is the document cookie,
436 436 for authentication."""
437 437 if isinstance(msg, unicode):
438 438 # Cookie can't constructor doesn't accept unicode strings for some reason
439 439 msg = msg.encode('utf8', 'replace')
440 440 try:
441 441 self.request._cookies = Cookie.SimpleCookie(msg)
442 442 except:
443 443 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
444 444
445 445 def on_first_message(self, msg):
446 446 self._inject_cookie_message(msg)
447 447 if self.get_current_user() is None:
448 448 logging.warn("Couldn't authenticate WebSocket connection")
449 449 raise web.HTTPError(403)
450 450 self.on_message = self.save_on_message
451 451
452 452
453 453 class IOPubHandler(AuthenticatedZMQStreamHandler):
454 454
455 455 def initialize(self, *args, **kwargs):
456 456 self._kernel_alive = True
457 457 self._beating = False
458 458 self.iopub_stream = None
459 459 self.hb_stream = None
460 460
461 461 def on_first_message(self, msg):
462 462 try:
463 463 super(IOPubHandler, self).on_first_message(msg)
464 464 except web.HTTPError:
465 465 self.close()
466 466 return
467 467 km = self.application.kernel_manager
468 468 self.time_to_dead = km.time_to_dead
469 469 self.first_beat = km.first_beat
470 470 kernel_id = self.kernel_id
471 471 try:
472 472 self.iopub_stream = km.create_iopub_stream(kernel_id)
473 473 self.hb_stream = km.create_hb_stream(kernel_id)
474 474 except web.HTTPError:
475 475 # WebSockets don't response to traditional error codes so we
476 476 # close the connection.
477 477 if not self.stream.closed():
478 478 self.stream.close()
479 479 self.close()
480 480 else:
481 481 self.iopub_stream.on_recv(self._on_zmq_reply)
482 482 self.start_hb(self.kernel_died)
483 483
484 484 def on_message(self, msg):
485 485 pass
486 486
487 487 def on_close(self):
488 488 # This method can be called twice, once by self.kernel_died and once
489 489 # from the WebSocket close event. If the WebSocket connection is
490 490 # closed before the ZMQ streams are setup, they could be None.
491 491 self.stop_hb()
492 492 if self.iopub_stream is not None and not self.iopub_stream.closed():
493 493 self.iopub_stream.on_recv(None)
494 494 self.iopub_stream.close()
495 495 if self.hb_stream is not None and not self.hb_stream.closed():
496 496 self.hb_stream.close()
497 497
498 498 def start_hb(self, callback):
499 499 """Start the heartbeating and call the callback if the kernel dies."""
500 500 if not self._beating:
501 501 self._kernel_alive = True
502 502
503 503 def ping_or_dead():
504 504 self.hb_stream.flush()
505 505 if self._kernel_alive:
506 506 self._kernel_alive = False
507 507 self.hb_stream.send(b'ping')
508 508 # flush stream to force immediate socket send
509 509 self.hb_stream.flush()
510 510 else:
511 511 try:
512 512 callback()
513 513 except:
514 514 pass
515 515 finally:
516 516 self.stop_hb()
517 517
518 518 def beat_received(msg):
519 519 self._kernel_alive = True
520 520
521 521 self.hb_stream.on_recv(beat_received)
522 522 loop = ioloop.IOLoop.instance()
523 523 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
524 524 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
525 525 self._beating= True
526 526
527 527 def _really_start_hb(self):
528 528 """callback for delayed heartbeat start
529 529
530 530 Only start the hb loop if we haven't been closed during the wait.
531 531 """
532 532 if self._beating and not self.hb_stream.closed():
533 533 self._hb_periodic_callback.start()
534 534
535 535 def stop_hb(self):
536 536 """Stop the heartbeating and cancel all related callbacks."""
537 537 if self._beating:
538 538 self._beating = False
539 539 self._hb_periodic_callback.stop()
540 540 if not self.hb_stream.closed():
541 541 self.hb_stream.on_recv(None)
542 542
543 543 def kernel_died(self):
544 544 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
545 545 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
546 546 self.write_message(
547 547 {'header': {'msg_type': 'status'},
548 548 'parent_header': {},
549 549 'content': {'execution_state':'dead'}
550 550 }
551 551 )
552 552 self.on_close()
553 553
554 554
555 555 class ShellHandler(AuthenticatedZMQStreamHandler):
556 556
557 557 def initialize(self, *args, **kwargs):
558 558 self.shell_stream = None
559 559
560 560 def on_first_message(self, msg):
561 561 try:
562 562 super(ShellHandler, self).on_first_message(msg)
563 563 except web.HTTPError:
564 564 self.close()
565 565 return
566 566 km = self.application.kernel_manager
567 567 self.max_msg_size = km.max_msg_size
568 568 kernel_id = self.kernel_id
569 569 try:
570 570 self.shell_stream = km.create_shell_stream(kernel_id)
571 571 except web.HTTPError:
572 572 # WebSockets don't response to traditional error codes so we
573 573 # close the connection.
574 574 if not self.stream.closed():
575 575 self.stream.close()
576 576 self.close()
577 577 else:
578 578 self.shell_stream.on_recv(self._on_zmq_reply)
579 579
580 580 def on_message(self, msg):
581 581 if len(msg) < self.max_msg_size:
582 582 msg = jsonapi.loads(msg)
583 583 self.session.send(self.shell_stream, msg)
584 584
585 585 def on_close(self):
586 586 # Make sure the stream exists and is not already closed.
587 587 if self.shell_stream is not None and not self.shell_stream.closed():
588 588 self.shell_stream.close()
589 589
590 590
591 591 #-----------------------------------------------------------------------------
592 592 # Notebook web service handlers
593 593 #-----------------------------------------------------------------------------
594 594
595 595 class NotebookRootHandler(AuthenticatedHandler):
596 596
597 597 @authenticate_unless_readonly
598 598 def get(self):
599 599 nbm = self.application.notebook_manager
600 600 km = self.application.kernel_manager
601 601 files = nbm.list_notebooks()
602 602 for f in files :
603 603 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
604 604 self.finish(jsonapi.dumps(files))
605 605
606 606 @web.authenticated
607 607 def post(self):
608 608 nbm = self.application.notebook_manager
609 609 body = self.request.body.strip()
610 610 format = self.get_argument('format', default='json')
611 611 name = self.get_argument('name', default=None)
612 612 if body:
613 613 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
614 614 else:
615 615 notebook_id = nbm.new_notebook()
616 616 self.set_header('Location', '/'+notebook_id)
617 617 self.finish(jsonapi.dumps(notebook_id))
618 618
619 619
620 620 class NotebookHandler(AuthenticatedHandler):
621 621
622 622 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
623 623
624 624 @authenticate_unless_readonly
625 625 def get(self, notebook_id):
626 626 nbm = self.application.notebook_manager
627 627 format = self.get_argument('format', default='json')
628 628 last_mod, name, data = nbm.get_notebook(notebook_id, format)
629 629
630 630 if format == u'json':
631 631 self.set_header('Content-Type', 'application/json')
632 632 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
633 633 elif format == u'py':
634 634 self.set_header('Content-Type', 'application/x-python')
635 635 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
636 636 self.set_header('Last-Modified', last_mod)
637 637 self.finish(data)
638 638
639 639 @web.authenticated
640 640 def put(self, notebook_id):
641 641 nbm = self.application.notebook_manager
642 642 format = self.get_argument('format', default='json')
643 643 name = self.get_argument('name', default=None)
644 644 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
645 645 self.set_status(204)
646 646 self.finish()
647 647
648 648 @web.authenticated
649 649 def delete(self, notebook_id):
650 650 nbm = self.application.notebook_manager
651 651 nbm.delete_notebook(notebook_id)
652 652 self.set_status(204)
653 653 self.finish()
654 654
655 655
656 656 class NotebookCopyHandler(AuthenticatedHandler):
657 657
658 658 @web.authenticated
659 659 def get(self, notebook_id):
660 660 nbm = self.application.notebook_manager
661 661 project = nbm.notebook_dir
662 662 notebook_id = nbm.copy_notebook(notebook_id)
663 663 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
664 664
665 665
666 666 #-----------------------------------------------------------------------------
667 667 # Cluster handlers
668 668 #-----------------------------------------------------------------------------
669 669
670 670
671 671 class MainClusterHandler(AuthenticatedHandler):
672 672
673 673 @web.authenticated
674 674 def get(self):
675 675 cm = self.application.cluster_manager
676 676 self.finish(jsonapi.dumps(cm.list_profiles()))
677 677
678 678
679 679 class ClusterProfileHandler(AuthenticatedHandler):
680 680
681 681 @web.authenticated
682 682 def get(self, profile):
683 683 cm = self.application.cluster_manager
684 684 self.finish(jsonapi.dumps(cm.profile_info(profile)))
685 685
686 686
687 687 class ClusterActionHandler(AuthenticatedHandler):
688 688
689 689 @web.authenticated
690 690 def post(self, profile, action):
691 691 cm = self.application.cluster_manager
692 692 if action == 'start':
693 693 n = self.get_argument('n',default=None)
694 694 if n is None:
695 695 data = cm.start_cluster(profile)
696 696 else:
697 697 data = cm.start_cluster(profile,int(n))
698 698 if action == 'stop':
699 699 data = cm.stop_cluster(profile)
700 700 self.finish(jsonapi.dumps(data))
701 701
702 702
703 703 #-----------------------------------------------------------------------------
704 704 # RST web service handlers
705 705 #-----------------------------------------------------------------------------
706 706
707 707
708 708 class RSTHandler(AuthenticatedHandler):
709 709
710 710 @web.authenticated
711 711 def post(self):
712 712 if publish_string is None:
713 713 raise web.HTTPError(503, u'docutils not available')
714 714 body = self.request.body.strip()
715 715 source = body
716 716 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
717 717 defaults = {'file_insertion_enabled': 0,
718 718 'raw_enabled': 0,
719 719 '_disable_config': 1,
720 720 'stylesheet_path': 0
721 721 # 'template': template_path
722 722 }
723 723 try:
724 724 html = publish_string(source, writer_name='html',
725 725 settings_overrides=defaults
726 726 )
727 727 except:
728 728 raise web.HTTPError(400, u'Invalid RST')
729 729 print html
730 730 self.set_header('Content-Type', 'text/html')
731 731 self.finish(html)
732 732
733 733 # to minimize subclass changes:
734 734 HTTPError = web.HTTPError
735 735
736 736 class FileFindHandler(web.StaticFileHandler):
737 737 """subclass of StaticFileHandler for serving files from a search path"""
738 738
739 739 _static_paths = {}
740 740 # _lock is needed for tornado < 2.2.0 compat
741 741 _lock = threading.Lock() # protects _static_hashes
742 742
743 743 def initialize(self, path, default_filename=None):
744 744 if isinstance(path, basestring):
745 745 path = [path]
746 746 self.roots = tuple(
747 747 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
748 748 )
749 749 self.default_filename = default_filename
750 750
751 751 @classmethod
752 752 def locate_file(cls, path, roots):
753 753 """locate a file to serve on our static file search path"""
754 754 with cls._lock:
755 755 if path in cls._static_paths:
756 756 return cls._static_paths[path]
757 757 try:
758 758 abspath = os.path.abspath(filefind(path, roots))
759 759 except IOError:
760 760 # empty string should always give exists=False
761 761 return ''
762 762
763 763 # os.path.abspath strips a trailing /
764 764 # it needs to be temporarily added back for requests to root/
765 765 if not (abspath + os.path.sep).startswith(roots):
766 766 raise HTTPError(403, "%s is not in root static directory", path)
767 767
768 768 cls._static_paths[path] = abspath
769 769 return abspath
770 770
771 771 def get(self, path, include_body=True):
772 772 path = self.parse_url_path(path)
773 773
774 774 # begin subclass override
775 775 abspath = self.locate_file(path, self.roots)
776 776 # end subclass override
777 777
778 778 if os.path.isdir(abspath) and self.default_filename is not None:
779 779 # need to look at the request.path here for when path is empty
780 780 # but there is some prefix to the path that was already
781 781 # trimmed by the routing
782 782 if not self.request.path.endswith("/"):
783 783 self.redirect(self.request.path + "/")
784 784 return
785 785 abspath = os.path.join(abspath, self.default_filename)
786 786 if not os.path.exists(abspath):
787 787 raise HTTPError(404)
788 788 if not os.path.isfile(abspath):
789 789 raise HTTPError(403, "%s is not a file", path)
790 790
791 791 stat_result = os.stat(abspath)
792 792 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
793 793
794 794 self.set_header("Last-Modified", modified)
795 795
796 796 mime_type, encoding = mimetypes.guess_type(abspath)
797 797 if mime_type:
798 798 self.set_header("Content-Type", mime_type)
799 799
800 800 cache_time = self.get_cache_time(path, modified, mime_type)
801 801
802 802 if cache_time > 0:
803 803 self.set_header("Expires", datetime.datetime.utcnow() + \
804 804 datetime.timedelta(seconds=cache_time))
805 805 self.set_header("Cache-Control", "max-age=" + str(cache_time))
806 806 else:
807 807 self.set_header("Cache-Control", "public")
808 808
809 809 self.set_extra_headers(path)
810 810
811 811 # Check the If-Modified-Since, and don't send the result if the
812 812 # content has not been modified
813 813 ims_value = self.request.headers.get("If-Modified-Since")
814 814 if ims_value is not None:
815 815 date_tuple = email.utils.parsedate(ims_value)
816 816 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
817 817 if if_since >= modified:
818 818 self.set_status(304)
819 819 return
820 820
821 821 with open(abspath, "rb") as file:
822 822 data = file.read()
823 823 hasher = hashlib.sha1()
824 824 hasher.update(data)
825 825 self.set_header("Etag", '"%s"' % hasher.hexdigest())
826 826 if include_body:
827 827 self.write(data)
828 828 else:
829 829 assert self.request.method == "HEAD"
830 830 self.set_header("Content-Length", len(data))
831 831
832 832 @classmethod
833 833 def get_version(cls, settings, path):
834 834 """Generate the version string to be used in static URLs.
835 835
836 836 This method may be overridden in subclasses (but note that it
837 837 is a class method rather than a static method). The default
838 838 implementation uses a hash of the file's contents.
839 839
840 840 ``settings`` is the `Application.settings` dictionary and ``path``
841 841 is the relative location of the requested asset on the filesystem.
842 842 The returned value should be a string, or ``None`` if no version
843 843 could be determined.
844 844 """
845 845 # begin subclass override:
846 846 static_paths = settings['static_path']
847 847 if isinstance(static_paths, basestring):
848 848 static_paths = [static_paths]
849 849 roots = tuple(
850 850 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
851 851 )
852 852
853 853 try:
854 854 abs_path = filefind(path, roots)
855 855 except IOError:
856 856 logging.error("Could not find static file %r", path)
857 857 return None
858 858
859 859 # end subclass override
860 860
861 861 with cls._lock:
862 862 hashes = cls._static_hashes
863 863 if abs_path not in hashes:
864 864 try:
865 865 f = open(abs_path, "rb")
866 866 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
867 867 f.close()
868 868 except Exception:
869 869 logging.error("Could not open static file %r", path)
870 870 hashes[abs_path] = None
871 871 hsh = hashes.get(abs_path)
872 872 if hsh:
873 873 return hsh[:5]
874 874 return None
875 875
876 876
877 877 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
878 878 # but needed for tornado < 2.2.0 compat
879 879 @classmethod
880 880 def make_static_url(cls, settings, path):
881 881 """Constructs a versioned url for the given path.
882 882
883 883 This method may be overridden in subclasses (but note that it is
884 884 a class method rather than an instance method).
885 885
886 886 ``settings`` is the `Application.settings` dictionary. ``path``
887 887 is the static path being requested. The url returned should be
888 888 relative to the current host.
889 889 """
890 890 static_url_prefix = settings.get('static_url_prefix', '/static/')
891 891 version_hash = cls.get_version(settings, path)
892 892 if version_hash:
893 893 return static_url_prefix + path + "?v=" + version_hash
894 894 return static_url_prefix + path
895 895
896 896 def parse_url_path(self, url_path):
897 897 """Converts a static URL path into a filesystem path.
898 898
899 899 ``url_path`` is the path component of the URL with
900 900 ``static_url_prefix`` removed. The return value should be
901 901 filesystem path relative to ``static_path``.
902 902 """
903 903 if os.path.sep != "/":
904 904 url_path = url_path.replace("/", os.path.sep)
905 905 return url_path
906 906
907 907
General Comments 0
You need to be logged in to leave comments. Login now