##// END OF EJS Templates
Notify user about invalid password.
Stefan van der Walt -
Show More
@@ -1,554 +1,562 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
19 import logging
20 import Cookie
20 import Cookie
21 import uuid
21 import uuid
22
22
23 from tornado import web
23 from tornado import web
24 from tornado import websocket
24 from tornado import websocket
25
25
26 from zmq.eventloop import ioloop
26 from zmq.eventloop import ioloop
27 from zmq.utils import jsonapi
27 from zmq.utils import jsonapi
28
28
29 from IPython.external.decorator import decorator
29 from IPython.external.decorator import decorator
30 from IPython.zmq.session import Session
30 from IPython.zmq.session import Session
31 from IPython.lib.security import passwd_check
31 from IPython.lib.security import passwd_check
32
32
33 try:
33 try:
34 from docutils.core import publish_string
34 from docutils.core import publish_string
35 except ImportError:
35 except ImportError:
36 publish_string = None
36 publish_string = None
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # parts tornado cares about haven't really changed, so it's OK to continue
43 # parts tornado cares about haven't really changed, so it's OK to continue
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # version as of Oct 30/2011) the version check fails, see the issue report:
45 # version as of Oct 30/2011) the version check fails, see the issue report:
46
46
47 # https://github.com/facebook/tornado/issues/385
47 # https://github.com/facebook/tornado/issues/385
48
48
49 # This issue has been fixed in Tornado post 2.1.1:
49 # This issue has been fixed in Tornado post 2.1.1:
50
50
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52
52
53 # Here we manually apply the same patch as above so that users of IPython can
53 # Here we manually apply the same patch as above so that users of IPython can
54 # continue to work with an officially released Tornado. We make the
54 # continue to work with an officially released Tornado. We make the
55 # monkeypatch version check as narrow as possible to limit its effects; once
55 # monkeypatch version check as narrow as possible to limit its effects; once
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57
57
58 import tornado
58 import tornado
59
59
60 if tornado.version_info <= (2,1,1):
60 if tornado.version_info <= (2,1,1):
61
61
62 def _execute(self, transforms, *args, **kwargs):
62 def _execute(self, transforms, *args, **kwargs):
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64
64
65 self.open_args = args
65 self.open_args = args
66 self.open_kwargs = kwargs
66 self.open_kwargs = kwargs
67
67
68 # The difference between version 8 and 13 is that in 8 the
68 # The difference between version 8 and 13 is that in 8 the
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # simply "Origin".
70 # simply "Origin".
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol8(self)
72 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection.accept_connection()
73 self.ws_connection.accept_connection()
74
74
75 elif self.request.headers.get("Sec-WebSocket-Version"):
75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 self.stream.write(tornado.escape.utf8(
76 self.stream.write(tornado.escape.utf8(
77 "HTTP/1.1 426 Upgrade Required\r\n"
77 "HTTP/1.1 426 Upgrade Required\r\n"
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 self.stream.close()
79 self.stream.close()
80
80
81 else:
81 else:
82 self.ws_connection = WebSocketProtocol76(self)
82 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
84
84
85 websocket.WebSocketHandler._execute = _execute
85 websocket.WebSocketHandler._execute = _execute
86 del _execute
86 del _execute
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Decorator for disabling read-only handlers
89 # Decorator for disabling read-only handlers
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 @decorator
92 @decorator
93 def not_if_readonly(f, self, *args, **kwargs):
93 def not_if_readonly(f, self, *args, **kwargs):
94 if self.application.read_only:
94 if self.application.read_only:
95 raise web.HTTPError(403, "Notebook server is read-only")
95 raise web.HTTPError(403, "Notebook server is read-only")
96 else:
96 else:
97 return f(self, *args, **kwargs)
97 return f(self, *args, **kwargs)
98
98
99 @decorator
99 @decorator
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 """authenticate this page *unless* readonly view is active.
101 """authenticate this page *unless* readonly view is active.
102
102
103 In read-only mode, the notebook list and print view should
103 In read-only mode, the notebook list and print view should
104 be accessible without authentication.
104 be accessible without authentication.
105 """
105 """
106
106
107 @web.authenticated
107 @web.authenticated
108 def auth_f(self, *args, **kwargs):
108 def auth_f(self, *args, **kwargs):
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110 if self.application.read_only:
110 if self.application.read_only:
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112 else:
112 else:
113 return auth_f(self, *args, **kwargs)
113 return auth_f(self, *args, **kwargs)
114
114
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116 # Top-level handlers
116 # Top-level handlers
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118
118
119 class AuthenticatedHandler(web.RequestHandler):
119 class AuthenticatedHandler(web.RequestHandler):
120 """A RequestHandler with an authenticated user."""
120 """A RequestHandler with an authenticated user."""
121
121
122 def get_current_user(self):
122 def get_current_user(self):
123 user_id = self.get_secure_cookie("username")
123 user_id = self.get_secure_cookie("username")
124 # For now the user_id should not return empty, but it could eventually
124 # For now the user_id should not return empty, but it could eventually
125 if user_id == '':
125 if user_id == '':
126 user_id = 'anonymous'
126 user_id = 'anonymous'
127 if user_id is None:
127 if user_id is None:
128 # prevent extra Invalid cookie sig warnings:
128 # prevent extra Invalid cookie sig warnings:
129 self.clear_cookie('username')
129 self.clear_cookie('username')
130 if not self.application.password and not self.application.read_only:
130 if not self.application.password and not self.application.read_only:
131 user_id = 'anonymous'
131 user_id = 'anonymous'
132 return user_id
132 return user_id
133
133
134 @property
134 @property
135 def read_only(self):
135 def read_only(self):
136 if self.application.read_only:
136 if self.application.read_only:
137 if self.application.password:
137 if self.application.password:
138 return self.get_current_user() is None
138 return self.get_current_user() is None
139 else:
139 else:
140 return True
140 return True
141 else:
141 else:
142 return False
142 return False
143
143
144 @property
144 @property
145 def ws_url(self):
145 def ws_url(self):
146 """websocket url matching the current request
146 """websocket url matching the current request
147
147
148 turns http[s]://host[:port] into
148 turns http[s]://host[:port] into
149 ws[s]://host[:port]
149 ws[s]://host[:port]
150 """
150 """
151 proto = self.request.protocol.replace('http', 'ws')
151 proto = self.request.protocol.replace('http', 'ws')
152 return "%s://%s" % (proto, self.request.host)
152 return "%s://%s" % (proto, self.request.host)
153
153
154
154
155 class ProjectDashboardHandler(AuthenticatedHandler):
155 class ProjectDashboardHandler(AuthenticatedHandler):
156
156
157 @authenticate_unless_readonly
157 @authenticate_unless_readonly
158 def get(self):
158 def get(self):
159 nbm = self.application.notebook_manager
159 nbm = self.application.notebook_manager
160 project = nbm.notebook_dir
160 project = nbm.notebook_dir
161 self.render(
161 self.render(
162 'projectdashboard.html', project=project,
162 'projectdashboard.html', project=project,
163 base_project_url=u'/', base_kernel_url=u'/',
163 base_project_url=u'/', base_kernel_url=u'/',
164 read_only=self.read_only,
164 read_only=self.read_only,
165 )
165 )
166
166
167
167
168 class LoginHandler(AuthenticatedHandler):
168 class LoginHandler(AuthenticatedHandler):
169
169
170 def get(self):
170 def _render(self, message=''):
171 self.render('login.html',
171 self.render('login.html',
172 next=self.get_argument('next', default='/'),
172 next=self.get_argument('next', default='/'),
173 read_only=self.read_only,
173 read_only=self.read_only,
174 message=message
174 )
175 )
175
176
177 def get(self):
178 self._render()
179
176 def post(self):
180 def post(self):
177 pwd = self.get_argument('password', default=u'')
181 pwd = self.get_argument('password', default=u'')
178 if self.application.password and \
182 if self.application.password:
179 passwd_check(self.application.password, pwd):
183 if passwd_check(self.application.password, pwd):
180 self.set_secure_cookie('username', str(uuid.uuid4()))
184 self.set_secure_cookie('username', str(uuid.uuid4()))
185 else:
186 self._render(message='Invalid password')
187 return
188
181 self.redirect(self.get_argument('next', default='/'))
189 self.redirect(self.get_argument('next', default='/'))
182
190
183
191
184 class NewHandler(AuthenticatedHandler):
192 class NewHandler(AuthenticatedHandler):
185
193
186 @web.authenticated
194 @web.authenticated
187 def get(self):
195 def get(self):
188 nbm = self.application.notebook_manager
196 nbm = self.application.notebook_manager
189 project = nbm.notebook_dir
197 project = nbm.notebook_dir
190 notebook_id = nbm.new_notebook()
198 notebook_id = nbm.new_notebook()
191 self.render(
199 self.render(
192 'notebook.html', project=project,
200 'notebook.html', project=project,
193 notebook_id=notebook_id,
201 notebook_id=notebook_id,
194 base_project_url=u'/', base_kernel_url=u'/',
202 base_project_url=u'/', base_kernel_url=u'/',
195 kill_kernel=False,
203 kill_kernel=False,
196 read_only=False,
204 read_only=False,
197 )
205 )
198
206
199
207
200 class NamedNotebookHandler(AuthenticatedHandler):
208 class NamedNotebookHandler(AuthenticatedHandler):
201
209
202 @authenticate_unless_readonly
210 @authenticate_unless_readonly
203 def get(self, notebook_id):
211 def get(self, notebook_id):
204 nbm = self.application.notebook_manager
212 nbm = self.application.notebook_manager
205 project = nbm.notebook_dir
213 project = nbm.notebook_dir
206 if not nbm.notebook_exists(notebook_id):
214 if not nbm.notebook_exists(notebook_id):
207 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
215 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
208
216
209 self.render(
217 self.render(
210 'notebook.html', project=project,
218 'notebook.html', project=project,
211 notebook_id=notebook_id,
219 notebook_id=notebook_id,
212 base_project_url=u'/', base_kernel_url=u'/',
220 base_project_url=u'/', base_kernel_url=u'/',
213 kill_kernel=False,
221 kill_kernel=False,
214 read_only=self.read_only,
222 read_only=self.read_only,
215 )
223 )
216
224
217
225
218 #-----------------------------------------------------------------------------
226 #-----------------------------------------------------------------------------
219 # Kernel handlers
227 # Kernel handlers
220 #-----------------------------------------------------------------------------
228 #-----------------------------------------------------------------------------
221
229
222
230
223 class MainKernelHandler(AuthenticatedHandler):
231 class MainKernelHandler(AuthenticatedHandler):
224
232
225 @web.authenticated
233 @web.authenticated
226 def get(self):
234 def get(self):
227 km = self.application.kernel_manager
235 km = self.application.kernel_manager
228 self.finish(jsonapi.dumps(km.kernel_ids))
236 self.finish(jsonapi.dumps(km.kernel_ids))
229
237
230 @web.authenticated
238 @web.authenticated
231 def post(self):
239 def post(self):
232 km = self.application.kernel_manager
240 km = self.application.kernel_manager
233 notebook_id = self.get_argument('notebook', default=None)
241 notebook_id = self.get_argument('notebook', default=None)
234 kernel_id = km.start_kernel(notebook_id)
242 kernel_id = km.start_kernel(notebook_id)
235 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
243 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
236 self.set_header('Location', '/'+kernel_id)
244 self.set_header('Location', '/'+kernel_id)
237 self.finish(jsonapi.dumps(data))
245 self.finish(jsonapi.dumps(data))
238
246
239
247
240 class KernelHandler(AuthenticatedHandler):
248 class KernelHandler(AuthenticatedHandler):
241
249
242 SUPPORTED_METHODS = ('DELETE')
250 SUPPORTED_METHODS = ('DELETE')
243
251
244 @web.authenticated
252 @web.authenticated
245 def delete(self, kernel_id):
253 def delete(self, kernel_id):
246 km = self.application.kernel_manager
254 km = self.application.kernel_manager
247 km.kill_kernel(kernel_id)
255 km.kill_kernel(kernel_id)
248 self.set_status(204)
256 self.set_status(204)
249 self.finish()
257 self.finish()
250
258
251
259
252 class KernelActionHandler(AuthenticatedHandler):
260 class KernelActionHandler(AuthenticatedHandler):
253
261
254 @web.authenticated
262 @web.authenticated
255 def post(self, kernel_id, action):
263 def post(self, kernel_id, action):
256 km = self.application.kernel_manager
264 km = self.application.kernel_manager
257 if action == 'interrupt':
265 if action == 'interrupt':
258 km.interrupt_kernel(kernel_id)
266 km.interrupt_kernel(kernel_id)
259 self.set_status(204)
267 self.set_status(204)
260 if action == 'restart':
268 if action == 'restart':
261 new_kernel_id = km.restart_kernel(kernel_id)
269 new_kernel_id = km.restart_kernel(kernel_id)
262 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
270 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
263 self.set_header('Location', '/'+new_kernel_id)
271 self.set_header('Location', '/'+new_kernel_id)
264 self.write(jsonapi.dumps(data))
272 self.write(jsonapi.dumps(data))
265 self.finish()
273 self.finish()
266
274
267
275
268 class ZMQStreamHandler(websocket.WebSocketHandler):
276 class ZMQStreamHandler(websocket.WebSocketHandler):
269
277
270 def _reserialize_reply(self, msg_list):
278 def _reserialize_reply(self, msg_list):
271 """Reserialize a reply message using JSON.
279 """Reserialize a reply message using JSON.
272
280
273 This takes the msg list from the ZMQ socket, unserializes it using
281 This takes the msg list from the ZMQ socket, unserializes it using
274 self.session and then serializes the result using JSON. This method
282 self.session and then serializes the result using JSON. This method
275 should be used by self._on_zmq_reply to build messages that can
283 should be used by self._on_zmq_reply to build messages that can
276 be sent back to the browser.
284 be sent back to the browser.
277 """
285 """
278 idents, msg_list = self.session.feed_identities(msg_list)
286 idents, msg_list = self.session.feed_identities(msg_list)
279 msg = self.session.unserialize(msg_list)
287 msg = self.session.unserialize(msg_list)
280 try:
288 try:
281 msg['header'].pop('date')
289 msg['header'].pop('date')
282 except KeyError:
290 except KeyError:
283 pass
291 pass
284 try:
292 try:
285 msg['parent_header'].pop('date')
293 msg['parent_header'].pop('date')
286 except KeyError:
294 except KeyError:
287 pass
295 pass
288 msg.pop('buffers')
296 msg.pop('buffers')
289 return jsonapi.dumps(msg)
297 return jsonapi.dumps(msg)
290
298
291 def _on_zmq_reply(self, msg_list):
299 def _on_zmq_reply(self, msg_list):
292 try:
300 try:
293 msg = self._reserialize_reply(msg_list)
301 msg = self._reserialize_reply(msg_list)
294 except:
302 except:
295 self.application.log.critical("Malformed message: %r" % msg_list)
303 self.application.log.critical("Malformed message: %r" % msg_list)
296 else:
304 else:
297 self.write_message(msg)
305 self.write_message(msg)
298
306
299
307
300 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
308 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
301
309
302 def open(self, kernel_id):
310 def open(self, kernel_id):
303 self.kernel_id = kernel_id.decode('ascii')
311 self.kernel_id = kernel_id.decode('ascii')
304 try:
312 try:
305 cfg = self.application.ipython_app.config
313 cfg = self.application.ipython_app.config
306 except AttributeError:
314 except AttributeError:
307 # protect from the case where this is run from something other than
315 # protect from the case where this is run from something other than
308 # the notebook app:
316 # the notebook app:
309 cfg = None
317 cfg = None
310 self.session = Session(config=cfg)
318 self.session = Session(config=cfg)
311 self.save_on_message = self.on_message
319 self.save_on_message = self.on_message
312 self.on_message = self.on_first_message
320 self.on_message = self.on_first_message
313
321
314 def get_current_user(self):
322 def get_current_user(self):
315 user_id = self.get_secure_cookie("username")
323 user_id = self.get_secure_cookie("username")
316 if user_id == '' or (user_id is None and not self.application.password):
324 if user_id == '' or (user_id is None and not self.application.password):
317 user_id = 'anonymous'
325 user_id = 'anonymous'
318 return user_id
326 return user_id
319
327
320 def _inject_cookie_message(self, msg):
328 def _inject_cookie_message(self, msg):
321 """Inject the first message, which is the document cookie,
329 """Inject the first message, which is the document cookie,
322 for authentication."""
330 for authentication."""
323 if isinstance(msg, unicode):
331 if isinstance(msg, unicode):
324 # Cookie can't constructor doesn't accept unicode strings for some reason
332 # Cookie can't constructor doesn't accept unicode strings for some reason
325 msg = msg.encode('utf8', 'replace')
333 msg = msg.encode('utf8', 'replace')
326 try:
334 try:
327 self.request._cookies = Cookie.SimpleCookie(msg)
335 self.request._cookies = Cookie.SimpleCookie(msg)
328 except:
336 except:
329 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
337 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
330
338
331 def on_first_message(self, msg):
339 def on_first_message(self, msg):
332 self._inject_cookie_message(msg)
340 self._inject_cookie_message(msg)
333 if self.get_current_user() is None:
341 if self.get_current_user() is None:
334 logging.warn("Couldn't authenticate WebSocket connection")
342 logging.warn("Couldn't authenticate WebSocket connection")
335 raise web.HTTPError(403)
343 raise web.HTTPError(403)
336 self.on_message = self.save_on_message
344 self.on_message = self.save_on_message
337
345
338
346
339 class IOPubHandler(AuthenticatedZMQStreamHandler):
347 class IOPubHandler(AuthenticatedZMQStreamHandler):
340
348
341 def initialize(self, *args, **kwargs):
349 def initialize(self, *args, **kwargs):
342 self._kernel_alive = True
350 self._kernel_alive = True
343 self._beating = False
351 self._beating = False
344 self.iopub_stream = None
352 self.iopub_stream = None
345 self.hb_stream = None
353 self.hb_stream = None
346
354
347 def on_first_message(self, msg):
355 def on_first_message(self, msg):
348 try:
356 try:
349 super(IOPubHandler, self).on_first_message(msg)
357 super(IOPubHandler, self).on_first_message(msg)
350 except web.HTTPError:
358 except web.HTTPError:
351 self.close()
359 self.close()
352 return
360 return
353 km = self.application.kernel_manager
361 km = self.application.kernel_manager
354 self.time_to_dead = km.time_to_dead
362 self.time_to_dead = km.time_to_dead
355 kernel_id = self.kernel_id
363 kernel_id = self.kernel_id
356 try:
364 try:
357 self.iopub_stream = km.create_iopub_stream(kernel_id)
365 self.iopub_stream = km.create_iopub_stream(kernel_id)
358 self.hb_stream = km.create_hb_stream(kernel_id)
366 self.hb_stream = km.create_hb_stream(kernel_id)
359 except web.HTTPError:
367 except web.HTTPError:
360 # WebSockets don't response to traditional error codes so we
368 # WebSockets don't response to traditional error codes so we
361 # close the connection.
369 # close the connection.
362 if not self.stream.closed():
370 if not self.stream.closed():
363 self.stream.close()
371 self.stream.close()
364 self.close()
372 self.close()
365 else:
373 else:
366 self.iopub_stream.on_recv(self._on_zmq_reply)
374 self.iopub_stream.on_recv(self._on_zmq_reply)
367 self.start_hb(self.kernel_died)
375 self.start_hb(self.kernel_died)
368
376
369 def on_message(self, msg):
377 def on_message(self, msg):
370 pass
378 pass
371
379
372 def on_close(self):
380 def on_close(self):
373 # This method can be called twice, once by self.kernel_died and once
381 # This method can be called twice, once by self.kernel_died and once
374 # from the WebSocket close event. If the WebSocket connection is
382 # from the WebSocket close event. If the WebSocket connection is
375 # closed before the ZMQ streams are setup, they could be None.
383 # closed before the ZMQ streams are setup, they could be None.
376 self.stop_hb()
384 self.stop_hb()
377 if self.iopub_stream is not None and not self.iopub_stream.closed():
385 if self.iopub_stream is not None and not self.iopub_stream.closed():
378 self.iopub_stream.on_recv(None)
386 self.iopub_stream.on_recv(None)
379 self.iopub_stream.close()
387 self.iopub_stream.close()
380 if self.hb_stream is not None and not self.hb_stream.closed():
388 if self.hb_stream is not None and not self.hb_stream.closed():
381 self.hb_stream.close()
389 self.hb_stream.close()
382
390
383 def start_hb(self, callback):
391 def start_hb(self, callback):
384 """Start the heartbeating and call the callback if the kernel dies."""
392 """Start the heartbeating and call the callback if the kernel dies."""
385 if not self._beating:
393 if not self._beating:
386 self._kernel_alive = True
394 self._kernel_alive = True
387
395
388 def ping_or_dead():
396 def ping_or_dead():
389 if self._kernel_alive:
397 if self._kernel_alive:
390 self._kernel_alive = False
398 self._kernel_alive = False
391 self.hb_stream.send(b'ping')
399 self.hb_stream.send(b'ping')
392 else:
400 else:
393 try:
401 try:
394 callback()
402 callback()
395 except:
403 except:
396 pass
404 pass
397 finally:
405 finally:
398 self._hb_periodic_callback.stop()
406 self._hb_periodic_callback.stop()
399
407
400 def beat_received(msg):
408 def beat_received(msg):
401 self._kernel_alive = True
409 self._kernel_alive = True
402
410
403 self.hb_stream.on_recv(beat_received)
411 self.hb_stream.on_recv(beat_received)
404 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
412 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
405 self._hb_periodic_callback.start()
413 self._hb_periodic_callback.start()
406 self._beating= True
414 self._beating= True
407
415
408 def stop_hb(self):
416 def stop_hb(self):
409 """Stop the heartbeating and cancel all related callbacks."""
417 """Stop the heartbeating and cancel all related callbacks."""
410 if self._beating:
418 if self._beating:
411 self._hb_periodic_callback.stop()
419 self._hb_periodic_callback.stop()
412 if not self.hb_stream.closed():
420 if not self.hb_stream.closed():
413 self.hb_stream.on_recv(None)
421 self.hb_stream.on_recv(None)
414
422
415 def kernel_died(self):
423 def kernel_died(self):
416 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
424 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
417 self.write_message(
425 self.write_message(
418 {'header': {'msg_type': 'status'},
426 {'header': {'msg_type': 'status'},
419 'parent_header': {},
427 'parent_header': {},
420 'content': {'execution_state':'dead'}
428 'content': {'execution_state':'dead'}
421 }
429 }
422 )
430 )
423 self.on_close()
431 self.on_close()
424
432
425
433
426 class ShellHandler(AuthenticatedZMQStreamHandler):
434 class ShellHandler(AuthenticatedZMQStreamHandler):
427
435
428 def initialize(self, *args, **kwargs):
436 def initialize(self, *args, **kwargs):
429 self.shell_stream = None
437 self.shell_stream = None
430
438
431 def on_first_message(self, msg):
439 def on_first_message(self, msg):
432 try:
440 try:
433 super(ShellHandler, self).on_first_message(msg)
441 super(ShellHandler, self).on_first_message(msg)
434 except web.HTTPError:
442 except web.HTTPError:
435 self.close()
443 self.close()
436 return
444 return
437 km = self.application.kernel_manager
445 km = self.application.kernel_manager
438 self.max_msg_size = km.max_msg_size
446 self.max_msg_size = km.max_msg_size
439 kernel_id = self.kernel_id
447 kernel_id = self.kernel_id
440 try:
448 try:
441 self.shell_stream = km.create_shell_stream(kernel_id)
449 self.shell_stream = km.create_shell_stream(kernel_id)
442 except web.HTTPError:
450 except web.HTTPError:
443 # WebSockets don't response to traditional error codes so we
451 # WebSockets don't response to traditional error codes so we
444 # close the connection.
452 # close the connection.
445 if not self.stream.closed():
453 if not self.stream.closed():
446 self.stream.close()
454 self.stream.close()
447 self.close()
455 self.close()
448 else:
456 else:
449 self.shell_stream.on_recv(self._on_zmq_reply)
457 self.shell_stream.on_recv(self._on_zmq_reply)
450
458
451 def on_message(self, msg):
459 def on_message(self, msg):
452 if len(msg) < self.max_msg_size:
460 if len(msg) < self.max_msg_size:
453 msg = jsonapi.loads(msg)
461 msg = jsonapi.loads(msg)
454 self.session.send(self.shell_stream, msg)
462 self.session.send(self.shell_stream, msg)
455
463
456 def on_close(self):
464 def on_close(self):
457 # Make sure the stream exists and is not already closed.
465 # Make sure the stream exists and is not already closed.
458 if self.shell_stream is not None and not self.shell_stream.closed():
466 if self.shell_stream is not None and not self.shell_stream.closed():
459 self.shell_stream.close()
467 self.shell_stream.close()
460
468
461
469
462 #-----------------------------------------------------------------------------
470 #-----------------------------------------------------------------------------
463 # Notebook web service handlers
471 # Notebook web service handlers
464 #-----------------------------------------------------------------------------
472 #-----------------------------------------------------------------------------
465
473
466 class NotebookRootHandler(AuthenticatedHandler):
474 class NotebookRootHandler(AuthenticatedHandler):
467
475
468 @authenticate_unless_readonly
476 @authenticate_unless_readonly
469 def get(self):
477 def get(self):
470
478
471 nbm = self.application.notebook_manager
479 nbm = self.application.notebook_manager
472 files = nbm.list_notebooks()
480 files = nbm.list_notebooks()
473 self.finish(jsonapi.dumps(files))
481 self.finish(jsonapi.dumps(files))
474
482
475 @web.authenticated
483 @web.authenticated
476 def post(self):
484 def post(self):
477 nbm = self.application.notebook_manager
485 nbm = self.application.notebook_manager
478 body = self.request.body.strip()
486 body = self.request.body.strip()
479 format = self.get_argument('format', default='json')
487 format = self.get_argument('format', default='json')
480 name = self.get_argument('name', default=None)
488 name = self.get_argument('name', default=None)
481 if body:
489 if body:
482 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
490 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
483 else:
491 else:
484 notebook_id = nbm.new_notebook()
492 notebook_id = nbm.new_notebook()
485 self.set_header('Location', '/'+notebook_id)
493 self.set_header('Location', '/'+notebook_id)
486 self.finish(jsonapi.dumps(notebook_id))
494 self.finish(jsonapi.dumps(notebook_id))
487
495
488
496
489 class NotebookHandler(AuthenticatedHandler):
497 class NotebookHandler(AuthenticatedHandler):
490
498
491 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
499 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
492
500
493 @authenticate_unless_readonly
501 @authenticate_unless_readonly
494 def get(self, notebook_id):
502 def get(self, notebook_id):
495 nbm = self.application.notebook_manager
503 nbm = self.application.notebook_manager
496 format = self.get_argument('format', default='json')
504 format = self.get_argument('format', default='json')
497 last_mod, name, data = nbm.get_notebook(notebook_id, format)
505 last_mod, name, data = nbm.get_notebook(notebook_id, format)
498
506
499 if format == u'json':
507 if format == u'json':
500 self.set_header('Content-Type', 'application/json')
508 self.set_header('Content-Type', 'application/json')
501 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
509 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
502 elif format == u'py':
510 elif format == u'py':
503 self.set_header('Content-Type', 'application/x-python')
511 self.set_header('Content-Type', 'application/x-python')
504 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
512 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
505 self.set_header('Last-Modified', last_mod)
513 self.set_header('Last-Modified', last_mod)
506 self.finish(data)
514 self.finish(data)
507
515
508 @web.authenticated
516 @web.authenticated
509 def put(self, notebook_id):
517 def put(self, notebook_id):
510 nbm = self.application.notebook_manager
518 nbm = self.application.notebook_manager
511 format = self.get_argument('format', default='json')
519 format = self.get_argument('format', default='json')
512 name = self.get_argument('name', default=None)
520 name = self.get_argument('name', default=None)
513 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
521 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
514 self.set_status(204)
522 self.set_status(204)
515 self.finish()
523 self.finish()
516
524
517 @web.authenticated
525 @web.authenticated
518 def delete(self, notebook_id):
526 def delete(self, notebook_id):
519 nbm = self.application.notebook_manager
527 nbm = self.application.notebook_manager
520 nbm.delete_notebook(notebook_id)
528 nbm.delete_notebook(notebook_id)
521 self.set_status(204)
529 self.set_status(204)
522 self.finish()
530 self.finish()
523
531
524 #-----------------------------------------------------------------------------
532 #-----------------------------------------------------------------------------
525 # RST web service handlers
533 # RST web service handlers
526 #-----------------------------------------------------------------------------
534 #-----------------------------------------------------------------------------
527
535
528
536
529 class RSTHandler(AuthenticatedHandler):
537 class RSTHandler(AuthenticatedHandler):
530
538
531 @web.authenticated
539 @web.authenticated
532 def post(self):
540 def post(self):
533 if publish_string is None:
541 if publish_string is None:
534 raise web.HTTPError(503, u'docutils not available')
542 raise web.HTTPError(503, u'docutils not available')
535 body = self.request.body.strip()
543 body = self.request.body.strip()
536 source = body
544 source = body
537 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
545 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
538 defaults = {'file_insertion_enabled': 0,
546 defaults = {'file_insertion_enabled': 0,
539 'raw_enabled': 0,
547 'raw_enabled': 0,
540 '_disable_config': 1,
548 '_disable_config': 1,
541 'stylesheet_path': 0
549 'stylesheet_path': 0
542 # 'template': template_path
550 # 'template': template_path
543 }
551 }
544 try:
552 try:
545 html = publish_string(source, writer_name='html',
553 html = publish_string(source, writer_name='html',
546 settings_overrides=defaults
554 settings_overrides=defaults
547 )
555 )
548 except:
556 except:
549 raise web.HTTPError(400, u'Invalid RST')
557 raise web.HTTPError(400, u'Invalid RST')
550 print html
558 print html
551 self.set_header('Content-Type', 'text/html')
559 self.set_header('Content-Type', 'text/html')
552 self.finish(html)
560 self.finish(html)
553
561
554
562
@@ -1,103 +1,115 b''
1
1
2 .border-box-sizing {
2 .border-box-sizing {
3 box-sizing: border-box;
3 box-sizing: border-box;
4 -moz-box-sizing: border-box;
4 -moz-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
6 }
6 }
7
7
8 /* Flexible box model classes */
8 /* Flexible box model classes */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
10
10
11 .hbox {
11 .hbox {
12 display: -webkit-box;
12 display: -webkit-box;
13 -webkit-box-orient: horizontal;
13 -webkit-box-orient: horizontal;
14 -webkit-box-align: stretch;
14 -webkit-box-align: stretch;
15
15
16 display: -moz-box;
16 display: -moz-box;
17 -moz-box-orient: horizontal;
17 -moz-box-orient: horizontal;
18 -moz-box-align: stretch;
18 -moz-box-align: stretch;
19
19
20 display: box;
20 display: box;
21 box-orient: horizontal;
21 box-orient: horizontal;
22 box-align: stretch;
22 box-align: stretch;
23 }
23 }
24
24
25 .hbox > * {
25 .hbox > * {
26 -webkit-box-flex: 0;
26 -webkit-box-flex: 0;
27 -moz-box-flex: 0;
27 -moz-box-flex: 0;
28 box-flex: 0;
28 box-flex: 0;
29 }
29 }
30
30
31 .vbox {
31 .vbox {
32 display: -webkit-box;
32 display: -webkit-box;
33 -webkit-box-orient: vertical;
33 -webkit-box-orient: vertical;
34 -webkit-box-align: stretch;
34 -webkit-box-align: stretch;
35
35
36 display: -moz-box;
36 display: -moz-box;
37 -moz-box-orient: vertical;
37 -moz-box-orient: vertical;
38 -moz-box-align: stretch;
38 -moz-box-align: stretch;
39
39
40 display: box;
40 display: box;
41 box-orient: vertical;
41 box-orient: vertical;
42 box-align: stretch;
42 box-align: stretch;
43 }
43 }
44
44
45 .vbox > * {
45 .vbox > * {
46 -webkit-box-flex: 0;
46 -webkit-box-flex: 0;
47 -moz-box-flex: 0;
47 -moz-box-flex: 0;
48 box-flex: 0;
48 box-flex: 0;
49 }
49 }
50
50
51 .reverse {
51 .reverse {
52 -webkit-box-direction: reverse;
52 -webkit-box-direction: reverse;
53 -moz-box-direction: reverse;
53 -moz-box-direction: reverse;
54 box-direction: reverse;
54 box-direction: reverse;
55 }
55 }
56
56
57 .box-flex0 {
57 .box-flex0 {
58 -webkit-box-flex: 0;
58 -webkit-box-flex: 0;
59 -moz-box-flex: 0;
59 -moz-box-flex: 0;
60 box-flex: 0;
60 box-flex: 0;
61 }
61 }
62
62
63 .box-flex1, .box-flex {
63 .box-flex1, .box-flex {
64 -webkit-box-flex: 1;
64 -webkit-box-flex: 1;
65 -moz-box-flex: 1;
65 -moz-box-flex: 1;
66 box-flex: 1;
66 box-flex: 1;
67 }
67 }
68
68
69 .box-flex2 {
69 .box-flex2 {
70 -webkit-box-flex: 2;
70 -webkit-box-flex: 2;
71 -moz-box-flex: 2;
71 -moz-box-flex: 2;
72 box-flex: 2;
72 box-flex: 2;
73 }
73 }
74
74
75 .box-group1 {
75 .box-group1 {
76 -webkit-box-flex-group: 1;
76 -webkit-box-flex-group: 1;
77 -moz-box-flex-group: 1;
77 -moz-box-flex-group: 1;
78 box-flex-group: 1;
78 box-flex-group: 1;
79 }
79 }
80
80
81 .box-group2 {
81 .box-group2 {
82 -webkit-box-flex-group: 2;
82 -webkit-box-flex-group: 2;
83 -moz-box-flex-group: 2;
83 -moz-box-flex-group: 2;
84 box-flex-group: 2;
84 box-flex-group: 2;
85 }
85 }
86
86
87 .start {
87 .start {
88 -webkit-box-pack: start;
88 -webkit-box-pack: start;
89 -moz-box-pack: start;
89 -moz-box-pack: start;
90 box-pack: start;
90 box-pack: start;
91 }
91 }
92
92
93 .end {
93 .end {
94 -webkit-box-pack: end;
94 -webkit-box-pack: end;
95 -moz-box-pack: end;
95 -moz-box-pack: end;
96 box-pack: end;
96 box-pack: end;
97 }
97 }
98
98
99 .center {
99 .center {
100 -webkit-box-pack: center;
100 -webkit-box-pack: center;
101 -moz-box-pack: center;
101 -moz-box-pack: center;
102 box-pack: center;
102 box-pack: center;
103 }
103 }
104
105 #message {
106 border: 1px solid red;
107 background-color: #FFD3D1;
108 text-align: center;
109 padding: 0.5em;
110 margin: 0.5em;
111 }
112
113 #content_panel {
114 margin: 0.5em;
115 } No newline at end of file
@@ -1,82 +1,82 b''
1
1
2 /**
2 /**
3 * Primary styles
3 * Primary styles
4 *
4 *
5 * Author: IPython Development Team
5 * Author: IPython Development Team
6 */
6 */
7
7
8
8
9 body {
9 body {
10 background-color: white;
10 background-color: white;
11 /* This makes sure that the body covers the entire window and needs to
11 /* This makes sure that the body covers the entire window and needs to
12 be in a different element than the display: box in wrapper below */
12 be in a different element than the display: box in wrapper below */
13 position: absolute;
13 position: absolute;
14 left: 0px;
14 left: 0px;
15 right: 0px;
15 right: 0px;
16 top: 0px;
16 top: 0px;
17 bottom: 0px;
17 bottom: 0px;
18 overflow: auto;
18 overflow: auto;
19 }
19 }
20
20
21 #left_panel {
21 #left_panel {
22 }
22 }
23
23
24 #drop_zone {
24 #drop_zone {
25 height: 200px;
25 height: 200px;
26 width: 200px
26 width: 200px
27 }
27 }
28
28
29 #content_panel {
29 #content_panel {
30 width: 600px;
30 width: 600px;
31 }
31 }
32
32
33 #content_toolbar {
33 #content_toolbar {
34 padding: 10px 5px 5px 5px;
34 padding: 5px;
35 height: 25px;
35 height: 25px;
36 line-height: 25px;
36 line-height: 25px;
37 }
37 }
38
38
39 #header_border {
39 #header_border {
40 width: 100%;
40 width: 100%;
41 height: 2px;
41 height: 2px;
42 }
42 }
43
43
44 #app_hbox {
44 #app_hbox {
45 width: 100%;
45 width: 100%;
46 }
46 }
47
47
48 #drag_info {
48 #drag_info {
49 float: left;
49 float: left;
50 }
50 }
51
51
52 #notebooks_buttons {
52 #notebooks_buttons {
53 float: right;
53 float: right;
54 }
54 }
55
55
56 #project_name {
56 #project_name {
57 height: 25px;
57 height: 25px;
58 line-height: 25px;
58 line-height: 25px;
59 padding: 3px;
59 padding: 3px;
60 }
60 }
61
61
62 .notebook_item {
62 .notebook_item {
63 height: 25px;
63 height: 25px;
64 line-height: 25px;
64 line-height: 25px;
65 padding: 3px;
65 padding: 3px;
66 }
66 }
67
67
68 .notebook_item a {
68 .notebook_item a {
69 text-decoration: none;
69 text-decoration: none;
70 }
70 }
71
71
72 .item_buttons {
72 .item_buttons {
73 float: right;
73 float: right;
74 }
74 }
75
75
76 .item_buttons .upload_button {
76 .item_buttons .upload_button {
77 color: darkred;
77 color: darkred;
78 }
78 }
79
79
80 .highlight_text {
80 .highlight_text {
81 color: blue;
81 color: blue;
82 }
82 }
@@ -1,55 +1,61 b''
1 <!DOCTYPE HTML>
1 <!DOCTYPE HTML>
2 <html>
2 <html>
3
3
4 <head>
4 <head>
5 <meta charset="utf-8">
5 <meta charset="utf-8">
6
6
7 <title>IPython Notebook</title>
7 <title>IPython Notebook</title>
8
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13
13
14 <meta name="read_only" content="{{read_only}}"/>
14 <meta name="read_only" content="{{read_only}}"/>
15
15
16 </head>
16 </head>
17
17
18 <body>
18 <body>
19
19
20 <div id="header">
20 <div id="header">
21 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
21 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
22 </div>
22 </div>
23
23
24 <div id="header_border"></div>
24 <div id="header_border"></div>
25
25
26 <div id="main_app">
26 <div id="main_app">
27
27
28 <div id="app_hbox">
28 <div id="app_hbox">
29
29
30 <div id="left_panel">
30 <div id="left_panel">
31 </div>
31 </div>
32
32
33 <div id="content_panel">
33 <div id="content_panel">
34 {% if message %}
35 <div id="message">
36 {{message}}
37 </div>
38 {% end %}
39
34 <form action="/login?next={{url_escape(next)}}" method="post">
40 <form action="/login?next={{url_escape(next)}}" method="post">
35 Password: <input type="password" name="password">
41 Password: <input type="password" name="password">
36 <input type="submit" value="Sign in" id="signin">
42 <input type="submit" value="Sign in" id="signin">
37 </form>
43 </form>
38 </div>
44 </div>
39 <div id="right_panel">
45 <div id="right_panel">
40 </div>
46 </div>
41
47
42 </div>
48 </div>
43
49
44 </div>
50 </div>
45
51
46 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
52 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
47 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
53 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
48 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
54 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
49 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
55 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
50
56
51 </body>
57 </body>
52
58
53 </html>
59 </html>
54
60
55
61
General Comments 0
You need to be logged in to leave comments. Login now