handlers.py
931 lines
| 30.5 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """Tornado handlers for the notebook. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
Brian E. Granger
|
r4346 | |||
#----------------------------------------------------------------------------- | ||||
Brian E. Granger
|
r4609 | # Copyright (C) 2008-2011 The IPython Development Team | ||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4609 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r4297 | |||
MinRK
|
r4707 | import Cookie | ||
MinRK
|
r7922 | import datetime | ||
import email.utils | ||||
import hashlib | ||||
import logging | ||||
import mimetypes | ||||
import os | ||||
import stat | ||||
MinRK
|
r8016 | import threading | ||
MinRK
|
r5812 | import time | ||
MinRK
|
r5101 | import uuid | ||
MinRK
|
r4707 | |||
Cameron Bates
|
r8907 | from tornado.escape import url_escape | ||
Brian Granger
|
r4297 | from tornado import web | ||
from tornado import websocket | ||||
MinRK
|
r10360 | try: | ||
from tornado.log import app_log | ||||
except ImportError: | ||||
app_log = logging.getLogger() | ||||
Brian E. Granger
|
r4545 | from zmq.eventloop import ioloop | ||
from zmq.utils import jsonapi | ||||
MinRK
|
r10360 | from IPython.config import Application | ||
MinRK
|
r5191 | from IPython.external.decorator import decorator | ||
MinRK
|
r9372 | from IPython.kernel.zmq.session import Session | ||
Stefan van der Walt
|
r5321 | from IPython.lib.security import passwd_check | ||
MinRK
|
r6870 | from IPython.utils.jsonutil import date_default | ||
MinRK
|
r7922 | from IPython.utils.path import filefind | ||
Mikhail Korobov
|
r9110 | from IPython.utils.py3compat import PY3 | ||
Brian E. Granger
|
r4545 | |||
Brian E. Granger
|
r4507 | try: | ||
from docutils.core import publish_string | ||||
except ImportError: | ||||
publish_string = None | ||||
Fernando Perez
|
r5240 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r5241 | # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! | ||
Fernando Perez
|
r5240 | #----------------------------------------------------------------------------- | ||
# Google Chrome, as of release 16, changed its websocket protocol number. The | ||||
# parts tornado cares about haven't really changed, so it's OK to continue | ||||
# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released | ||||
# version as of Oct 30/2011) the version check fails, see the issue report: | ||||
# https://github.com/facebook/tornado/issues/385 | ||||
# This issue has been fixed in Tornado post 2.1.1: | ||||
# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710 | ||||
# Here we manually apply the same patch as above so that users of IPython can | ||||
# continue to work with an officially released Tornado. We make the | ||||
# monkeypatch version check as narrow as possible to limit its effects; once | ||||
# Tornado 2.1.1 is no longer found in the wild we'll delete this code. | ||||
import tornado | ||||
Fernando Perez
|
r5241 | if tornado.version_info <= (2,1,1): | ||
Fernando Perez
|
r5240 | |||
def _execute(self, transforms, *args, **kwargs): | ||||
from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76 | ||||
self.open_args = args | ||||
self.open_kwargs = kwargs | ||||
# The difference between version 8 and 13 is that in 8 the | ||||
# client sends a "Sec-Websocket-Origin" header and in 13 it's | ||||
# simply "Origin". | ||||
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"): | ||||
self.ws_connection = WebSocketProtocol8(self) | ||||
self.ws_connection.accept_connection() | ||||
elif self.request.headers.get("Sec-WebSocket-Version"): | ||||
self.stream.write(tornado.escape.utf8( | ||||
"HTTP/1.1 426 Upgrade Required\r\n" | ||||
"Sec-WebSocket-Version: 8\r\n\r\n")) | ||||
self.stream.close() | ||||
else: | ||||
self.ws_connection = WebSocketProtocol76(self) | ||||
self.ws_connection.accept_connection() | ||||
Brian E. Granger
|
r4484 | |||
Fernando Perez
|
r5240 | websocket.WebSocketHandler._execute = _execute | ||
del _execute | ||||
MinRK
|
r10360 | |||
MinRK
|
r5191 | #----------------------------------------------------------------------------- | ||
# Decorator for disabling read-only handlers | ||||
#----------------------------------------------------------------------------- | ||||
@decorator | ||||
def not_if_readonly(f, self, *args, **kwargs): | ||||
MinRK
|
r10355 | if self.settings.get('read_only', False): | ||
MinRK
|
r5191 | raise web.HTTPError(403, "Notebook server is read-only") | ||
else: | ||||
return f(self, *args, **kwargs) | ||||
Brian E. Granger
|
r4545 | |||
MinRK
|
r5200 | @decorator | ||
def authenticate_unless_readonly(f, self, *args, **kwargs): | ||||
"""authenticate this page *unless* readonly view is active. | ||||
In read-only mode, the notebook list and print view should | ||||
be accessible without authentication. | ||||
""" | ||||
@web.authenticated | ||||
def auth_f(self, *args, **kwargs): | ||||
return f(self, *args, **kwargs) | ||||
MinRK
|
r5824 | |||
MinRK
|
r10355 | if self.settings.get('read_only', False): | ||
MinRK
|
r5200 | return f(self, *args, **kwargs) | ||
else: | ||||
return auth_f(self, *args, **kwargs) | ||||
Matthias BUSSONNIER
|
r8097 | def urljoin(*pieces): | ||
MinRK
|
r10355 | """Join components of url into a relative url | ||
Matthias BUSSONNIER
|
r8097 | |||
Use to prevent double slash when joining subpath | ||||
""" | ||||
striped = [s.strip('/') for s in pieces] | ||||
return '/'.join(s for s in striped if s) | ||||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4494 | # Top-level handlers | ||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r4292 | |||
Stefan van der Walt
|
r5324 | class RequestHandler(web.RequestHandler): | ||
"""RequestHandler with default variable setting.""" | ||||
def render(*args, **kwargs): | ||||
kwargs.setdefault('message', '') | ||||
return web.RequestHandler.render(*args, **kwargs) | ||||
class AuthenticatedHandler(RequestHandler): | ||||
MinRK
|
r4706 | """A RequestHandler with an authenticated user.""" | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r10355 | def clear_login_cookie(self): | ||
self.clear_cookie(self.cookie_name) | ||||
Satrajit Ghosh
|
r4690 | def get_current_user(self): | ||
MinRK
|
r10355 | user_id = self.get_secure_cookie(self.cookie_name) | ||
Brian E. Granger
|
r5102 | # For now the user_id should not return empty, but it could eventually | ||
MinRK
|
r4724 | if user_id == '': | ||
user_id = 'anonymous' | ||||
if user_id is None: | ||||
# prevent extra Invalid cookie sig warnings: | ||||
MinRK
|
r10355 | self.clear_login_cookie() | ||
if not self.read_only and not self.login_available: | ||||
MinRK
|
r4724 | user_id = 'anonymous' | ||
return user_id | ||||
Stefan van der Walt
|
r5721 | |||
MinRK
|
r5213 | @property | ||
MinRK
|
r10355 | def cookie_name(self): | ||
return self.settings.get('cookie_name', '') | ||||
@property | ||||
def password(self): | ||||
"""our password""" | ||||
return self.settings.get('password', '') | ||||
@property | ||||
Stefan van der Walt
|
r5722 | def logged_in(self): | ||
"""Is a user currently logged in? | ||||
""" | ||||
user = self.get_current_user() | ||||
return (user and not user == 'anonymous') | ||||
@property | ||||
def login_available(self): | ||||
"""May a user proceed to log in? | ||||
This returns True if login capability is available, irrespective of | ||||
whether the user is already logged in or not. | ||||
""" | ||||
MinRK
|
r10355 | return bool(self.settings.get('password', '')) | ||
Stefan van der Walt
|
r5722 | |||
@property | ||||
MinRK
|
r5213 | def read_only(self): | ||
Stefan van der Walt
|
r5721 | """Is the notebook read-only? | ||
""" | ||||
MinRK
|
r10355 | return self.settings.get('read_only', False) | ||
Stefan van der Walt
|
r5721 | |||
MinRK
|
r10355 | class IPythonHandler(AuthenticatedHandler): | ||
"""IPython-specific extensions to authenticated handling | ||||
Mostly property shortcuts to IPython-specific settings. | ||||
""" | ||||
@property | ||||
def config(self): | ||||
return self.settings.get('config', None) | ||||
MinRK
|
r5252 | @property | ||
MinRK
|
r10360 | def log(self): | ||
"""use the IPython log by default, falling back on tornado's logger""" | ||||
if Application.initialized(): | ||||
return Application.instance().log | ||||
else: | ||||
return app_log | ||||
@property | ||||
Bussonnier Matthias
|
r9266 | def use_less(self): | ||
Matthias BUSSONNIER
|
r9296 | """Use less instead of css in templates""" | ||
MinRK
|
r10355 | return self.settings.get('use_less', False) | ||
#--------------------------------------------------------------- | ||||
# URLs | ||||
#--------------------------------------------------------------- | ||||
MinRK
|
r5252 | @property | ||
def ws_url(self): | ||||
"""websocket url matching the current request | ||||
Andrew Straw
|
r6006 | |||
MinRK
|
r5300 | turns http[s]://host[:port] into | ||
ws[s]://host[:port] | ||||
MinRK
|
r5252 | """ | ||
MinRK
|
r5300 | proto = self.request.protocol.replace('http', 'ws') | ||
MinRK
|
r10355 | host = self.settings.get('websocket_host', '') | ||
# default to config value | ||||
Andrew Straw
|
r6006 | if host == '': | ||
host = self.request.host # get from request | ||||
return "%s://%s" % (proto, host) | ||||
MinRK
|
r10355 | |||
@property | ||||
def mathjax_url(self): | ||||
return self.settings.get('mathjax_url', '') | ||||
@property | ||||
def base_project_url(self): | ||||
return self.settings.get('base_project_url', '/') | ||||
@property | ||||
def base_kernel_url(self): | ||||
return self.settings.get('base_kernel_url', '/') | ||||
#--------------------------------------------------------------- | ||||
# Manager objects | ||||
#--------------------------------------------------------------- | ||||
@property | ||||
def kernel_manager(self): | ||||
return self.settings['kernel_manager'] | ||||
@property | ||||
def notebook_manager(self): | ||||
return self.settings['notebook_manager'] | ||||
@property | ||||
def cluster_manager(self): | ||||
return self.settings['cluster_manager'] | ||||
@property | ||||
def project(self): | ||||
return self.notebook_manager.notebook_dir | ||||
#--------------------------------------------------------------- | ||||
# template rendering | ||||
#--------------------------------------------------------------- | ||||
def get_template(self, name): | ||||
"""Return the jinja template object for a given name""" | ||||
return self.settings['jinja2_env'].get_template(name) | ||||
def render_template(self, name, **ns): | ||||
ns.update(self.template_namespace) | ||||
template = self.get_template(name) | ||||
return template.render(**ns) | ||||
@property | ||||
def template_namespace(self): | ||||
return dict( | ||||
base_project_url=self.base_project_url, | ||||
base_kernel_url=self.base_kernel_url, | ||||
read_only=self.read_only, | ||||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
use_less=self.use_less, | ||||
) | ||||
Satrajit Ghosh
|
r4690 | |||
MinRK
|
r10355 | class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler): | ||
MinRK
|
r5824 | """static files should only be accessible when logged in""" | ||
@authenticate_unless_readonly | ||||
def get(self, path): | ||||
return web.StaticFileHandler.get(self, path) | ||||
MinRK
|
r10355 | class ProjectDashboardHandler(IPythonHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4488 | def get(self): | ||
MinRK
|
r10355 | self.write(self.render_template('projectdashboard.html', | ||
project=self.project, | ||||
project_component=self.project.split('/'), | ||||
)) | ||||
Brian E. Granger
|
r4488 | |||
Brian E. Granger
|
r5102 | |||
MinRK
|
r10355 | class LoginHandler(IPythonHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r10355 | def _render(self, message=None): | ||
self.write(self.render_template('login.html', | ||||
next=url_escape(self.get_argument('next', default=self.base_project_url)), | ||||
message=message, | ||||
Cameron Bates
|
r8350 | )) | ||
Satrajit Ghosh
|
r4690 | |||
Stefan van der Walt
|
r5323 | def get(self): | ||
Stefan van der Walt
|
r5327 | if self.current_user: | ||
MinRK
|
r10355 | self.redirect(self.get_argument('next', default=self.base_project_url)) | ||
Stefan van der Walt
|
r5327 | else: | ||
self._render() | ||||
Stefan van der Walt
|
r5323 | |||
Satrajit Ghosh
|
r4690 | def post(self): | ||
Brian E. Granger
|
r5109 | pwd = self.get_argument('password', default=u'') | ||
MinRK
|
r10355 | if self.login_available: | ||
if passwd_check(self.password, pwd): | ||||
self.set_secure_cookie(self.cookie_name, str(uuid.uuid4())) | ||||
Stefan van der Walt
|
r5323 | else: | ||
Stefan van der Walt
|
r5326 | self._render(message={'error': 'Invalid password'}) | ||
Stefan van der Walt
|
r5323 | return | ||
MinRK
|
r10355 | self.redirect(self.get_argument('next', default=self.base_project_url)) | ||
Brian E. Granger
|
r4488 | |||
Brian E. Granger
|
r5102 | |||
MinRK
|
r10355 | class LogoutHandler(IPythonHandler): | ||
Stefan van der Walt
|
r5325 | |||
def get(self): | ||||
MinRK
|
r10355 | self.clear_login_cookie() | ||
Stefan van der Walt
|
r5722 | if self.login_available: | ||
message = {'info': 'Successfully logged out.'} | ||||
else: | ||||
message = {'warning': 'Cannot log out. Notebook authentication ' | ||||
'is disabled.'} | ||||
MinRK
|
r10355 | self.write(self.render_template('logout.html', | ||
Cameron Bates
|
r8350 | message=message)) | ||
Stefan van der Walt
|
r5325 | |||
MinRK
|
r10355 | class NewHandler(IPythonHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4292 | def get(self): | ||
MinRK
|
r10355 | notebook_id = self.notebook_manager.new_notebook() | ||
self.redirect('/' + urljoin(self.base_project_url, notebook_id)) | ||||
Brian E. Granger
|
r4484 | |||
MinRK
|
r10355 | class NamedNotebookHandler(IPythonHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
MinRK
|
r10355 | nbm = self.notebook_manager | ||
Brian E. Granger
|
r4484 | if not nbm.notebook_exists(notebook_id): | ||
Cameron Bates
|
r8792 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
MinRK
|
r10355 | self.write(self.render_template('notebook.html', | ||
project=self.project, | ||||
Brian E. Granger
|
r5105 | notebook_id=notebook_id, | ||
MinRK
|
r5213 | kill_kernel=False, | ||
MinRK
|
r10355 | mathjax_url=self.mathjax_url, | ||
Matthias BUSSONNIER
|
r9384 | ) | ||
Matthias BUSSONNIER
|
r9296 | ) | ||
Brian Granger
|
r4292 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Kernel handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r10355 | class MainKernelHandler(IPythonHandler): | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4297 | def get(self): | ||
MinRK
|
r10355 | km = self.kernel_manager | ||
Brian E. Granger
|
r9112 | self.finish(jsonapi.dumps(km.list_kernel_ids())) | ||
Brian Granger
|
r4298 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4306 | def post(self): | ||
MinRK
|
r10355 | km = self.kernel_manager | ||
nbm = self.notebook_manager | ||||
Brian E. Granger
|
r4494 | notebook_id = self.get_argument('notebook', default=None) | ||
MinRK
|
r7558 | kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir) | ||
MinRK
|
r5252 | data = {'ws_url':self.ws_url,'kernel_id':kernel_id} | ||
MinRK
|
r10515 | self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id)) | ||
Brian E. Granger
|
r4572 | self.finish(jsonapi.dumps(data)) | ||
Brian Granger
|
r4297 | |||
MinRK
|
r10355 | class KernelHandler(IPythonHandler): | ||
Brian E. Granger
|
r4494 | |||
SUPPORTED_METHODS = ('DELETE') | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4494 | def delete(self, kernel_id): | ||
MinRK
|
r10355 | km = self.kernel_manager | ||
MinRK
|
r7627 | km.shutdown_kernel(kernel_id) | ||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
self.finish() | ||||
MinRK
|
r10355 | class KernelActionHandler(IPythonHandler): | ||
Brian Granger
|
r4308 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4309 | def post(self, kernel_id, action): | ||
MinRK
|
r10355 | km = self.kernel_manager | ||
Brian Granger
|
r4309 | if action == 'interrupt': | ||
Brian E. Granger
|
r4545 | km.interrupt_kernel(kernel_id) | ||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
Brian Granger
|
r4309 | if action == 'restart': | ||
Brian E. Granger
|
r9113 | km.restart_kernel(kernel_id) | ||
data = {'ws_url':self.ws_url, 'kernel_id':kernel_id} | ||||
MinRK
|
r10515 | self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id)) | ||
Brian E. Granger
|
r4572 | self.write(jsonapi.dumps(data)) | ||
Brian E. Granger
|
r4493 | self.finish() | ||
Brian Granger
|
r4308 | |||
Brian Granger
|
r4306 | class ZMQStreamHandler(websocket.WebSocketHandler): | ||
MinRK
|
r10360 | |||
MinRK
|
r10357 | def clear_cookie(self, *args, **kwargs): | ||
"""meaningless for websockets""" | ||||
pass | ||||
Brian E. Granger
|
r4545 | def _reserialize_reply(self, msg_list): | ||
"""Reserialize a reply message using JSON. | ||||
This takes the msg list from the ZMQ socket, unserializes it using | ||||
self.session and then serializes the result using JSON. This method | ||||
should be used by self._on_zmq_reply to build messages that can | ||||
be sent back to the browser. | ||||
""" | ||||
idents, msg_list = self.session.feed_identities(msg_list) | ||||
msg = self.session.unserialize(msg_list) | ||||
Brian E. Granger
|
r4561 | try: | ||
msg['header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
try: | ||||
msg['parent_header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
Brian E. Granger
|
r4545 | msg.pop('buffers') | ||
MinRK
|
r6870 | return jsonapi.dumps(msg, default=date_default) | ||
Brian E. Granger
|
r4545 | |||
Brian E. Granger
|
r4561 | def _on_zmq_reply(self, msg_list): | ||
Brian Granger
|
r10282 | # Sometimes this gets triggered when the on_close method is scheduled in the | ||
# eventloop but hasn't been called. | ||||
if self.stream.closed(): return | ||||
Brian E. Granger
|
r4561 | try: | ||
msg = self._reserialize_reply(msg_list) | ||||
MinRK
|
r6870 | except Exception: | ||
MinRK
|
r10360 | self.log.critical("Malformed message: %r" % msg_list, exc_info=True) | ||
Brian E. Granger
|
r4561 | else: | ||
self.write_message(msg) | ||||
MinRK
|
r6070 | def allow_draft76(self): | ||
"""Allow draft 76, until browsers such as Safari update to RFC 6455. | ||||
This has been disabled by default in tornado in release 2.2.0, and | ||||
support will be removed in later versions. | ||||
""" | ||||
return True | ||||
Brian E. Granger
|
r5114 | |||
MinRK
|
r10355 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): | ||
Brian E. Granger
|
r5114 | |||
MinRK
|
r4707 | def open(self, kernel_id): | ||
Thomas Kluyver
|
r4839 | self.kernel_id = kernel_id.decode('ascii') | ||
MinRK
|
r10355 | self.session = Session(config=self.config) | ||
MinRK
|
r4707 | self.save_on_message = self.on_message | ||
self.on_message = self.on_first_message | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def _inject_cookie_message(self, msg): | ||
"""Inject the first message, which is the document cookie, | ||||
for authentication.""" | ||||
Mikhail Korobov
|
r9110 | if not PY3 and isinstance(msg, unicode): | ||
# Cookie constructor doesn't accept unicode strings | ||||
# under Python 2.x for some reason | ||||
MinRK
|
r4707 | msg = msg.encode('utf8', 'replace') | ||
try: | ||||
MinRK
|
r10378 | identity, msg = msg.split(':', 1) | ||
self.session.session = identity.decode('ascii') | ||||
MinRK
|
r10365 | except Exception: | ||
MinRK
|
r10378 | logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg) | ||
MinRK
|
r10365 | try: | ||
Brian E. Granger
|
r5119 | self.request._cookies = Cookie.SimpleCookie(msg) | ||
MinRK
|
r4707 | except: | ||
MinRK
|
r10360 | self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True) | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
self._inject_cookie_message(msg) | ||||
if self.get_current_user() is None: | ||||
MinRK
|
r10360 | self.log.warn("Couldn't authenticate WebSocket connection") | ||
MinRK
|
r4707 | raise web.HTTPError(403) | ||
self.on_message = self.save_on_message | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | |||
MinRK
|
r10363 | class ZMQChannelHandler(AuthenticatedZMQStreamHandler): | ||
@property | ||||
def max_msg_size(self): | ||||
return self.settings.get('max_msg_size', 65535) | ||||
def create_stream(self): | ||||
km = self.kernel_manager | ||||
meth = getattr(km, 'connect_%s' % self.channel) | ||||
MinRK
|
r10365 | self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession) | ||
MinRK
|
r10363 | |||
Brian E. Granger
|
r4545 | def initialize(self, *args, **kwargs): | ||
MinRK
|
r10363 | self.zmq_stream = None | ||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
MinRK
|
r10363 | super(ZMQChannelHandler, self).on_first_message(msg) | ||
MinRK
|
r4707 | except web.HTTPError: | ||
self.close() | ||||
return | ||||
Brian E. Granger
|
r4572 | try: | ||
MinRK
|
r10363 | self.create_stream() | ||
Brian E. Granger
|
r4572 | except web.HTTPError: | ||
# WebSockets don't response to traditional error codes so we | ||||
# close the connection. | ||||
if not self.stream.closed(): | ||||
self.stream.close() | ||||
MinRK
|
r4707 | self.close() | ||
Brian E. Granger
|
r4572 | else: | ||
MinRK
|
r10363 | self.zmq_stream.on_recv(self._on_zmq_reply) | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def on_message(self, msg): | ||
MinRK
|
r10363 | if len(msg) < self.max_msg_size: | ||
msg = jsonapi.loads(msg) | ||||
self.session.send(self.zmq_stream, msg) | ||||
MinRK
|
r10315 | |||
Brian E. Granger
|
r4545 | def on_close(self): | ||
Bernardo B. Marques
|
r4872 | # This method can be called twice, once by self.kernel_died and once | ||
Brian E. Granger
|
r4572 | # from the WebSocket close event. If the WebSocket connection is | ||
# closed before the ZMQ streams are setup, they could be None. | ||||
MinRK
|
r10363 | if self.zmq_stream is not None and not self.zmq_stream.closed(): | ||
self.zmq_stream.on_recv(None) | ||||
self.zmq_stream.close() | ||||
class IOPubHandler(ZMQChannelHandler): | ||||
channel = 'iopub' | ||||
def create_stream(self): | ||||
super(IOPubHandler, self).create_stream() | ||||
km = self.kernel_manager | ||||
km.add_restart_callback(self.kernel_id, self.on_kernel_restarted) | ||||
km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead') | ||||
def on_close(self): | ||||
MinRK
|
r10355 | km = self.kernel_manager | ||
MinRK
|
r10321 | if self.kernel_id in km: | ||
km.remove_restart_callback( | ||||
self.kernel_id, self.on_kernel_restarted, | ||||
) | ||||
km.remove_restart_callback( | ||||
self.kernel_id, self.on_restart_failed, 'dead', | ||||
) | ||||
MinRK
|
r10363 | super(IOPubHandler, self).on_close() | ||
MinRK
|
r10355 | |||
MinRK
|
r10363 | def _send_status_message(self, status): | ||
msg = self.session.msg("status", | ||||
{'execution_state': status} | ||||
) | ||||
self.write_message(jsonapi.dumps(msg, default=date_default)) | ||||
Brian Granger
|
r4297 | |||
MinRK
|
r10363 | def on_kernel_restarted(self): | ||
logging.warn("kernel %s restarted", self.kernel_id) | ||||
self._send_status_message('restarting') | ||||
Brian E. Granger
|
r4545 | |||
MinRK
|
r10363 | def on_restart_failed(self): | ||
logging.error("kernel %s restarted failed!", self.kernel_id) | ||||
self._send_status_message('dead') | ||||
MinRK
|
r10372 | def on_message(self, msg): | ||
"""IOPub messages make no sense""" | ||||
pass | ||||
MinRK
|
r10363 | class ShellHandler(ZMQChannelHandler): | ||
channel = 'shell' | ||||
Brian Granger
|
r4297 | |||
MinRK
|
r10363 | class StdinHandler(ZMQChannelHandler): | ||
channel = 'stdin' | ||||
Brian Granger
|
r4297 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Notebook web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r10355 | class NotebookRedirectHandler(IPythonHandler): | ||
MinRK
|
r10008 | |||
@authenticate_unless_readonly | ||||
def get(self, notebook_name): | ||||
MinRK
|
r10187 | # strip trailing .ipynb: | ||
notebook_name = os.path.splitext(notebook_name)[0] | ||||
MinRK
|
r10355 | notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '') | ||
MinRK
|
r10008 | if notebook_id: | ||
url = self.settings.get('base_project_url', '/') + notebook_id | ||||
return self.redirect(url) | ||||
else: | ||||
raise HTTPError(404) | ||||
MinRK
|
r10355 | |||
class NotebookRootHandler(IPythonHandler): | ||||
Brian Granger
|
r4301 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian Granger
|
r4301 | def get(self): | ||
MinRK
|
r10355 | nbm = self.notebook_manager | ||
km = self.kernel_manager | ||||
Brian E. Granger
|
r4484 | files = nbm.list_notebooks() | ||
Matthias BUSSONNIER
|
r6841 | for f in files : | ||
Matthias BUSSONNIER
|
r6848 | f['kernel_id'] = km.kernel_for_notebook(f['notebook_id']) | ||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(files)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def post(self): | ||
MinRK
|
r10355 | nbm = self.notebook_manager | ||
Brian E. Granger
|
r4484 | body = self.request.body.strip() | ||
format = self.get_argument('format', default='json') | ||||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
Brian E. Granger
|
r4484 | if body: | ||
Brian E. Granger
|
r4491 | notebook_id = nbm.save_new_notebook(body, name=name, format=format) | ||
Brian E. Granger
|
r4484 | else: | ||
notebook_id = nbm.new_notebook() | ||||
MinRK
|
r10515 | self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id)) | ||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(notebook_id)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r10355 | class NotebookHandler(IPythonHandler): | ||
Brian Granger
|
r4301 | |||
Brian E. Granger
|
r4484 | SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') | ||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
MinRK
|
r10355 | nbm = self.notebook_manager | ||
Brian E. Granger
|
r4484 | format = self.get_argument('format', default='json') | ||
last_mod, name, data = nbm.get_notebook(notebook_id, format) | ||||
MinRK
|
r5200 | |||
Brian E. Granger
|
r4484 | if format == u'json': | ||
self.set_header('Content-Type', 'application/json') | ||||
Brian E. Granger
|
r4634 | self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) | ||
Brian E. Granger
|
r4484 | elif format == u'py': | ||
Brian E. Granger
|
r4493 | self.set_header('Content-Type', 'application/x-python') | ||
Brian E. Granger
|
r4558 | self.set_header('Content-Disposition','attachment; filename="%s.py"' % name) | ||
Brian E. Granger
|
r4484 | self.set_header('Last-Modified', last_mod) | ||
self.finish(data) | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def put(self, notebook_id): | ||
MinRK
|
r10355 | nbm = self.notebook_manager | ||
Brian E. Granger
|
r4484 | format = self.get_argument('format', default='json') | ||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
nbm.save_notebook(notebook_id, self.request.body, name=name, format=format) | ||||
Brian E. Granger
|
r4484 | self.set_status(204) | ||
Brian Granger
|
r4301 | self.finish() | ||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def delete(self, notebook_id): | ||
MinRK
|
r10355 | self.notebook_manager.delete_notebook(notebook_id) | ||
Brian Granger
|
r4301 | self.set_status(204) | ||
self.finish() | ||||
MinRK
|
r10498 | |||
MinRK
|
r10512 | class NotebookCheckpointsHandler(IPythonHandler): | ||
MinRK
|
r10499 | |||
SUPPORTED_METHODS = ('GET', 'POST') | ||||
MinRK
|
r10498 | |||
@web.authenticated | ||||
def get(self, notebook_id): | ||||
"""get lists checkpoints for a notebook""" | ||||
MinRK
|
r10512 | nbm = self.notebook_manager | ||
MinRK
|
r10498 | checkpoints = nbm.list_checkpoints(notebook_id) | ||
MinRK
|
r10499 | data = jsonapi.dumps(checkpoints, default=date_default) | ||
self.finish(data) | ||||
MinRK
|
r10498 | |||
@web.authenticated | ||||
def post(self, notebook_id): | ||||
MinRK
|
r10499 | """post creates a new checkpoint""" | ||
MinRK
|
r10512 | nbm = self.notebook_manager | ||
MinRK
|
r10499 | checkpoint = nbm.create_checkpoint(notebook_id) | ||
data = jsonapi.dumps(checkpoint, default=date_default) | ||||
MinRK
|
r10515 | self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format( | ||
self.base_project_url, notebook_id, checkpoint['checkpoint_id'] | ||||
)) | ||||
MinRK
|
r10512 | |||
MinRK
|
r10499 | self.finish(data) | ||
MinRK
|
r10512 | class ModifyNotebookCheckpointsHandler(IPythonHandler): | ||
MinRK
|
r10499 | |||
SUPPORTED_METHODS = ('POST', 'DELETE') | ||||
@web.authenticated | ||||
def post(self, notebook_id, checkpoint_id): | ||||
MinRK
|
r10498 | """post restores a notebook from a checkpoint""" | ||
MinRK
|
r10512 | nbm = self.notebook_manager | ||
MinRK
|
r10498 | nbm.restore_checkpoint(notebook_id, checkpoint_id) | ||
self.set_status(204) | ||||
self.finish() | ||||
@web.authenticated | ||||
MinRK
|
r10499 | def delete(self, notebook_id, checkpoint_id): | ||
MinRK
|
r10498 | """delete clears a checkpoint for a given notebook""" | ||
MinRK
|
r10512 | nbm = self.notebook_manager | ||
MinRK
|
r10498 | nbm.delte_checkpoint(notebook_id, checkpoint_id) | ||
self.set_status(204) | ||||
self.finish() | ||||
MinRK
|
r10355 | class NotebookCopyHandler(IPythonHandler): | ||
Brian Granger
|
r5860 | |||
@web.authenticated | ||||
def get(self, notebook_id): | ||||
MinRK
|
r10355 | notebook_id = self.notebook_manager.copy_notebook(notebook_id) | ||
self.redirect('/'+urljoin(self.base_project_url, notebook_id)) | ||||
Brian Granger
|
r5860 | |||
Brian Granger
|
r6191 | |||
#----------------------------------------------------------------------------- | ||||
# Cluster handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r10355 | class MainClusterHandler(IPythonHandler): | ||
Brian Granger
|
r6191 | |||
@web.authenticated | ||||
def get(self): | ||||
MinRK
|
r10355 | self.finish(jsonapi.dumps(self.cluster_manager.list_profiles())) | ||
Brian Granger
|
r6191 | |||
MinRK
|
r10355 | class ClusterProfileHandler(IPythonHandler): | ||
Brian Granger
|
r6191 | |||
@web.authenticated | ||||
def get(self, profile): | ||||
MinRK
|
r10355 | self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile))) | ||
Brian Granger
|
r6191 | |||
MinRK
|
r10355 | class ClusterActionHandler(IPythonHandler): | ||
Brian Granger
|
r6191 | |||
@web.authenticated | ||||
def post(self, profile, action): | ||||
MinRK
|
r10355 | cm = self.cluster_manager | ||
Brian Granger
|
r6191 | if action == 'start': | ||
Brian Granger
|
r6199 | n = self.get_argument('n',default=None) | ||
if n is None: | ||||
data = cm.start_cluster(profile) | ||||
else: | ||||
MinRK
|
r10355 | data = cm.start_cluster(profile, int(n)) | ||
Brian Granger
|
r6191 | if action == 'stop': | ||
Brian Granger
|
r6197 | data = cm.stop_cluster(profile) | ||
self.finish(jsonapi.dumps(data)) | ||||
Brian Granger
|
r6191 | |||
Brian E. Granger
|
r4507 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r10392 | # File handler | ||
Brian E. Granger
|
r4507 | #----------------------------------------------------------------------------- | ||
MinRK
|
r8016 | # to minimize subclass changes: | ||
HTTPError = web.HTTPError | ||||
Brian E. Granger
|
r4507 | |||
MinRK
|
r7922 | class FileFindHandler(web.StaticFileHandler): | ||
"""subclass of StaticFileHandler for serving files from a search path""" | ||||
_static_paths = {} | ||||
MinRK
|
r8016 | # _lock is needed for tornado < 2.2.0 compat | ||
_lock = threading.Lock() # protects _static_hashes | ||||
MinRK
|
r7922 | |||
def initialize(self, path, default_filename=None): | ||||
MinRK
|
r7930 | if isinstance(path, basestring): | ||
path = [path] | ||||
MinRK
|
r7922 | self.roots = tuple( | ||
os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path | ||||
) | ||||
self.default_filename = default_filename | ||||
@classmethod | ||||
def locate_file(cls, path, roots): | ||||
"""locate a file to serve on our static file search path""" | ||||
with cls._lock: | ||||
if path in cls._static_paths: | ||||
return cls._static_paths[path] | ||||
try: | ||||
abspath = os.path.abspath(filefind(path, roots)) | ||||
except IOError: | ||||
# empty string should always give exists=False | ||||
return '' | ||||
# os.path.abspath strips a trailing / | ||||
# it needs to be temporarily added back for requests to root/ | ||||
if not (abspath + os.path.sep).startswith(roots): | ||||
MinRK
|
r8016 | raise HTTPError(403, "%s is not in root static directory", path) | ||
MinRK
|
r7922 | |||
cls._static_paths[path] = abspath | ||||
return abspath | ||||
def get(self, path, include_body=True): | ||||
path = self.parse_url_path(path) | ||||
MinRK
|
r8016 | # begin subclass override | ||
abspath = self.locate_file(path, self.roots) | ||||
# end subclass override | ||||
MinRK
|
r7922 | |||
if os.path.isdir(abspath) and self.default_filename is not None: | ||||
# need to look at the request.path here for when path is empty | ||||
# but there is some prefix to the path that was already | ||||
# trimmed by the routing | ||||
if not self.request.path.endswith("/"): | ||||
self.redirect(self.request.path + "/") | ||||
return | ||||
abspath = os.path.join(abspath, self.default_filename) | ||||
if not os.path.exists(abspath): | ||||
MinRK
|
r8016 | raise HTTPError(404) | ||
MinRK
|
r7922 | if not os.path.isfile(abspath): | ||
MinRK
|
r8016 | raise HTTPError(403, "%s is not a file", path) | ||
MinRK
|
r7922 | |||
stat_result = os.stat(abspath) | ||||
MinRK
|
r10228 | modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME]) | ||
MinRK
|
r7922 | |||
self.set_header("Last-Modified", modified) | ||||
mime_type, encoding = mimetypes.guess_type(abspath) | ||||
if mime_type: | ||||
self.set_header("Content-Type", mime_type) | ||||
cache_time = self.get_cache_time(path, modified, mime_type) | ||||
if cache_time > 0: | ||||
self.set_header("Expires", datetime.datetime.utcnow() + \ | ||||
datetime.timedelta(seconds=cache_time)) | ||||
self.set_header("Cache-Control", "max-age=" + str(cache_time)) | ||||
else: | ||||
self.set_header("Cache-Control", "public") | ||||
self.set_extra_headers(path) | ||||
# Check the If-Modified-Since, and don't send the result if the | ||||
# content has not been modified | ||||
ims_value = self.request.headers.get("If-Modified-Since") | ||||
if ims_value is not None: | ||||
date_tuple = email.utils.parsedate(ims_value) | ||||
MinRK
|
r10228 | if_since = datetime.datetime(*date_tuple[:6]) | ||
MinRK
|
r7922 | if if_since >= modified: | ||
self.set_status(304) | ||||
return | ||||
with open(abspath, "rb") as file: | ||||
data = file.read() | ||||
hasher = hashlib.sha1() | ||||
hasher.update(data) | ||||
self.set_header("Etag", '"%s"' % hasher.hexdigest()) | ||||
if include_body: | ||||
self.write(data) | ||||
else: | ||||
assert self.request.method == "HEAD" | ||||
self.set_header("Content-Length", len(data)) | ||||
@classmethod | ||||
def get_version(cls, settings, path): | ||||
"""Generate the version string to be used in static URLs. | ||||
This method may be overridden in subclasses (but note that it | ||||
is a class method rather than a static method). The default | ||||
implementation uses a hash of the file's contents. | ||||
``settings`` is the `Application.settings` dictionary and ``path`` | ||||
is the relative location of the requested asset on the filesystem. | ||||
The returned value should be a string, or ``None`` if no version | ||||
could be determined. | ||||
""" | ||||
# begin subclass override: | ||||
MinRK
|
r7930 | static_paths = settings['static_path'] | ||
if isinstance(static_paths, basestring): | ||||
static_paths = [static_paths] | ||||
MinRK
|
r7922 | roots = tuple( | ||
MinRK
|
r7930 | os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths | ||
MinRK
|
r7922 | ) | ||
try: | ||||
abs_path = filefind(path, roots) | ||||
MinRK
|
r7930 | except IOError: | ||
MinRK
|
r10360 | app_log.error("Could not find static file %r", path) | ||
MinRK
|
r7922 | return None | ||
# end subclass override | ||||
with cls._lock: | ||||
hashes = cls._static_hashes | ||||
if abs_path not in hashes: | ||||
try: | ||||
f = open(abs_path, "rb") | ||||
hashes[abs_path] = hashlib.md5(f.read()).hexdigest() | ||||
f.close() | ||||
except Exception: | ||||
MinRK
|
r10360 | app_log.error("Could not open static file %r", path) | ||
MinRK
|
r7922 | hashes[abs_path] = None | ||
hsh = hashes.get(abs_path) | ||||
if hsh: | ||||
return hsh[:5] | ||||
return None | ||||
MinRK
|
r8016 | |||
def parse_url_path(self, url_path): | ||||
"""Converts a static URL path into a filesystem path. | ||||
``url_path`` is the path component of the URL with | ||||
``static_url_prefix`` removed. The return value should be | ||||
filesystem path relative to ``static_path``. | ||||
""" | ||||
if os.path.sep != "/": | ||||
url_path = url_path.replace("/", os.path.sep) | ||||
return url_path | ||||