diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 1530aae..6f953fc 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -16,6 +16,9 @@ Authors: # Imports #----------------------------------------------------------------------------- +import logging +import Cookie + from tornado import web from tornado import websocket @@ -35,21 +38,46 @@ except ImportError: # Top-level handlers #----------------------------------------------------------------------------- - -class NBBrowserHandler(web.RequestHandler): +class AuthenticatedHandler(web.RequestHandler): + """A RequestHandler with an authenticated user.""" + def get_current_user(self): + password = self.get_secure_cookie("password") + if password is None: + # cookie doesn't exist, or is invalid. Clear to prevent repeated + # 'Invalid cookie signature' warnings. + self.clear_cookie('password') + self.clear_cookie("user_id") + if self.application.password and self.application.password != password: + return None + return self.get_secure_cookie("user") or 'anonymous' + +class NBBrowserHandler(AuthenticatedHandler): + @web.authenticated def get(self): nbm = self.application.notebook_manager project = nbm.notebook_dir self.render('nbbrowser.html', project=project) +class LoginHandler(AuthenticatedHandler): + def get(self): + user_id = self.get_secure_cookie("user") or '' + self.render('login.html', user_id=user_id) + + def post(self): + self.set_secure_cookie("user", self.get_argument("name", default=u'')) + self.set_secure_cookie("password", self.get_argument("password", default=u'')) + url = self.get_argument("next", default="/") + self.redirect(url) -class NewHandler(web.RequestHandler): +class NewHandler(AuthenticatedHandler): + @web.authenticated def get(self): notebook_id = self.application.notebook_manager.new_notebook() self.render('notebook.html', notebook_id=notebook_id) -class NamedNotebookHandler(web.RequestHandler): +class NamedNotebookHandler(AuthenticatedHandler): + @web.authenticated def get(self, notebook_id): nbm = self.application.notebook_manager if not nbm.notebook_exists(notebook_id): @@ -62,12 +90,14 @@ class NamedNotebookHandler(web.RequestHandler): #----------------------------------------------------------------------------- -class MainKernelHandler(web.RequestHandler): +class MainKernelHandler(AuthenticatedHandler): + @web.authenticated def get(self): km = self.application.kernel_manager self.finish(jsonapi.dumps(km.kernel_ids)) + @web.authenticated def post(self): km = self.application.kernel_manager notebook_id = self.get_argument('notebook', default=None) @@ -78,10 +108,11 @@ class MainKernelHandler(web.RequestHandler): self.finish(jsonapi.dumps(data)) -class KernelHandler(web.RequestHandler): +class KernelHandler(AuthenticatedHandler): SUPPORTED_METHODS = ('DELETE') + @web.authenticated def delete(self, kernel_id): km = self.application.kernel_manager km.kill_kernel(kernel_id) @@ -89,8 +120,9 @@ class KernelHandler(web.RequestHandler): self.finish() -class KernelActionHandler(web.RequestHandler): +class KernelActionHandler(AuthenticatedHandler): + @web.authenticated def post(self, kernel_id, action): km = self.application.kernel_manager if action == 'interrupt': @@ -136,20 +168,58 @@ class ZMQStreamHandler(websocket.WebSocketHandler): else: self.write_message(msg) - -class IOPubHandler(ZMQStreamHandler): +class AuthenticatedZMQStreamHandler(ZMQStreamHandler): + def open(self, kernel_id): + self.kernel_id = kernel_id + self.session = Session() + self.save_on_message = self.on_message + self.on_message = self.on_first_message + + def get_current_user(self): + password = self.get_secure_cookie("password") + if password is None: + # clear cookies, to prevent future Invalid cookie signature warnings + self._cookies = Cookie.SimpleCookie() + if self.application.password and self.application.password != password: + return None + return self.get_secure_cookie("user") or 'anonymous' + + def _inject_cookie_message(self, msg): + """Inject the first message, which is the document cookie, + for authentication.""" + if isinstance(msg, unicode): + # Cookie can't constructor doesn't accept unicode strings for some reason + msg = msg.encode('utf8', 'replace') + try: + self._cookies = Cookie.SimpleCookie(msg) + except: + logging.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: + logging.warn("Couldn't authenticate WebSocket connection") + raise web.HTTPError(403) + self.on_message = self.save_on_message + + +class IOPubHandler(AuthenticatedZMQStreamHandler): def initialize(self, *args, **kwargs): self._kernel_alive = True self._beating = False self.iopub_stream = None self.hb_stream = None - - def open(self, kernel_id): + + def on_first_message(self, msg): + try: + super(IOPubHandler, self).on_first_message(msg) + except web.HTTPError: + self.close() + return km = self.application.kernel_manager - self.kernel_id = kernel_id - self.session = Session() self.time_to_dead = km.time_to_dead + kernel_id = self.kernel_id try: self.iopub_stream = km.create_iopub_stream(kernel_id) self.hb_stream = km.create_hb_stream(kernel_id) @@ -158,9 +228,13 @@ class IOPubHandler(ZMQStreamHandler): # close the connection. if not self.stream.closed(): self.stream.close() + self.close() else: self.iopub_stream.on_recv(self._on_zmq_reply) self.start_hb(self.kernel_died) + + def on_message(self, msg): + pass def on_close(self): # This method can be called twice, once by self.kernel_died and once @@ -216,15 +290,20 @@ class IOPubHandler(ZMQStreamHandler): self.on_close() -class ShellHandler(ZMQStreamHandler): +class ShellHandler(AuthenticatedZMQStreamHandler): def initialize(self, *args, **kwargs): self.shell_stream = None - def open(self, kernel_id): + def on_first_message(self, msg): + try: + super(ShellHandler, self).on_first_message(msg) + except web.HTTPError: + self.close() + return km = self.application.kernel_manager self.max_msg_size = km.max_msg_size - self.kernel_id = kernel_id + kernel_id = self.kernel_id try: self.shell_stream = km.create_shell_stream(kernel_id) except web.HTTPError: @@ -232,8 +311,8 @@ class ShellHandler(ZMQStreamHandler): # close the connection. if not self.stream.closed(): self.stream.close() + self.close() else: - self.session = Session() self.shell_stream.on_recv(self._on_zmq_reply) def on_message(self, msg): @@ -251,13 +330,15 @@ class ShellHandler(ZMQStreamHandler): # Notebook web service handlers #----------------------------------------------------------------------------- -class NotebookRootHandler(web.RequestHandler): +class NotebookRootHandler(AuthenticatedHandler): + @web.authenticated def get(self): nbm = self.application.notebook_manager files = nbm.list_notebooks() self.finish(jsonapi.dumps(files)) + @web.authenticated def post(self): nbm = self.application.notebook_manager body = self.request.body.strip() @@ -271,10 +352,11 @@ class NotebookRootHandler(web.RequestHandler): self.finish(jsonapi.dumps(notebook_id)) -class NotebookHandler(web.RequestHandler): +class NotebookHandler(AuthenticatedHandler): SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') + @web.authenticated def get(self, notebook_id): nbm = self.application.notebook_manager format = self.get_argument('format', default='json') @@ -288,6 +370,7 @@ class NotebookHandler(web.RequestHandler): self.set_header('Last-Modified', last_mod) self.finish(data) + @web.authenticated def put(self, notebook_id): nbm = self.application.notebook_manager format = self.get_argument('format', default='json') @@ -296,6 +379,7 @@ class NotebookHandler(web.RequestHandler): self.set_status(204) self.finish() + @web.authenticated def delete(self, notebook_id): nbm = self.application.notebook_manager nbm.delete_notebook(notebook_id) @@ -307,8 +391,9 @@ class NotebookHandler(web.RequestHandler): #----------------------------------------------------------------------------- -class RSTHandler(web.RequestHandler): +class RSTHandler(AuthenticatedHandler): + @web.authenticated def post(self): if publish_string is None: raise web.HTTPError(503, u'docutils not available') diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 5821481..067f0db 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -35,7 +35,7 @@ from tornado import httpserver from tornado import web from .kernelmanager import MappingKernelManager -from .handlers import ( +from .handlers import (LoginHandler, NBBrowserHandler, NewHandler, NamedNotebookHandler, MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler, ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler @@ -80,6 +80,7 @@ class NotebookWebApplication(web.Application): def __init__(self, ipython_app, kernel_manager, notebook_manager, log): handlers = [ (r"/", NBBrowserHandler), + (r"/login", LoginHandler), (r"/new", NewHandler), (r"/%s" % _notebook_id_regex, NamedNotebookHandler), (r"/kernels", MainKernelHandler), @@ -94,6 +95,8 @@ class NotebookWebApplication(web.Application): settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), + cookie_secret=os.urandom(1024), + login_url="/login", ) web.Application.__init__(self, handlers, **settings) @@ -122,7 +125,7 @@ aliases.update({ 'keyfile': 'IPythonNotebookApp.keyfile', 'certfile': 'IPythonNotebookApp.certfile', 'ws-hostname': 'IPythonNotebookApp.ws_hostname', - 'notebook-dir': 'NotebookManager.notebook_dir' + 'notebook-dir': 'NotebookManager.notebook_dir', }) notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'ws-hostname', @@ -185,6 +188,10 @@ class IPythonNotebookApp(BaseIPythonApplication): help="""The full path to a private key file for usage with SSL/TLS.""" ) + password = Unicode(u'', config=True, + help="""Password to use for web authentication""" + ) + def get_ws_url(self): """Return the WebSocket URL for this server.""" if self.certfile: @@ -241,6 +248,7 @@ class IPythonNotebookApp(BaseIPythonApplication): ssl_options['keyfile'] = self.keyfile else: ssl_options = None + self.web_app.password = self.password self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options) if ssl_options is None and not self.ip: self.log.critical('WARNING: the notebook server is listening on all IP addresses ' diff --git a/IPython/frontend/html/notebook/static/js/kernel.js b/IPython/frontend/html/notebook/static/js/kernel.js index ef7ea0c..137d59c 100644 --- a/IPython/frontend/html/notebook/static/js/kernel.js +++ b/IPython/frontend/html/notebook/static/js/kernel.js @@ -95,6 +95,12 @@ var IPython = (function (IPython) { console.log("Starting WS:", ws_url); this.shell_channel = new this.WebSocket(ws_url + "/shell"); this.iopub_channel = new this.WebSocket(ws_url + "/iopub"); + send_cookie = function(){ + this.send(document.cookie); + console.log(this); + } + this.shell_channel.onopen = send_cookie; + this.iopub_channel.onopen = send_cookie; }; diff --git a/IPython/frontend/html/notebook/templates/login.html b/IPython/frontend/html/notebook/templates/login.html new file mode 100644 index 0000000..855db58 --- /dev/null +++ b/IPython/frontend/html/notebook/templates/login.html @@ -0,0 +1,66 @@ + + + + + + + IPython Notebook + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+ +
+
+ Name: + Password: + +
+
+
+
+ +
+ +
+ + + + + + + + + + + +