##// END OF EJS Templates
Renaming user cookie to username to better match usage.
MinRK -
Show More
@@ -1,448 +1,448 b''
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import logging
20 20 import Cookie
21 21 import uuid
22 22
23 23 from tornado import web
24 24 from tornado import websocket
25 25
26 26 from zmq.eventloop import ioloop
27 27 from zmq.utils import jsonapi
28 28
29 29 from IPython.zmq.session import Session
30 30
31 31 try:
32 32 from docutils.core import publish_string
33 33 except ImportError:
34 34 publish_string = None
35 35
36 36
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Top-level handlers
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class AuthenticatedHandler(web.RequestHandler):
43 43 """A RequestHandler with an authenticated user."""
44 44
45 45 def get_current_user(self):
46 user_id = self.get_secure_cookie("user")
46 user_id = self.get_secure_cookie("username")
47 47 # For now the user_id should not return empty, but it could eventually
48 48 if user_id == '':
49 49 user_id = 'anonymous'
50 50 if user_id is None:
51 51 # prevent extra Invalid cookie sig warnings:
52 self.clear_cookie('user')
52 self.clear_cookie('username')
53 53 if not self.application.password:
54 54 user_id = 'anonymous'
55 55 return user_id
56 56
57 57
58 58 class ProjectDashboardHandler(AuthenticatedHandler):
59 59
60 60 @web.authenticated
61 61 def get(self):
62 62 nbm = self.application.notebook_manager
63 63 project = nbm.notebook_dir
64 64 self.render(
65 65 'projectdashboard.html', project=project,
66 66 base_project_url=u'/', base_kernel_url=u'/'
67 67 )
68 68
69 69
70 70 class LoginHandler(AuthenticatedHandler):
71 71
72 72 def get(self):
73 73 self.render('login.html', next='/')
74 74
75 75 def post(self):
76 76 pwd = self.get_argument('password', default=u'')
77 77 if self.application.password and pwd == self.application.password:
78 self.set_secure_cookie('user', str(uuid.uuid4()))
78 self.set_secure_cookie('username', str(uuid.uuid4()))
79 79 url = self.get_argument('next', default='/')
80 80 self.redirect(url)
81 81
82 82
83 83 class NewHandler(AuthenticatedHandler):
84 84
85 85 @web.authenticated
86 86 def get(self):
87 87 nbm = self.application.notebook_manager
88 88 project = nbm.notebook_dir
89 89 notebook_id = nbm.new_notebook()
90 90 self.render(
91 91 'notebook.html', project=project,
92 92 notebook_id=notebook_id,
93 93 base_project_url=u'/', base_kernel_url=u'/'
94 94 )
95 95
96 96
97 97 class NamedNotebookHandler(AuthenticatedHandler):
98 98
99 99 @web.authenticated
100 100 def get(self, notebook_id):
101 101 nbm = self.application.notebook_manager
102 102 project = nbm.notebook_dir
103 103 if not nbm.notebook_exists(notebook_id):
104 104 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
105 105 self.render(
106 106 'notebook.html', project=project,
107 107 notebook_id=notebook_id,
108 108 base_project_url=u'/', base_kernel_url=u'/'
109 109 )
110 110
111 111
112 112 #-----------------------------------------------------------------------------
113 113 # Kernel handlers
114 114 #-----------------------------------------------------------------------------
115 115
116 116
117 117 class MainKernelHandler(AuthenticatedHandler):
118 118
119 119 @web.authenticated
120 120 def get(self):
121 121 km = self.application.kernel_manager
122 122 self.finish(jsonapi.dumps(km.kernel_ids))
123 123
124 124 @web.authenticated
125 125 def post(self):
126 126 km = self.application.kernel_manager
127 127 notebook_id = self.get_argument('notebook', default=None)
128 128 kernel_id = km.start_kernel(notebook_id)
129 129 ws_url = self.application.ipython_app.get_ws_url()
130 130 data = {'ws_url':ws_url,'kernel_id':kernel_id}
131 131 self.set_header('Location', '/'+kernel_id)
132 132 self.finish(jsonapi.dumps(data))
133 133
134 134
135 135 class KernelHandler(AuthenticatedHandler):
136 136
137 137 SUPPORTED_METHODS = ('DELETE')
138 138
139 139 @web.authenticated
140 140 def delete(self, kernel_id):
141 141 km = self.application.kernel_manager
142 142 km.kill_kernel(kernel_id)
143 143 self.set_status(204)
144 144 self.finish()
145 145
146 146
147 147 class KernelActionHandler(AuthenticatedHandler):
148 148
149 149 @web.authenticated
150 150 def post(self, kernel_id, action):
151 151 km = self.application.kernel_manager
152 152 if action == 'interrupt':
153 153 km.interrupt_kernel(kernel_id)
154 154 self.set_status(204)
155 155 if action == 'restart':
156 156 new_kernel_id = km.restart_kernel(kernel_id)
157 157 ws_url = self.application.ipython_app.get_ws_url()
158 158 data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
159 159 self.set_header('Location', '/'+new_kernel_id)
160 160 self.write(jsonapi.dumps(data))
161 161 self.finish()
162 162
163 163
164 164 class ZMQStreamHandler(websocket.WebSocketHandler):
165 165
166 166 def _reserialize_reply(self, msg_list):
167 167 """Reserialize a reply message using JSON.
168 168
169 169 This takes the msg list from the ZMQ socket, unserializes it using
170 170 self.session and then serializes the result using JSON. This method
171 171 should be used by self._on_zmq_reply to build messages that can
172 172 be sent back to the browser.
173 173 """
174 174 idents, msg_list = self.session.feed_identities(msg_list)
175 175 msg = self.session.unserialize(msg_list)
176 176 try:
177 177 msg['header'].pop('date')
178 178 except KeyError:
179 179 pass
180 180 try:
181 181 msg['parent_header'].pop('date')
182 182 except KeyError:
183 183 pass
184 184 msg.pop('buffers')
185 185 return jsonapi.dumps(msg)
186 186
187 187 def _on_zmq_reply(self, msg_list):
188 188 try:
189 189 msg = self._reserialize_reply(msg_list)
190 190 except:
191 191 self.application.log.critical("Malformed message: %r" % msg_list)
192 192 else:
193 193 self.write_message(msg)
194 194
195 195
196 196 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
197 197
198 198 def open(self, kernel_id):
199 199 self.kernel_id = kernel_id.decode('ascii')
200 200 try:
201 201 cfg = self.application.ipython_app.config
202 202 except AttributeError:
203 203 # protect from the case where this is run from something other than
204 204 # the notebook app:
205 205 cfg = None
206 206 self.session = Session(config=cfg)
207 207 self.save_on_message = self.on_message
208 208 self.on_message = self.on_first_message
209 209
210 210 def get_current_user(self):
211 user_id = self.get_secure_cookie("user")
211 user_id = self.get_secure_cookie("username")
212 212 if user_id == '' or (user_id is None and not self.application.password):
213 213 user_id = 'anonymous'
214 214 return user_id
215 215
216 216 def _inject_cookie_message(self, msg):
217 217 """Inject the first message, which is the document cookie,
218 218 for authentication."""
219 219 if isinstance(msg, unicode):
220 220 # Cookie can't constructor doesn't accept unicode strings for some reason
221 221 msg = msg.encode('utf8', 'replace')
222 222 try:
223 223 self._cookies = Cookie.SimpleCookie(msg)
224 224 except:
225 225 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
226 226
227 227 def on_first_message(self, msg):
228 228 self._inject_cookie_message(msg)
229 229 if self.get_current_user() is None:
230 230 logging.warn("Couldn't authenticate WebSocket connection")
231 231 raise web.HTTPError(403)
232 232 self.on_message = self.save_on_message
233 233
234 234
235 235 class IOPubHandler(AuthenticatedZMQStreamHandler):
236 236
237 237 def initialize(self, *args, **kwargs):
238 238 self._kernel_alive = True
239 239 self._beating = False
240 240 self.iopub_stream = None
241 241 self.hb_stream = None
242 242
243 243 def on_first_message(self, msg):
244 244 try:
245 245 super(IOPubHandler, self).on_first_message(msg)
246 246 except web.HTTPError:
247 247 self.close()
248 248 return
249 249 km = self.application.kernel_manager
250 250 self.time_to_dead = km.time_to_dead
251 251 kernel_id = self.kernel_id
252 252 try:
253 253 self.iopub_stream = km.create_iopub_stream(kernel_id)
254 254 self.hb_stream = km.create_hb_stream(kernel_id)
255 255 except web.HTTPError:
256 256 # WebSockets don't response to traditional error codes so we
257 257 # close the connection.
258 258 if not self.stream.closed():
259 259 self.stream.close()
260 260 self.close()
261 261 else:
262 262 self.iopub_stream.on_recv(self._on_zmq_reply)
263 263 self.start_hb(self.kernel_died)
264 264
265 265 def on_message(self, msg):
266 266 pass
267 267
268 268 def on_close(self):
269 269 # This method can be called twice, once by self.kernel_died and once
270 270 # from the WebSocket close event. If the WebSocket connection is
271 271 # closed before the ZMQ streams are setup, they could be None.
272 272 self.stop_hb()
273 273 if self.iopub_stream is not None and not self.iopub_stream.closed():
274 274 self.iopub_stream.on_recv(None)
275 275 self.iopub_stream.close()
276 276 if self.hb_stream is not None and not self.hb_stream.closed():
277 277 self.hb_stream.close()
278 278
279 279 def start_hb(self, callback):
280 280 """Start the heartbeating and call the callback if the kernel dies."""
281 281 if not self._beating:
282 282 self._kernel_alive = True
283 283
284 284 def ping_or_dead():
285 285 if self._kernel_alive:
286 286 self._kernel_alive = False
287 287 self.hb_stream.send(b'ping')
288 288 else:
289 289 try:
290 290 callback()
291 291 except:
292 292 pass
293 293 finally:
294 294 self._hb_periodic_callback.stop()
295 295
296 296 def beat_received(msg):
297 297 self._kernel_alive = True
298 298
299 299 self.hb_stream.on_recv(beat_received)
300 300 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
301 301 self._hb_periodic_callback.start()
302 302 self._beating= True
303 303
304 304 def stop_hb(self):
305 305 """Stop the heartbeating and cancel all related callbacks."""
306 306 if self._beating:
307 307 self._hb_periodic_callback.stop()
308 308 if not self.hb_stream.closed():
309 309 self.hb_stream.on_recv(None)
310 310
311 311 def kernel_died(self):
312 312 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
313 313 self.write_message(
314 314 {'header': {'msg_type': 'status'},
315 315 'parent_header': {},
316 316 'content': {'execution_state':'dead'}
317 317 }
318 318 )
319 319 self.on_close()
320 320
321 321
322 322 class ShellHandler(AuthenticatedZMQStreamHandler):
323 323
324 324 def initialize(self, *args, **kwargs):
325 325 self.shell_stream = None
326 326
327 327 def on_first_message(self, msg):
328 328 try:
329 329 super(ShellHandler, self).on_first_message(msg)
330 330 except web.HTTPError:
331 331 self.close()
332 332 return
333 333 km = self.application.kernel_manager
334 334 self.max_msg_size = km.max_msg_size
335 335 kernel_id = self.kernel_id
336 336 try:
337 337 self.shell_stream = km.create_shell_stream(kernel_id)
338 338 except web.HTTPError:
339 339 # WebSockets don't response to traditional error codes so we
340 340 # close the connection.
341 341 if not self.stream.closed():
342 342 self.stream.close()
343 343 self.close()
344 344 else:
345 345 self.shell_stream.on_recv(self._on_zmq_reply)
346 346
347 347 def on_message(self, msg):
348 348 if len(msg) < self.max_msg_size:
349 349 msg = jsonapi.loads(msg)
350 350 self.session.send(self.shell_stream, msg)
351 351
352 352 def on_close(self):
353 353 # Make sure the stream exists and is not already closed.
354 354 if self.shell_stream is not None and not self.shell_stream.closed():
355 355 self.shell_stream.close()
356 356
357 357
358 358 #-----------------------------------------------------------------------------
359 359 # Notebook web service handlers
360 360 #-----------------------------------------------------------------------------
361 361
362 362 class NotebookRootHandler(AuthenticatedHandler):
363 363
364 364 @web.authenticated
365 365 def get(self):
366 366 nbm = self.application.notebook_manager
367 367 files = nbm.list_notebooks()
368 368 self.finish(jsonapi.dumps(files))
369 369
370 370 @web.authenticated
371 371 def post(self):
372 372 nbm = self.application.notebook_manager
373 373 body = self.request.body.strip()
374 374 format = self.get_argument('format', default='json')
375 375 name = self.get_argument('name', default=None)
376 376 if body:
377 377 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
378 378 else:
379 379 notebook_id = nbm.new_notebook()
380 380 self.set_header('Location', '/'+notebook_id)
381 381 self.finish(jsonapi.dumps(notebook_id))
382 382
383 383
384 384 class NotebookHandler(AuthenticatedHandler):
385 385
386 386 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
387 387
388 388 @web.authenticated
389 389 def get(self, notebook_id):
390 390 nbm = self.application.notebook_manager
391 391 format = self.get_argument('format', default='json')
392 392 last_mod, name, data = nbm.get_notebook(notebook_id, format)
393 393 if format == u'json':
394 394 self.set_header('Content-Type', 'application/json')
395 395 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
396 396 elif format == u'py':
397 397 self.set_header('Content-Type', 'application/x-python')
398 398 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
399 399 self.set_header('Last-Modified', last_mod)
400 400 self.finish(data)
401 401
402 402 @web.authenticated
403 403 def put(self, notebook_id):
404 404 nbm = self.application.notebook_manager
405 405 format = self.get_argument('format', default='json')
406 406 name = self.get_argument('name', default=None)
407 407 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
408 408 self.set_status(204)
409 409 self.finish()
410 410
411 411 @web.authenticated
412 412 def delete(self, notebook_id):
413 413 nbm = self.application.notebook_manager
414 414 nbm.delete_notebook(notebook_id)
415 415 self.set_status(204)
416 416 self.finish()
417 417
418 418 #-----------------------------------------------------------------------------
419 419 # RST web service handlers
420 420 #-----------------------------------------------------------------------------
421 421
422 422
423 423 class RSTHandler(AuthenticatedHandler):
424 424
425 425 @web.authenticated
426 426 def post(self):
427 427 if publish_string is None:
428 428 raise web.HTTPError(503, u'docutils not available')
429 429 body = self.request.body.strip()
430 430 source = body
431 431 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
432 432 defaults = {'file_insertion_enabled': 0,
433 433 'raw_enabled': 0,
434 434 '_disable_config': 1,
435 435 'stylesheet_path': 0
436 436 # 'template': template_path
437 437 }
438 438 try:
439 439 html = publish_string(source, writer_name='html',
440 440 settings_overrides=defaults
441 441 )
442 442 except:
443 443 raise web.HTTPError(400, u'Invalid RST')
444 444 print html
445 445 self.set_header('Content-Type', 'text/html')
446 446 self.finish(html)
447 447
448 448
General Comments 0
You need to be logged in to leave comments. Login now