##// END OF EJS Templates
Merge branch 'enh/httpauth' of https://github.com/satra/ipython into satra-enh/httpauth
Brian E. Granger -
r4723:06032c55 merge
parent child Browse files
Show More
@@ -0,0 +1,66 b''
1 <!DOCTYPE HTML>
2 <html>
3
4 <head>
5 <meta charset="utf-8">
6
7 <title>IPython Notebook</title>
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <!-- <link rel="stylesheet" href="static/jquery/css/themes/rocket/jquery-wijmo.css" type="text/css" /> -->
11 <!-- <link rel="stylesheet" href="static/jquery/css/themes/smoothness/jquery-ui-1.8.14.custom.css" type="text/css" />-->
12
13 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
14 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
15 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
16 <script type="text/javascript" charset="utf-8">
17 function add_next_to_action(){
18 // add 'next' argument to action url, to preserve redirect
19 var query = location.search.substring(1);
20 var form = document.forms[0];
21 var action = form.getAttribute("action");
22 form.setAttribute("action", action + '?' + query);
23 }
24 </script>
25 </head>
26
27 <body onload="add_next_to_action()">
28
29 <div id="header">
30 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
31 </div>
32
33 <div id="header_border"></div>
34
35 <div id="main_app">
36
37 <div id="app_hbox">
38
39 <div id="left_panel">
40 </div>
41
42 <div id="content_panel">
43 <form action="/login" method="post">
44 Name: <input type="text" name="name" value="{{user_id}}">
45 Password: <input type="password" name="password">
46 <input type="submit" value="Sign in">
47 </form>
48 </div>
49 <div id="right_panel">
50 </div>
51
52 </div>
53
54 </div>
55
56 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
57 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
58 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
59 <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
60 <script src="static/js/nbbrowser_main.js" type="text/javascript" charset="utf-8"></script>
61
62 </body>
63
64 </html>
65
66
@@ -16,6 +16,9 b' Authors:'
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 import logging
20 import Cookie
21
19 22 from tornado import web
20 23 from tornado import websocket
21 24
@@ -35,21 +38,46 b' except ImportError:'
35 38 # Top-level handlers
36 39 #-----------------------------------------------------------------------------
37 40
38
39 class NBBrowserHandler(web.RequestHandler):
41 class AuthenticatedHandler(web.RequestHandler):
42 """A RequestHandler with an authenticated user."""
43 def get_current_user(self):
44 password = self.get_secure_cookie("password")
45 if password is None:
46 # cookie doesn't exist, or is invalid. Clear to prevent repeated
47 # 'Invalid cookie signature' warnings.
48 self.clear_cookie('password')
49 self.clear_cookie("user_id")
50 if self.application.password and self.application.password != password:
51 return None
52 return self.get_secure_cookie("user") or 'anonymous'
53
54 class NBBrowserHandler(AuthenticatedHandler):
55 @web.authenticated
40 56 def get(self):
41 57 nbm = self.application.notebook_manager
42 58 project = nbm.notebook_dir
43 59 self.render('nbbrowser.html', project=project)
44 60
61 class LoginHandler(AuthenticatedHandler):
62 def get(self):
63 user_id = self.get_secure_cookie("user") or ''
64 self.render('login.html', user_id=user_id)
65
66 def post(self):
67 self.set_secure_cookie("user", self.get_argument("name", default=u''))
68 self.set_secure_cookie("password", self.get_argument("password", default=u''))
69 url = self.get_argument("next", default="/")
70 self.redirect(url)
45 71
46 class NewHandler(web.RequestHandler):
72 class NewHandler(AuthenticatedHandler):
73 @web.authenticated
47 74 def get(self):
48 75 notebook_id = self.application.notebook_manager.new_notebook()
49 76 self.render('notebook.html', notebook_id=notebook_id)
50 77
51 78
52 class NamedNotebookHandler(web.RequestHandler):
79 class NamedNotebookHandler(AuthenticatedHandler):
80 @web.authenticated
53 81 def get(self, notebook_id):
54 82 nbm = self.application.notebook_manager
55 83 if not nbm.notebook_exists(notebook_id):
@@ -62,12 +90,14 b' class NamedNotebookHandler(web.RequestHandler):'
62 90 #-----------------------------------------------------------------------------
63 91
64 92
65 class MainKernelHandler(web.RequestHandler):
93 class MainKernelHandler(AuthenticatedHandler):
66 94
95 @web.authenticated
67 96 def get(self):
68 97 km = self.application.kernel_manager
69 98 self.finish(jsonapi.dumps(km.kernel_ids))
70 99
100 @web.authenticated
71 101 def post(self):
72 102 km = self.application.kernel_manager
73 103 notebook_id = self.get_argument('notebook', default=None)
@@ -78,10 +108,11 b' class MainKernelHandler(web.RequestHandler):'
78 108 self.finish(jsonapi.dumps(data))
79 109
80 110
81 class KernelHandler(web.RequestHandler):
111 class KernelHandler(AuthenticatedHandler):
82 112
83 113 SUPPORTED_METHODS = ('DELETE')
84 114
115 @web.authenticated
85 116 def delete(self, kernel_id):
86 117 km = self.application.kernel_manager
87 118 km.kill_kernel(kernel_id)
@@ -89,8 +120,9 b' class KernelHandler(web.RequestHandler):'
89 120 self.finish()
90 121
91 122
92 class KernelActionHandler(web.RequestHandler):
123 class KernelActionHandler(AuthenticatedHandler):
93 124
125 @web.authenticated
94 126 def post(self, kernel_id, action):
95 127 km = self.application.kernel_manager
96 128 if action == 'interrupt':
@@ -136,20 +168,58 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
136 168 else:
137 169 self.write_message(msg)
138 170
139
140 class IOPubHandler(ZMQStreamHandler):
171 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
172 def open(self, kernel_id):
173 self.kernel_id = kernel_id
174 self.session = Session()
175 self.save_on_message = self.on_message
176 self.on_message = self.on_first_message
177
178 def get_current_user(self):
179 password = self.get_secure_cookie("password")
180 if password is None:
181 # clear cookies, to prevent future Invalid cookie signature warnings
182 self._cookies = Cookie.SimpleCookie()
183 if self.application.password and self.application.password != password:
184 return None
185 return self.get_secure_cookie("user") or 'anonymous'
186
187 def _inject_cookie_message(self, msg):
188 """Inject the first message, which is the document cookie,
189 for authentication."""
190 if isinstance(msg, unicode):
191 # Cookie can't constructor doesn't accept unicode strings for some reason
192 msg = msg.encode('utf8', 'replace')
193 try:
194 self._cookies = Cookie.SimpleCookie(msg)
195 except:
196 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
197
198 def on_first_message(self, msg):
199 self._inject_cookie_message(msg)
200 if self.get_current_user() is None:
201 logging.warn("Couldn't authenticate WebSocket connection")
202 raise web.HTTPError(403)
203 self.on_message = self.save_on_message
204
205
206 class IOPubHandler(AuthenticatedZMQStreamHandler):
141 207
142 208 def initialize(self, *args, **kwargs):
143 209 self._kernel_alive = True
144 210 self._beating = False
145 211 self.iopub_stream = None
146 212 self.hb_stream = None
147
148 def open(self, kernel_id):
213
214 def on_first_message(self, msg):
215 try:
216 super(IOPubHandler, self).on_first_message(msg)
217 except web.HTTPError:
218 self.close()
219 return
149 220 km = self.application.kernel_manager
150 self.kernel_id = kernel_id
151 self.session = Session()
152 221 self.time_to_dead = km.time_to_dead
222 kernel_id = self.kernel_id
153 223 try:
154 224 self.iopub_stream = km.create_iopub_stream(kernel_id)
155 225 self.hb_stream = km.create_hb_stream(kernel_id)
@@ -158,9 +228,13 b' class IOPubHandler(ZMQStreamHandler):'
158 228 # close the connection.
159 229 if not self.stream.closed():
160 230 self.stream.close()
231 self.close()
161 232 else:
162 233 self.iopub_stream.on_recv(self._on_zmq_reply)
163 234 self.start_hb(self.kernel_died)
235
236 def on_message(self, msg):
237 pass
164 238
165 239 def on_close(self):
166 240 # This method can be called twice, once by self.kernel_died and once
@@ -216,15 +290,20 b' class IOPubHandler(ZMQStreamHandler):'
216 290 self.on_close()
217 291
218 292
219 class ShellHandler(ZMQStreamHandler):
293 class ShellHandler(AuthenticatedZMQStreamHandler):
220 294
221 295 def initialize(self, *args, **kwargs):
222 296 self.shell_stream = None
223 297
224 def open(self, kernel_id):
298 def on_first_message(self, msg):
299 try:
300 super(ShellHandler, self).on_first_message(msg)
301 except web.HTTPError:
302 self.close()
303 return
225 304 km = self.application.kernel_manager
226 305 self.max_msg_size = km.max_msg_size
227 self.kernel_id = kernel_id
306 kernel_id = self.kernel_id
228 307 try:
229 308 self.shell_stream = km.create_shell_stream(kernel_id)
230 309 except web.HTTPError:
@@ -232,8 +311,8 b' class ShellHandler(ZMQStreamHandler):'
232 311 # close the connection.
233 312 if not self.stream.closed():
234 313 self.stream.close()
314 self.close()
235 315 else:
236 self.session = Session()
237 316 self.shell_stream.on_recv(self._on_zmq_reply)
238 317
239 318 def on_message(self, msg):
@@ -251,13 +330,15 b' class ShellHandler(ZMQStreamHandler):'
251 330 # Notebook web service handlers
252 331 #-----------------------------------------------------------------------------
253 332
254 class NotebookRootHandler(web.RequestHandler):
333 class NotebookRootHandler(AuthenticatedHandler):
255 334
335 @web.authenticated
256 336 def get(self):
257 337 nbm = self.application.notebook_manager
258 338 files = nbm.list_notebooks()
259 339 self.finish(jsonapi.dumps(files))
260 340
341 @web.authenticated
261 342 def post(self):
262 343 nbm = self.application.notebook_manager
263 344 body = self.request.body.strip()
@@ -271,10 +352,11 b' class NotebookRootHandler(web.RequestHandler):'
271 352 self.finish(jsonapi.dumps(notebook_id))
272 353
273 354
274 class NotebookHandler(web.RequestHandler):
355 class NotebookHandler(AuthenticatedHandler):
275 356
276 357 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
277 358
359 @web.authenticated
278 360 def get(self, notebook_id):
279 361 nbm = self.application.notebook_manager
280 362 format = self.get_argument('format', default='json')
@@ -288,6 +370,7 b' class NotebookHandler(web.RequestHandler):'
288 370 self.set_header('Last-Modified', last_mod)
289 371 self.finish(data)
290 372
373 @web.authenticated
291 374 def put(self, notebook_id):
292 375 nbm = self.application.notebook_manager
293 376 format = self.get_argument('format', default='json')
@@ -296,6 +379,7 b' class NotebookHandler(web.RequestHandler):'
296 379 self.set_status(204)
297 380 self.finish()
298 381
382 @web.authenticated
299 383 def delete(self, notebook_id):
300 384 nbm = self.application.notebook_manager
301 385 nbm.delete_notebook(notebook_id)
@@ -307,8 +391,9 b' class NotebookHandler(web.RequestHandler):'
307 391 #-----------------------------------------------------------------------------
308 392
309 393
310 class RSTHandler(web.RequestHandler):
394 class RSTHandler(AuthenticatedHandler):
311 395
396 @web.authenticated
312 397 def post(self):
313 398 if publish_string is None:
314 399 raise web.HTTPError(503, u'docutils not available')
@@ -35,7 +35,7 b' from tornado import httpserver'
35 35 from tornado import web
36 36
37 37 from .kernelmanager import MappingKernelManager
38 from .handlers import (
38 from .handlers import (LoginHandler,
39 39 NBBrowserHandler, NewHandler, NamedNotebookHandler,
40 40 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
41 41 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
@@ -80,6 +80,7 b' class NotebookWebApplication(web.Application):'
80 80 def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
81 81 handlers = [
82 82 (r"/", NBBrowserHandler),
83 (r"/login", LoginHandler),
83 84 (r"/new", NewHandler),
84 85 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
85 86 (r"/kernels", MainKernelHandler),
@@ -94,6 +95,8 b' class NotebookWebApplication(web.Application):'
94 95 settings = dict(
95 96 template_path=os.path.join(os.path.dirname(__file__), "templates"),
96 97 static_path=os.path.join(os.path.dirname(__file__), "static"),
98 cookie_secret=os.urandom(1024),
99 login_url="/login",
97 100 )
98 101 web.Application.__init__(self, handlers, **settings)
99 102
@@ -122,7 +125,7 b' aliases.update({'
122 125 'keyfile': 'IPythonNotebookApp.keyfile',
123 126 'certfile': 'IPythonNotebookApp.certfile',
124 127 'ws-hostname': 'IPythonNotebookApp.ws_hostname',
125 'notebook-dir': 'NotebookManager.notebook_dir'
128 'notebook-dir': 'NotebookManager.notebook_dir',
126 129 })
127 130
128 131 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'ws-hostname',
@@ -185,6 +188,10 b' class IPythonNotebookApp(BaseIPythonApplication):'
185 188 help="""The full path to a private key file for usage with SSL/TLS."""
186 189 )
187 190
191 password = Unicode(u'', config=True,
192 help="""Password to use for web authentication"""
193 )
194
188 195 def get_ws_url(self):
189 196 """Return the WebSocket URL for this server."""
190 197 if self.certfile:
@@ -241,6 +248,7 b' class IPythonNotebookApp(BaseIPythonApplication):'
241 248 ssl_options['keyfile'] = self.keyfile
242 249 else:
243 250 ssl_options = None
251 self.web_app.password = self.password
244 252 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
245 253 if ssl_options is None and not self.ip:
246 254 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
@@ -95,6 +95,12 b' var IPython = (function (IPython) {'
95 95 console.log("Starting WS:", ws_url);
96 96 this.shell_channel = new this.WebSocket(ws_url + "/shell");
97 97 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
98 send_cookie = function(){
99 this.send(document.cookie);
100 console.log(this);
101 }
102 this.shell_channel.onopen = send_cookie;
103 this.iopub_channel.onopen = send_cookie;
98 104 };
99 105
100 106
General Comments 0
You need to be logged in to leave comments. Login now