diff --git a/IPython/frontend/html/notebook/handlers/base.py b/IPython/frontend/html/notebook/handlers/base.py
index 32ec552..5d42ea2 100644
--- a/IPython/frontend/html/notebook/handlers/base.py
+++ b/IPython/frontend/html/notebook/handlers/base.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Base Tornado handlers for the notebook.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,19 +16,8 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
+
from tornado import web
from tornado import websocket
@@ -37,21 +26,8 @@ try:
except ImportError:
app_log = logging.getLogger()
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
from IPython.config import Application
from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
#-----------------------------------------------------------------------------
# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
@@ -131,14 +107,6 @@ def authenticate_unless_readonly(f, self, *args, **kwargs):
else:
return auth_f(self, *args, **kwargs)
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
-
#-----------------------------------------------------------------------------
# Top-level handlers
#-----------------------------------------------------------------------------
@@ -306,626 +274,3 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
@authenticate_unless_readonly
def get(self, path):
return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/clustersapi.py b/IPython/frontend/html/notebook/handlers/clustersapi.py
index 32ec552..25b35d7 100644
--- a/IPython/frontend/html/notebook/handlers/clustersapi.py
+++ b/IPython/frontend/html/notebook/handlers/clustersapi.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers for cluster web service.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,723 +16,11 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-from zmq.eventloop import ioloop
from zmq.utils import jsonapi
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
-
-#-----------------------------------------------------------------------------
-# Top-level handlers
-#-----------------------------------------------------------------------------
-
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
+from .base import IPythonHandler
#-----------------------------------------------------------------------------
# Cluster handlers
@@ -767,165 +55,3 @@ class ClusterActionHandler(IPythonHandler):
if action == 'stop':
data = cm.stop_cluster(profile)
self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/files.py b/IPython/frontend/html/notebook/handlers/files.py
new file mode 100644
index 0000000..d17518b
--- /dev/null
+++ b/IPython/frontend/html/notebook/handlers/files.py
@@ -0,0 +1,196 @@
+"""Tornado handlers handling general files.
+
+Authors:
+
+* Brian Granger
+"""
+
+#-----------------------------------------------------------------------------
+# Copyright (C) 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.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+import datetime
+import email.utils
+import hashlib
+import logging
+import mimetypes
+import os
+import stat
+import threading
+
+from tornado import web
+
+try:
+ from tornado.log import app_log
+except ImportError:
+ app_log = logging.getLogger()
+
+from IPython.utils.path import filefind
+
+#-----------------------------------------------------------------------------
+# File handler
+#-----------------------------------------------------------------------------
+
+# to minimize subclass changes:
+HTTPError = web.HTTPError
+
+class FileFindHandler(web.StaticFileHandler):
+ """subclass of StaticFileHandler for serving files from a search path"""
+
+ _static_paths = {}
+ # _lock is needed for tornado < 2.2.0 compat
+ _lock = threading.Lock() # protects _static_hashes
+
+ def initialize(self, path, default_filename=None):
+ if isinstance(path, basestring):
+ path = [path]
+ 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):
+ raise HTTPError(403, "%s is not in root static directory", path)
+
+ cls._static_paths[path] = abspath
+ return abspath
+
+ def get(self, path, include_body=True):
+ path = self.parse_url_path(path)
+
+ # begin subclass override
+ abspath = self.locate_file(path, self.roots)
+ # end subclass override
+
+ 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):
+ raise HTTPError(404)
+ if not os.path.isfile(abspath):
+ raise HTTPError(403, "%s is not a file", path)
+
+ stat_result = os.stat(abspath)
+ modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
+
+ 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)
+ if_since = datetime.datetime(*date_tuple[:6])
+ 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:
+ static_paths = settings['static_path']
+ if isinstance(static_paths, basestring):
+ static_paths = [static_paths]
+ roots = tuple(
+ os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
+ )
+
+ try:
+ abs_path = filefind(path, roots)
+ except IOError:
+ app_log.error("Could not find static file %r", path)
+ 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:
+ app_log.error("Could not open static file %r", path)
+ hashes[abs_path] = None
+ hsh = hashes.get(abs_path)
+ if hsh:
+ return hsh[:5]
+ return None
+
+
+ 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
+
+
diff --git a/IPython/frontend/html/notebook/handlers/handlers.py b/IPython/frontend/html/notebook/handlers/handlers.py
deleted file mode 100644
index 32ec552..0000000
--- a/IPython/frontend/html/notebook/handlers/handlers.py
+++ /dev/null
@@ -1,931 +0,0 @@
-"""Tornado handlers for the notebook.
-
-Authors:
-
-* Brian Granger
-"""
-
-#-----------------------------------------------------------------------------
-# 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.
-#-----------------------------------------------------------------------------
-
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
-
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
-from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
-
-#-----------------------------------------------------------------------------
-# Top-level handlers
-#-----------------------------------------------------------------------------
-
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/kernelsapi.py b/IPython/frontend/html/notebook/handlers/kernelsapi.py
index 32ec552..c1519d3 100644
--- a/IPython/frontend/html/notebook/handlers/kernelsapi.py
+++ b/IPython/frontend/html/notebook/handlers/kernelsapi.py
@@ -17,368 +17,17 @@ Authors:
#-----------------------------------------------------------------------------
import Cookie
-import datetime
-import email.utils
-import hashlib
import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
from tornado import web
from tornado import websocket
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
from zmq.utils import jsonapi
-from IPython.config import Application
-from IPython.external.decorator import decorator
from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
from IPython.utils.py3compat import PY3
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
-
-#-----------------------------------------------------------------------------
-# Top-level handlers
-#-----------------------------------------------------------------------------
-
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
+from .base import IPythonHandler
#-----------------------------------------------------------------------------
# Kernel handlers
@@ -600,332 +249,3 @@ class ShellHandler(ZMQChannelHandler):
class StdinHandler(ZMQChannelHandler):
channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/login.py b/IPython/frontend/html/notebook/handlers/login.py
index 32ec552..27d953c 100644
--- a/IPython/frontend/html/notebook/handlers/login.py
+++ b/IPython/frontend/html/notebook/handlers/login.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers logging into the notebook.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,307 +16,18 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
import uuid
from tornado.escape import url_escape
-from tornado import web
-from tornado import websocket
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
+from .base import IPythonHandler
#-----------------------------------------------------------------------------
-# Top-level handlers
+# Handler
#-----------------------------------------------------------------------------
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
class LoginHandler(IPythonHandler):
@@ -344,588 +55,3 @@ class LoginHandler(IPythonHandler):
self.redirect(self.get_argument('next', default=self.base_project_url))
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/logout.py b/IPython/frontend/html/notebook/handlers/logout.py
index 32ec552..7474eba 100644
--- a/IPython/frontend/html/notebook/handlers/logout.py
+++ b/IPython/frontend/html/notebook/handlers/logout.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers for logging out of the notebook.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,333 +16,12 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
-from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
+from .base import IPythonHandler
#-----------------------------------------------------------------------------
-# Top-level handlers
+# Handler
#-----------------------------------------------------------------------------
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
class LogoutHandler(IPythonHandler):
@@ -355,577 +34,3 @@ class LogoutHandler(IPythonHandler):
'is disabled.'}
self.write(self.render_template('logout.html',
message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/handlers/notebooks.py b/IPython/frontend/html/notebook/handlers/notebooks.py
index 32ec552..e0fced3 100644
--- a/IPython/frontend/html/notebook/handlers/notebooks.py
+++ b/IPython/frontend/html/notebook/handlers/notebooks.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers for the live notebook view.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,353 +16,25 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
+HTTPError = web.HTTPError
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
+from .base import IPythonHandler, authenticate_unless_readonly
+from ..utils import url_path_join
#-----------------------------------------------------------------------------
-# Top-level handlers
+# Handlers
#-----------------------------------------------------------------------------
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
class NewHandler(IPythonHandler):
@web.authenticated
def get(self):
notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
+ self.redirect('/' + url_path_join(self.base_project_url, notebook_id))
+
class NamedNotebookHandler(IPythonHandler):
@@ -380,232 +52,6 @@ class NamedNotebookHandler(IPythonHandler):
)
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
class NotebookRedirectHandler(IPythonHandler):
@authenticate_unless_readonly
@@ -620,312 +66,10 @@ class NotebookRedirectHandler(IPythonHandler):
raise HTTPError(404)
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
class NotebookCopyHandler(IPythonHandler):
@web.authenticated
def get(self, notebook_id):
notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
+ self.redirect('/'+url_path_join(self.base_project_url, notebook_id))
diff --git a/IPython/frontend/html/notebook/handlers/notebooksapi.py b/IPython/frontend/html/notebook/handlers/notebooksapi.py
index 32ec552..c0f4db7 100644
--- a/IPython/frontend/html/notebook/handlers/notebooksapi.py
+++ b/IPython/frontend/html/notebook/handlers/notebooksapi.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers for the notebooks web service.
Authors:
@@ -16,610 +16,18 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-from zmq.eventloop import ioloop
from zmq.utils import jsonapi
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
-
-#-----------------------------------------------------------------------------
-# Top-level handlers
-#-----------------------------------------------------------------------------
-
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
-
-class ProjectDashboardHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- self.write(self.render_template('projectdashboard.html',
- project=self.project,
- project_component=self.project.split('/'),
- ))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
+from .base import IPythonHandler, authenticate_unless_readonly
#-----------------------------------------------------------------------------
# Notebook web service handlers
#-----------------------------------------------------------------------------
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
class NotebookRootHandler(IPythonHandler):
@authenticate_unless_readonly
@@ -726,206 +134,5 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler):
self.finish()
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
diff --git a/IPython/frontend/html/notebook/handlers/tree.py b/IPython/frontend/html/notebook/handlers/tree.py
index 32ec552..ffcfce6 100644
--- a/IPython/frontend/html/notebook/handlers/tree.py
+++ b/IPython/frontend/html/notebook/handlers/tree.py
@@ -1,4 +1,4 @@
-"""Tornado handlers for the notebook.
+"""Tornado handlers for the tree view.
Authors:
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# Copyright (C) 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.
@@ -16,297 +16,12 @@ Authors:
# Imports
#-----------------------------------------------------------------------------
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import logging
-import mimetypes
-import os
-import stat
-import threading
-import time
-import uuid
-
-from tornado.escape import url_escape
-from tornado import web
-from tornado import websocket
-
-try:
- from tornado.log import app_log
-except ImportError:
- app_log = logging.getLogger()
-
-from zmq.eventloop import ioloop
-from zmq.utils import jsonapi
-
-from IPython.config import Application
-from IPython.external.decorator import decorator
-from IPython.kernel.zmq.session import Session
-from IPython.lib.security import passwd_check
-from IPython.utils.jsonutil import date_default
-from IPython.utils.path import filefind
-from IPython.utils.py3compat import PY3
-
-try:
- from docutils.core import publish_string
-except ImportError:
- publish_string = None
-
-#-----------------------------------------------------------------------------
-# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
-#-----------------------------------------------------------------------------
-
-# 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
-
-if tornado.version_info <= (2,1,1):
-
- 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()
-
- websocket.WebSocketHandler._execute = _execute
- del _execute
-
-#-----------------------------------------------------------------------------
-# Decorator for disabling read-only handlers
-#-----------------------------------------------------------------------------
-
-@decorator
-def not_if_readonly(f, self, *args, **kwargs):
- if self.settings.get('read_only', False):
- raise web.HTTPError(403, "Notebook server is read-only")
- else:
- return f(self, *args, **kwargs)
-
-@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)
-
- if self.settings.get('read_only', False):
- return f(self, *args, **kwargs)
- else:
- return auth_f(self, *args, **kwargs)
-
-def urljoin(*pieces):
- """Join components of url into a relative url
-
- Use to prevent double slash when joining subpath
- """
- striped = [s.strip('/') for s in pieces]
- return '/'.join(s for s in striped if s)
+from .base import IPythonHandler, authenticate_unless_readonly
#-----------------------------------------------------------------------------
-# Top-level handlers
+# Handlers
#-----------------------------------------------------------------------------
-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):
- """A RequestHandler with an authenticated user."""
-
- def clear_login_cookie(self):
- self.clear_cookie(self.cookie_name)
-
- def get_current_user(self):
- user_id = self.get_secure_cookie(self.cookie_name)
- # For now the user_id should not return empty, but it could eventually
- if user_id == '':
- user_id = 'anonymous'
- if user_id is None:
- # prevent extra Invalid cookie sig warnings:
- self.clear_login_cookie()
- if not self.read_only and not self.login_available:
- user_id = 'anonymous'
- return user_id
-
- @property
- def cookie_name(self):
- return self.settings.get('cookie_name', '')
-
- @property
- def password(self):
- """our password"""
- return self.settings.get('password', '')
-
- @property
- 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.
-
- """
- return bool(self.settings.get('password', ''))
-
- @property
- def read_only(self):
- """Is the notebook read-only?
-
- """
- return self.settings.get('read_only', False)
-
-
-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)
-
- @property
- 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
- def use_less(self):
- """Use less instead of css in templates"""
- return self.settings.get('use_less', False)
-
- #---------------------------------------------------------------
- # URLs
- #---------------------------------------------------------------
-
- @property
- def ws_url(self):
- """websocket url matching the current request
-
- turns http[s]://host[:port] into
- ws[s]://host[:port]
- """
- proto = self.request.protocol.replace('http', 'ws')
- host = self.settings.get('websocket_host', '')
- # default to config value
- if host == '':
- host = self.request.host # get from request
- return "%s://%s" % (proto, host)
-
- @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,
- )
-
-class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
- """static files should only be accessible when logged in"""
-
- @authenticate_unless_readonly
- def get(self, path):
- return web.StaticFileHandler.get(self, path)
-
class ProjectDashboardHandler(IPythonHandler):
@@ -316,616 +31,3 @@ class ProjectDashboardHandler(IPythonHandler):
project=self.project,
project_component=self.project.split('/'),
))
-
-
-class LoginHandler(IPythonHandler):
-
- 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,
- ))
-
- def get(self):
- if self.current_user:
- self.redirect(self.get_argument('next', default=self.base_project_url))
- else:
- self._render()
-
- def post(self):
- pwd = self.get_argument('password', default=u'')
- if self.login_available:
- if passwd_check(self.password, pwd):
- self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
- else:
- self._render(message={'error': 'Invalid password'})
- return
-
- self.redirect(self.get_argument('next', default=self.base_project_url))
-
-
-class LogoutHandler(IPythonHandler):
-
- def get(self):
- self.clear_login_cookie()
- if self.login_available:
- message = {'info': 'Successfully logged out.'}
- else:
- message = {'warning': 'Cannot log out. Notebook authentication '
- 'is disabled.'}
- self.write(self.render_template('logout.html',
- message=message))
-
-
-class NewHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- notebook_id = self.notebook_manager.new_notebook()
- self.redirect('/' + urljoin(self.base_project_url, notebook_id))
-
-class NamedNotebookHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- if not nbm.notebook_exists(notebook_id):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
- self.write(self.render_template('notebook.html',
- project=self.project,
- notebook_id=notebook_id,
- kill_kernel=False,
- mathjax_url=self.mathjax_url,
- )
- )
-
-
-#-----------------------------------------------------------------------------
-# Kernel handlers
-#-----------------------------------------------------------------------------
-
-
-class MainKernelHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- km = self.kernel_manager
- self.finish(jsonapi.dumps(km.list_kernel_ids()))
-
- @web.authenticated
- def post(self):
- km = self.kernel_manager
- nbm = self.notebook_manager
- notebook_id = self.get_argument('notebook', default=None)
- kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
- data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.finish(jsonapi.dumps(data))
-
-
-class KernelHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('DELETE')
-
- @web.authenticated
- def delete(self, kernel_id):
- km = self.kernel_manager
- km.shutdown_kernel(kernel_id)
- self.set_status(204)
- self.finish()
-
-
-class KernelActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, kernel_id, action):
- km = self.kernel_manager
- if action == 'interrupt':
- km.interrupt_kernel(kernel_id)
- self.set_status(204)
- if action == 'restart':
- km.restart_kernel(kernel_id)
- data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
- self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
- self.write(jsonapi.dumps(data))
- self.finish()
-
-
-class ZMQStreamHandler(websocket.WebSocketHandler):
-
- def clear_cookie(self, *args, **kwargs):
- """meaningless for websockets"""
- pass
-
- 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)
- try:
- msg['header'].pop('date')
- except KeyError:
- pass
- try:
- msg['parent_header'].pop('date')
- except KeyError:
- pass
- msg.pop('buffers')
- return jsonapi.dumps(msg, default=date_default)
-
- def _on_zmq_reply(self, msg_list):
- # Sometimes this gets triggered when the on_close method is scheduled in the
- # eventloop but hasn't been called.
- if self.stream.closed(): return
- try:
- msg = self._reserialize_reply(msg_list)
- except Exception:
- self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
- else:
- self.write_message(msg)
-
- 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
-
-
-class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
-
- def open(self, kernel_id):
- self.kernel_id = kernel_id.decode('ascii')
- self.session = Session(config=self.config)
- self.save_on_message = self.on_message
- self.on_message = self.on_first_message
-
- def _inject_cookie_message(self, msg):
- """Inject the first message, which is the document cookie,
- for authentication."""
- if not PY3 and isinstance(msg, unicode):
- # Cookie constructor doesn't accept unicode strings
- # under Python 2.x for some reason
- msg = msg.encode('utf8', 'replace')
- try:
- identity, msg = msg.split(':', 1)
- self.session.session = identity.decode('ascii')
- except Exception:
- logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
-
- try:
- self.request._cookies = Cookie.SimpleCookie(msg)
- except:
- self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
-
- def on_first_message(self, msg):
- self._inject_cookie_message(msg)
- if self.get_current_user() is None:
- self.log.warn("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
- self.on_message = self.save_on_message
-
-
-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)
- self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-
- def initialize(self, *args, **kwargs):
- self.zmq_stream = None
-
- def on_first_message(self, msg):
- try:
- super(ZMQChannelHandler, self).on_first_message(msg)
- except web.HTTPError:
- self.close()
- return
- try:
- self.create_stream()
- except web.HTTPError:
- # WebSockets don't response to traditional error codes so we
- # close the connection.
- if not self.stream.closed():
- self.stream.close()
- self.close()
- else:
- self.zmq_stream.on_recv(self._on_zmq_reply)
-
- def on_message(self, msg):
- if len(msg) < self.max_msg_size:
- msg = jsonapi.loads(msg)
- self.session.send(self.zmq_stream, msg)
-
- def on_close(self):
- # This method can be called twice, once by self.kernel_died and once
- # from the WebSocket close event. If the WebSocket connection is
- # closed before the ZMQ streams are setup, they could be None.
- 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):
- km = self.kernel_manager
- 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',
- )
- super(IOPubHandler, self).on_close()
-
- def _send_status_message(self, status):
- msg = self.session.msg("status",
- {'execution_state': status}
- )
- self.write_message(jsonapi.dumps(msg, default=date_default))
-
- def on_kernel_restarted(self):
- logging.warn("kernel %s restarted", self.kernel_id)
- self._send_status_message('restarting')
-
- def on_restart_failed(self):
- logging.error("kernel %s restarted failed!", self.kernel_id)
- self._send_status_message('dead')
-
- def on_message(self, msg):
- """IOPub messages make no sense"""
- pass
-
-class ShellHandler(ZMQChannelHandler):
- channel = 'shell'
-
-class StdinHandler(ZMQChannelHandler):
- channel = 'stdin'
-
-
-#-----------------------------------------------------------------------------
-# Notebook web service handlers
-#-----------------------------------------------------------------------------
-
-class NotebookRedirectHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self, notebook_name):
- # strip trailing .ipynb:
- notebook_name = os.path.splitext(notebook_name)[0]
- notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
- if notebook_id:
- url = self.settings.get('base_project_url', '/') + notebook_id
- return self.redirect(url)
- else:
- raise HTTPError(404)
-
-
-class NotebookRootHandler(IPythonHandler):
-
- @authenticate_unless_readonly
- def get(self):
- nbm = self.notebook_manager
- km = self.kernel_manager
- files = nbm.list_notebooks()
- for f in files :
- f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
- self.finish(jsonapi.dumps(files))
-
- @web.authenticated
- def post(self):
- nbm = self.notebook_manager
- body = self.request.body.strip()
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- if body:
- notebook_id = nbm.save_new_notebook(body, name=name, format=format)
- else:
- notebook_id = nbm.new_notebook()
- self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
- self.finish(jsonapi.dumps(notebook_id))
-
-
-class NotebookHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
-
- @authenticate_unless_readonly
- def get(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- last_mod, name, data = nbm.get_notebook(notebook_id, format)
-
- if format == u'json':
- self.set_header('Content-Type', 'application/json')
- self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
- elif format == u'py':
- self.set_header('Content-Type', 'application/x-python')
- self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
- self.set_header('Last-Modified', last_mod)
- self.finish(data)
-
- @web.authenticated
- def put(self, notebook_id):
- nbm = self.notebook_manager
- format = self.get_argument('format', default='json')
- name = self.get_argument('name', default=None)
- nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id):
- self.notebook_manager.delete_notebook(notebook_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('GET', 'POST')
-
- @web.authenticated
- def get(self, notebook_id):
- """get lists checkpoints for a notebook"""
- nbm = self.notebook_manager
- checkpoints = nbm.list_checkpoints(notebook_id)
- data = jsonapi.dumps(checkpoints, default=date_default)
- self.finish(data)
-
- @web.authenticated
- def post(self, notebook_id):
- """post creates a new checkpoint"""
- nbm = self.notebook_manager
- checkpoint = nbm.create_checkpoint(notebook_id)
- data = jsonapi.dumps(checkpoint, default=date_default)
- self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
- self.base_project_url, notebook_id, checkpoint['checkpoint_id']
- ))
-
- self.finish(data)
-
-
-class ModifyNotebookCheckpointsHandler(IPythonHandler):
-
- SUPPORTED_METHODS = ('POST', 'DELETE')
-
- @web.authenticated
- def post(self, notebook_id, checkpoint_id):
- """post restores a notebook from a checkpoint"""
- nbm = self.notebook_manager
- nbm.restore_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
- @web.authenticated
- def delete(self, notebook_id, checkpoint_id):
- """delete clears a checkpoint for a given notebook"""
- nbm = self.notebook_manager
- nbm.delte_checkpoint(notebook_id, checkpoint_id)
- self.set_status(204)
- self.finish()
-
-
-class NotebookCopyHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, notebook_id):
- notebook_id = self.notebook_manager.copy_notebook(notebook_id)
- self.redirect('/'+urljoin(self.base_project_url, notebook_id))
-
-
-#-----------------------------------------------------------------------------
-# Cluster handlers
-#-----------------------------------------------------------------------------
-
-
-class MainClusterHandler(IPythonHandler):
-
- @web.authenticated
- def get(self):
- self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
-
-
-class ClusterProfileHandler(IPythonHandler):
-
- @web.authenticated
- def get(self, profile):
- self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
-
-
-class ClusterActionHandler(IPythonHandler):
-
- @web.authenticated
- def post(self, profile, action):
- cm = self.cluster_manager
- if action == 'start':
- n = self.get_argument('n',default=None)
- if n is None:
- data = cm.start_cluster(profile)
- else:
- data = cm.start_cluster(profile, int(n))
- if action == 'stop':
- data = cm.stop_cluster(profile)
- self.finish(jsonapi.dumps(data))
-
-
-#-----------------------------------------------------------------------------
-# File handler
-#-----------------------------------------------------------------------------
-
-# to minimize subclass changes:
-HTTPError = web.HTTPError
-
-class FileFindHandler(web.StaticFileHandler):
- """subclass of StaticFileHandler for serving files from a search path"""
-
- _static_paths = {}
- # _lock is needed for tornado < 2.2.0 compat
- _lock = threading.Lock() # protects _static_hashes
-
- def initialize(self, path, default_filename=None):
- if isinstance(path, basestring):
- path = [path]
- 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):
- raise HTTPError(403, "%s is not in root static directory", path)
-
- cls._static_paths[path] = abspath
- return abspath
-
- def get(self, path, include_body=True):
- path = self.parse_url_path(path)
-
- # begin subclass override
- abspath = self.locate_file(path, self.roots)
- # end subclass override
-
- 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):
- raise HTTPError(404)
- if not os.path.isfile(abspath):
- raise HTTPError(403, "%s is not a file", path)
-
- stat_result = os.stat(abspath)
- modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
-
- 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)
- if_since = datetime.datetime(*date_tuple[:6])
- 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:
- static_paths = settings['static_path']
- if isinstance(static_paths, basestring):
- static_paths = [static_paths]
- roots = tuple(
- os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
- )
-
- try:
- abs_path = filefind(path, roots)
- except IOError:
- app_log.error("Could not find static file %r", path)
- 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:
- app_log.error("Could not open static file %r", path)
- hashes[abs_path] = None
- hsh = hashes.get(abs_path)
- if hsh:
- return hsh[:5]
- return None
-
-
- 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
-
-
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
index 344fd3c..cf6b7e2 100644
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -21,7 +21,6 @@ import errno
import logging
import os
import random
-import re
import select
import signal
import socket
@@ -64,40 +63,54 @@ from tornado import web
# Our own libraries
from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
from .kernelmanager import MappingKernelManager
-from .handlers import (LoginHandler, LogoutHandler,
- ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
- MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler, StdinHandler,
- ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
- NotebookRedirectHandler, NotebookCheckpointsHandler, ModifyNotebookCheckpointsHandler,
- AuthenticatedFileHandler, FileFindHandler,
- MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
+
+from .handlers.clustersapi import (
+ MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
+)
+from .handlers.kernelsapi import (
+ MainKernelHandler, KernelHandler, KernelActionHandler,
+ IOPubHandler, StdinHandler, ShellHandler
+)
+from .handlers.notebooksapi import (
+ NotebookRootHandler, NotebookHandler,
+ NotebookCheckpointsHandler, ModifyNotebookCheckpointsHandler
+)
+from .handlers.tree import ProjectDashboardHandler
+from .handlers.login import LoginHandler
+from .handlers.logout import LogoutHandler
+from .handlers.notebooks import (
+ NewHandler, NamedNotebookHandler,
+ NotebookCopyHandler, NotebookRedirectHandler
)
+
+from .handlers.base import AuthenticatedFileHandler
+from .handlers.files import FileFindHandler
+
from .nbmanager import NotebookManager
from .filenbmanager import FileNotebookManager
from .clustermanager import ClusterManager
from IPython.config.application import catch_config_error, boolean_flag
from IPython.core.application import BaseIPythonApplication
-from IPython.core.profiledir import ProfileDir
from IPython.frontend.consoleapp import IPythonConsoleApp
from IPython.kernel import swallow_argv
-from IPython.kernel.zmq.session import Session, default_secure
-from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
+from IPython.kernel.zmq.session import default_secure
from IPython.kernel.zmq.kernelapp import (
kernel_flags,
kernel_aliases,
- IPKernelApp
)
from IPython.utils.importstring import import_item
from IPython.utils.localinterfaces import LOCALHOST
from IPython.utils import submodule
from IPython.utils.traitlets import (
- Dict, Unicode, Integer, List, Enum, Bool,
+ Dict, Unicode, Integer, List, Bool,
DottedObjectName
)
from IPython.utils import py3compat
from IPython.utils.path import filefind
+from .utils import url_path_join
+
#-----------------------------------------------------------------------------
# Module globals
#-----------------------------------------------------------------------------
@@ -122,12 +135,6 @@ ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
# Helper functions
#-----------------------------------------------------------------------------
-def url_path_join(a,b):
- if a.endswith('/') and b.startswith('/'):
- return a[:-1]+b
- else:
- return a+b
-
def random_ports(port, n):
"""Generate a list of n random ports near the given port.
diff --git a/IPython/frontend/html/notebook/utils.py b/IPython/frontend/html/notebook/utils.py
new file mode 100644
index 0000000..cf3fba8
--- /dev/null
+++ b/IPython/frontend/html/notebook/utils.py
@@ -0,0 +1,31 @@
+"""Notebook related utilities
+
+Authors:
+
+* Brian Granger
+"""
+
+#-----------------------------------------------------------------------------
+# Copyright (C) 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.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+def url_path_join(*pieces):
+ """Join components of url into a relative url
+
+ Use to prevent double slash when joining subpath. This will leave the
+ initial and final / in place
+ """
+ initial = pieces[0].startswith('/')
+ final = pieces[-1].endswith('/')
+ striped = [s.strip('/') for s in pieces]
+ result = '/'.join(s for s in striped if s)
+ if initial: result = '/' + result
+ if final: result = result + '/'
+ return result