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