Merge branch 'enh/httpauth' of https://github.com/satra/ipython into satra-enh/httpauth

pull/37/head
Brian E. Granger 15 years ago
commit 98393e9a28

@ -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')

@ -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 '

@ -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;
};

@ -0,0 +1,66 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>IPython Notebook</title>
<link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
<!-- <link rel="stylesheet" href="static/jquery/css/themes/rocket/jquery-wijmo.css" type="text/css" /> -->
<!-- <link rel="stylesheet" href="static/jquery/css/themes/smoothness/jquery-ui-1.8.14.custom.css" type="text/css" />-->
<link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
<link rel="stylesheet" href="static/css/layout.css" type="text/css" />
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
<script type="text/javascript" charset="utf-8">
function add_next_to_action(){
// add 'next' argument to action url, to preserve redirect
var query = location.search.substring(1);
var form = document.forms[0];
var action = form.getAttribute("action");
form.setAttribute("action", action + '?' + query);
}
</script>
</head>
<body onload="add_next_to_action()">
<div id="header">
<span id="ipython_notebook"><h1>IPython Notebook</h1></span>
</div>
<div id="header_border"></div>
<div id="main_app">
<div id="app_hbox">
<div id="left_panel">
</div>
<div id="content_panel">
<form action="/login" method="post">
Name: <input type="text" name="name" value="{{user_id}}">
Password: <input type="password" name="password">
<input type="submit" value="Sign in">
</form>
</div>
<div id="right_panel">
</div>
</div>
</div>
<script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/nbbrowser_main.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
Loading…
Cancel
Save