From f9727e6899d9efe1a23919be3be80fbf07538ed4 2013-05-15 16:28:21 From: Brian E. Granger Date: 2013-05-15 16:28:21 Subject: [PATCH] Splitting handlers into different files. I have also created a top-level utils.py for notebook related stuff --- 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