diff --git a/notebook/services/kernels/handlers.py b/notebook/services/kernels/handlers.py index ef90bf9122..cfef2a4a0e 100644 --- a/notebook/services/kernels/handlers.py +++ b/notebook/services/kernels/handlers.py @@ -306,8 +306,13 @@ def on_message(self, msg): if channel not in self.channels: self.log.warning("No such channel: %r", channel) return - stream = self.channels[channel] - self.session.send(stream, msg) + am = self.kernel_manager.allowed_message_types + mt = msg['header']['msg_type'] + if am and mt not in am: + self.log.warning('Received message of type "%s", which is not allowed. Ignoring.' % mt) + else: + stream = self.channels[channel] + self.session.send(stream, msg) def _on_zmq_reply(self, stream, msg_list): idents, fed_msg_list = self.session.feed_identities(msg_list) diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 6d2158ef8a..02031d7e51 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -39,7 +39,7 @@ def _default_kernel_manager_class(self): kernel_argv = List(Unicode()) root_dir = Unicode(config=True) - + _kernel_connections = Dict() _culler_callback = None @@ -95,15 +95,15 @@ def _update_root_dir(self, proposal): no frontends are connected. """ ) - + kernel_info_timeout = Float(60, config=True, help="""Timeout for giving up on a kernel (in seconds). On starting and restarting kernels, we check whether the kernel is running and responsive by sending kernel_info_requests. This sets the timeout in seconds for how long the kernel can take - before being presumed dead. - This affects the MappingKernelManager (which handles kernel restarts) + before being presumed dead. + This affects the MappingKernelManager (which handles kernel restarts) and the ZMQChannelsHandler (which handles the startup). """ ) @@ -120,6 +120,12 @@ def __init__(self, **kwargs): super(MappingKernelManager, self).__init__(**kwargs) self.last_kernel_activity = utcnow() + allowed_message_types = List(trait=Unicode(), config=True, + help="""White list of allowed kernel message types. + When the list is empty, all message types are allowed. + """ + ) + #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -304,32 +310,32 @@ def restart_kernel(self, kernel_id): # return a Future that will resolve when the kernel has successfully restarted channel = kernel.connect_shell() future = Future() - + def finish(): """Common cleanup when restart finishes/fails for any reason.""" if not channel.closed(): channel.close() loop.remove_timeout(timeout) kernel.remove_restart_callback(on_restart_failed, 'dead') - + def on_reply(msg): self.log.debug("Kernel info reply received: %s", kernel_id) finish() if not future.done(): future.set_result(msg) - + def on_timeout(): self.log.warning("Timeout waiting for kernel_info_reply: %s", kernel_id) finish() if not future.done(): future.set_exception(gen.TimeoutError("Timeout waiting for restart")) - + def on_restart_failed(): self.log.warning("Restarting kernel failed: %s", kernel_id) finish() if not future.done(): future.set_exception(RuntimeError("Restart failed")) - + kernel.add_restart_callback(on_restart_failed, 'dead') kernel.session.send(channel, "kernel_info_request") channel.on_recv(on_reply) @@ -383,7 +389,7 @@ def _check_kernel_id(self, kernel_id): def start_watching_activity(self, kernel_id): """Start watching IOPub messages on a kernel for activity. - + - update last_activity on every message - record execution_state from status messages """ diff --git a/notebook/services/kernels/tests/test_kernels_api.py b/notebook/services/kernels/tests/test_kernels_api.py index 27d19709ec..83bfb0c3e0 100644 --- a/notebook/services/kernels/tests/test_kernels_api.py +++ b/notebook/services/kernels/tests/test_kernels_api.py @@ -3,6 +3,8 @@ import json import time +from traitlets.config import Config + from tornado.httpclient import HTTPRequest from tornado.ioloop import IOLoop from tornado.websocket import websocket_connect @@ -12,6 +14,7 @@ from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error + class KernelAPI(object): """Wrapper for kernel REST API requests""" def __init__(self, request, base_url, headers): @@ -183,3 +186,19 @@ def test_connections(self): break model = self.kern_api.get(kid).json() self.assertEqual(model['connections'], 0) + + +class KernelFilterTest(NotebookTestBase): + + # A special install of NotebookTestBase where only `kernel_info_request` + # messages are allowed. + config = Config({ + 'NotebookApp': { + 'MappingKernelManager': { + 'allowed_message_types': ['kernel_info_request'] + } + } + }) + # Sanity check verifying that the configurable was properly set. + def test_config(self): + self.assertEqual(self.notebook.kernel_manager.allowed_message_types, ['kernel_info_request']) diff --git a/notebook/tree/tests/handlers.py b/notebook/tree/tests/handlers.py new file mode 100644 index 0000000000..ef42527616 --- /dev/null +++ b/notebook/tree/tests/handlers.py @@ -0,0 +1,77 @@ +"""Tornado handlers for the tree view.""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import web +import os +from ..base.handlers import IPythonHandler, path_regex +from ..utils import url_path_join, url_escape + + +class TreeHandler(IPythonHandler): + """Render the tree view, listing notebooks, etc.""" + + def generate_breadcrumbs(self, path): + breadcrumbs = [(url_path_join(self.base_url, 'tree'), '')] + parts = path.split('/') + for i in range(len(parts)): + if parts[i]: + link = url_path_join(self.base_url, 'tree', + url_escape(url_path_join(*parts[:i+1])), + ) + breadcrumbs.append((link, parts[i])) + return breadcrumbs + + def generate_page_title(self, path): + comps = path.split('/') + if len(comps) > 3: + for i in range(len(comps)-2): + comps.pop(0) + page_title = url_path_join(*comps) + if page_title: + return page_title+'/' + else: + return 'Home' + + @web.authenticated + def get(self, path=''): + path = path.strip('/') + cm = self.contents_manager + + if cm.dir_exists(path=path): + if cm.is_hidden(path) and not cm.allow_hidden: + self.log.info("Refusing to serve hidden directory, via 404 Error") + raise web.HTTPError(404) + breadcrumbs = self.generate_breadcrumbs(path) + page_title = self.generate_page_title(path) + self.write(self.render_template('tree.html', + page_title=page_title, + notebook_path=path, + breadcrumbs=breadcrumbs, + terminals_available=self.settings['terminals_available'], + server_root=self.settings['server_root_dir'], + )) + elif cm.file_exists(path): + # it's not a directory, we have redirecting to do + model = cm.get(path, content=False) + # redirect to /api/notebooks if it's a notebook, otherwise /api/files + service = 'notebooks' if model['type'] == 'notebook' else 'files' + url = url_path_join( + self.base_url, service, url_escape(path), + ) + self.log.debug("Redirecting %s to %s", self.request.path, url) + self.redirect(url) + else: + raise web.HTTPError(404) + + +#----------------------------------------------------------------------------- +# URL to handler mappings +#----------------------------------------------------------------------------- + + +default_handlers = [ + (r"/tree%s" % path_regex, TreeHandler), + (r"/tree", TreeHandler), + ]