##// END OF EJS Templates
Refactoring kernel_died method to make subclass friendly.
Brian E. Granger -
Show More
@@ -1,888 +1,892 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 Cookie
19 import Cookie
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28 import time
28 import time
29 import uuid
29 import uuid
30 import os
30 import os
31
31
32 from tornado.escape import url_escape
32 from tornado.escape import url_escape
33 from tornado import web
33 from tornado import web
34 from tornado import websocket
34 from tornado import websocket
35
35
36 from zmq.eventloop import ioloop
36 from zmq.eventloop import ioloop
37 from zmq.utils import jsonapi
37 from zmq.utils import jsonapi
38
38
39 from IPython.external.decorator import decorator
39 from IPython.external.decorator import decorator
40 from IPython.zmq.session import Session
40 from IPython.zmq.session import Session
41 from IPython.lib.security import passwd_check
41 from IPython.lib.security import passwd_check
42 from IPython.utils.jsonutil import date_default
42 from IPython.utils.jsonutil import date_default
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44
44
45 try:
45 try:
46 from docutils.core import publish_string
46 from docutils.core import publish_string
47 except ImportError:
47 except ImportError:
48 publish_string = None
48 publish_string = None
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 # parts tornado cares about haven't really changed, so it's OK to continue
55 # parts tornado cares about haven't really changed, so it's OK to continue
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 # version as of Oct 30/2011) the version check fails, see the issue report:
57 # version as of Oct 30/2011) the version check fails, see the issue report:
58
58
59 # https://github.com/facebook/tornado/issues/385
59 # https://github.com/facebook/tornado/issues/385
60
60
61 # This issue has been fixed in Tornado post 2.1.1:
61 # This issue has been fixed in Tornado post 2.1.1:
62
62
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64
64
65 # Here we manually apply the same patch as above so that users of IPython can
65 # Here we manually apply the same patch as above so that users of IPython can
66 # continue to work with an officially released Tornado. We make the
66 # continue to work with an officially released Tornado. We make the
67 # monkeypatch version check as narrow as possible to limit its effects; once
67 # monkeypatch version check as narrow as possible to limit its effects; once
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69
69
70 import tornado
70 import tornado
71
71
72 if tornado.version_info <= (2,1,1):
72 if tornado.version_info <= (2,1,1):
73
73
74 def _execute(self, transforms, *args, **kwargs):
74 def _execute(self, transforms, *args, **kwargs):
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76
76
77 self.open_args = args
77 self.open_args = args
78 self.open_kwargs = kwargs
78 self.open_kwargs = kwargs
79
79
80 # The difference between version 8 and 13 is that in 8 the
80 # The difference between version 8 and 13 is that in 8 the
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 # simply "Origin".
82 # simply "Origin".
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 self.ws_connection = WebSocketProtocol8(self)
84 self.ws_connection = WebSocketProtocol8(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 elif self.request.headers.get("Sec-WebSocket-Version"):
87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 self.stream.write(tornado.escape.utf8(
88 self.stream.write(tornado.escape.utf8(
89 "HTTP/1.1 426 Upgrade Required\r\n"
89 "HTTP/1.1 426 Upgrade Required\r\n"
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 self.stream.close()
91 self.stream.close()
92
92
93 else:
93 else:
94 self.ws_connection = WebSocketProtocol76(self)
94 self.ws_connection = WebSocketProtocol76(self)
95 self.ws_connection.accept_connection()
95 self.ws_connection.accept_connection()
96
96
97 websocket.WebSocketHandler._execute = _execute
97 websocket.WebSocketHandler._execute = _execute
98 del _execute
98 del _execute
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Decorator for disabling read-only handlers
101 # Decorator for disabling read-only handlers
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 @decorator
104 @decorator
105 def not_if_readonly(f, self, *args, **kwargs):
105 def not_if_readonly(f, self, *args, **kwargs):
106 if self.application.read_only:
106 if self.application.read_only:
107 raise web.HTTPError(403, "Notebook server is read-only")
107 raise web.HTTPError(403, "Notebook server is read-only")
108 else:
108 else:
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110
110
111 @decorator
111 @decorator
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 """authenticate this page *unless* readonly view is active.
113 """authenticate this page *unless* readonly view is active.
114
114
115 In read-only mode, the notebook list and print view should
115 In read-only mode, the notebook list and print view should
116 be accessible without authentication.
116 be accessible without authentication.
117 """
117 """
118
118
119 @web.authenticated
119 @web.authenticated
120 def auth_f(self, *args, **kwargs):
120 def auth_f(self, *args, **kwargs):
121 return f(self, *args, **kwargs)
121 return f(self, *args, **kwargs)
122
122
123 if self.application.read_only:
123 if self.application.read_only:
124 return f(self, *args, **kwargs)
124 return f(self, *args, **kwargs)
125 else:
125 else:
126 return auth_f(self, *args, **kwargs)
126 return auth_f(self, *args, **kwargs)
127
127
128 def urljoin(*pieces):
128 def urljoin(*pieces):
129 """Join componenet of url into a relative url
129 """Join componenet of url into a relative url
130
130
131 Use to prevent double slash when joining subpath
131 Use to prevent double slash when joining subpath
132 """
132 """
133 striped = [s.strip('/') for s in pieces]
133 striped = [s.strip('/') for s in pieces]
134 return '/'.join(s for s in striped if s)
134 return '/'.join(s for s in striped if s)
135
135
136 #-----------------------------------------------------------------------------
136 #-----------------------------------------------------------------------------
137 # Top-level handlers
137 # Top-level handlers
138 #-----------------------------------------------------------------------------
138 #-----------------------------------------------------------------------------
139
139
140 class RequestHandler(web.RequestHandler):
140 class RequestHandler(web.RequestHandler):
141 """RequestHandler with default variable setting."""
141 """RequestHandler with default variable setting."""
142
142
143 def render(*args, **kwargs):
143 def render(*args, **kwargs):
144 kwargs.setdefault('message', '')
144 kwargs.setdefault('message', '')
145 return web.RequestHandler.render(*args, **kwargs)
145 return web.RequestHandler.render(*args, **kwargs)
146
146
147 class AuthenticatedHandler(RequestHandler):
147 class AuthenticatedHandler(RequestHandler):
148 """A RequestHandler with an authenticated user."""
148 """A RequestHandler with an authenticated user."""
149
149
150 def get_current_user(self):
150 def get_current_user(self):
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
152 # For now the user_id should not return empty, but it could eventually
152 # For now the user_id should not return empty, but it could eventually
153 if user_id == '':
153 if user_id == '':
154 user_id = 'anonymous'
154 user_id = 'anonymous'
155 if user_id is None:
155 if user_id is None:
156 # prevent extra Invalid cookie sig warnings:
156 # prevent extra Invalid cookie sig warnings:
157 self.clear_cookie(self.settings['cookie_name'])
157 self.clear_cookie(self.settings['cookie_name'])
158 if not self.application.password and not self.application.read_only:
158 if not self.application.password and not self.application.read_only:
159 user_id = 'anonymous'
159 user_id = 'anonymous'
160 return user_id
160 return user_id
161
161
162 @property
162 @property
163 def logged_in(self):
163 def logged_in(self):
164 """Is a user currently logged in?
164 """Is a user currently logged in?
165
165
166 """
166 """
167 user = self.get_current_user()
167 user = self.get_current_user()
168 return (user and not user == 'anonymous')
168 return (user and not user == 'anonymous')
169
169
170 @property
170 @property
171 def login_available(self):
171 def login_available(self):
172 """May a user proceed to log in?
172 """May a user proceed to log in?
173
173
174 This returns True if login capability is available, irrespective of
174 This returns True if login capability is available, irrespective of
175 whether the user is already logged in or not.
175 whether the user is already logged in or not.
176
176
177 """
177 """
178 return bool(self.application.password)
178 return bool(self.application.password)
179
179
180 @property
180 @property
181 def read_only(self):
181 def read_only(self):
182 """Is the notebook read-only?
182 """Is the notebook read-only?
183
183
184 """
184 """
185 return self.application.read_only
185 return self.application.read_only
186
186
187 @property
187 @property
188 def ws_url(self):
188 def ws_url(self):
189 """websocket url matching the current request
189 """websocket url matching the current request
190
190
191 turns http[s]://host[:port] into
191 turns http[s]://host[:port] into
192 ws[s]://host[:port]
192 ws[s]://host[:port]
193 """
193 """
194 proto = self.request.protocol.replace('http', 'ws')
194 proto = self.request.protocol.replace('http', 'ws')
195 host = self.application.ipython_app.websocket_host # default to config value
195 host = self.application.ipython_app.websocket_host # default to config value
196 if host == '':
196 if host == '':
197 host = self.request.host # get from request
197 host = self.request.host # get from request
198 return "%s://%s" % (proto, host)
198 return "%s://%s" % (proto, host)
199
199
200
200
201 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
201 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
202 """static files should only be accessible when logged in"""
202 """static files should only be accessible when logged in"""
203
203
204 @authenticate_unless_readonly
204 @authenticate_unless_readonly
205 def get(self, path):
205 def get(self, path):
206 return web.StaticFileHandler.get(self, path)
206 return web.StaticFileHandler.get(self, path)
207
207
208
208
209 class ProjectDashboardHandler(AuthenticatedHandler):
209 class ProjectDashboardHandler(AuthenticatedHandler):
210
210
211 @authenticate_unless_readonly
211 @authenticate_unless_readonly
212 def get(self):
212 def get(self):
213 nbm = self.application.notebook_manager
213 nbm = self.application.notebook_manager
214 project = nbm.notebook_dir
214 project = nbm.notebook_dir
215 template = self.application.jinja2_env.get_template('projectdashboard.html')
215 template = self.application.jinja2_env.get_template('projectdashboard.html')
216 self.write( template.render(project=project,
216 self.write( template.render(project=project,
217 base_project_url=self.application.ipython_app.base_project_url,
217 base_project_url=self.application.ipython_app.base_project_url,
218 base_kernel_url=self.application.ipython_app.base_kernel_url,
218 base_kernel_url=self.application.ipython_app.base_kernel_url,
219 read_only=self.read_only,
219 read_only=self.read_only,
220 logged_in=self.logged_in,
220 logged_in=self.logged_in,
221 login_available=self.login_available))
221 login_available=self.login_available))
222
222
223
223
224 class LoginHandler(AuthenticatedHandler):
224 class LoginHandler(AuthenticatedHandler):
225
225
226 def _render(self, message=None):
226 def _render(self, message=None):
227 template = self.application.jinja2_env.get_template('login.html')
227 template = self.application.jinja2_env.get_template('login.html')
228 self.write( template.render(
228 self.write( template.render(
229 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
229 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
230 read_only=self.read_only,
230 read_only=self.read_only,
231 logged_in=self.logged_in,
231 logged_in=self.logged_in,
232 login_available=self.login_available,
232 login_available=self.login_available,
233 base_project_url=self.application.ipython_app.base_project_url,
233 base_project_url=self.application.ipython_app.base_project_url,
234 message=message
234 message=message
235 ))
235 ))
236
236
237 def get(self):
237 def get(self):
238 if self.current_user:
238 if self.current_user:
239 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
239 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
240 else:
240 else:
241 self._render()
241 self._render()
242
242
243 def post(self):
243 def post(self):
244 pwd = self.get_argument('password', default=u'')
244 pwd = self.get_argument('password', default=u'')
245 if self.application.password:
245 if self.application.password:
246 if passwd_check(self.application.password, pwd):
246 if passwd_check(self.application.password, pwd):
247 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
247 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
248 else:
248 else:
249 self._render(message={'error': 'Invalid password'})
249 self._render(message={'error': 'Invalid password'})
250 return
250 return
251
251
252 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
252 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
253
253
254
254
255 class LogoutHandler(AuthenticatedHandler):
255 class LogoutHandler(AuthenticatedHandler):
256
256
257 def get(self):
257 def get(self):
258 self.clear_cookie(self.settings['cookie_name'])
258 self.clear_cookie(self.settings['cookie_name'])
259 if self.login_available:
259 if self.login_available:
260 message = {'info': 'Successfully logged out.'}
260 message = {'info': 'Successfully logged out.'}
261 else:
261 else:
262 message = {'warning': 'Cannot log out. Notebook authentication '
262 message = {'warning': 'Cannot log out. Notebook authentication '
263 'is disabled.'}
263 'is disabled.'}
264 template = self.application.jinja2_env.get_template('logout.html')
264 template = self.application.jinja2_env.get_template('logout.html')
265 self.write( template.render(
265 self.write( template.render(
266 read_only=self.read_only,
266 read_only=self.read_only,
267 logged_in=self.logged_in,
267 logged_in=self.logged_in,
268 login_available=self.login_available,
268 login_available=self.login_available,
269 base_project_url=self.application.ipython_app.base_project_url,
269 base_project_url=self.application.ipython_app.base_project_url,
270 message=message))
270 message=message))
271
271
272
272
273 class NewHandler(AuthenticatedHandler):
273 class NewHandler(AuthenticatedHandler):
274
274
275 @web.authenticated
275 @web.authenticated
276 def get(self):
276 def get(self):
277 nbm = self.application.notebook_manager
277 nbm = self.application.notebook_manager
278 project = nbm.notebook_dir
278 project = nbm.notebook_dir
279 notebook_id = nbm.new_notebook()
279 notebook_id = nbm.new_notebook()
280 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
280 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
281
281
282 class NamedNotebookHandler(AuthenticatedHandler):
282 class NamedNotebookHandler(AuthenticatedHandler):
283
283
284 @authenticate_unless_readonly
284 @authenticate_unless_readonly
285 def get(self, notebook_id):
285 def get(self, notebook_id):
286 nbm = self.application.notebook_manager
286 nbm = self.application.notebook_manager
287 project = nbm.notebook_dir
287 project = nbm.notebook_dir
288 if not nbm.notebook_exists(notebook_id):
288 if not nbm.notebook_exists(notebook_id):
289 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
289 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
290 template = self.application.jinja2_env.get_template('notebook.html')
290 template = self.application.jinja2_env.get_template('notebook.html')
291 self.write( template.render(project=project,
291 self.write( template.render(project=project,
292 notebook_id=notebook_id,
292 notebook_id=notebook_id,
293 base_project_url=self.application.ipython_app.base_project_url,
293 base_project_url=self.application.ipython_app.base_project_url,
294 base_kernel_url=self.application.ipython_app.base_kernel_url,
294 base_kernel_url=self.application.ipython_app.base_kernel_url,
295 kill_kernel=False,
295 kill_kernel=False,
296 read_only=self.read_only,
296 read_only=self.read_only,
297 logged_in=self.logged_in,
297 logged_in=self.logged_in,
298 login_available=self.login_available,
298 login_available=self.login_available,
299 mathjax_url=self.application.ipython_app.mathjax_url,))
299 mathjax_url=self.application.ipython_app.mathjax_url,))
300
300
301
301
302 class PrintNotebookHandler(AuthenticatedHandler):
302 class PrintNotebookHandler(AuthenticatedHandler):
303
303
304 @authenticate_unless_readonly
304 @authenticate_unless_readonly
305 def get(self, notebook_id):
305 def get(self, notebook_id):
306 nbm = self.application.notebook_manager
306 nbm = self.application.notebook_manager
307 project = nbm.notebook_dir
307 project = nbm.notebook_dir
308 if not nbm.notebook_exists(notebook_id):
308 if not nbm.notebook_exists(notebook_id):
309 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
309 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
310 template = self.application.jinja2_env.get_template('printnotebook.html')
310 template = self.application.jinja2_env.get_template('printnotebook.html')
311 self.write( template.render(
311 self.write( template.render(
312 project=project,
312 project=project,
313 notebook_id=notebook_id,
313 notebook_id=notebook_id,
314 base_project_url=self.application.ipython_app.base_project_url,
314 base_project_url=self.application.ipython_app.base_project_url,
315 base_kernel_url=self.application.ipython_app.base_kernel_url,
315 base_kernel_url=self.application.ipython_app.base_kernel_url,
316 kill_kernel=False,
316 kill_kernel=False,
317 read_only=self.read_only,
317 read_only=self.read_only,
318 logged_in=self.logged_in,
318 logged_in=self.logged_in,
319 login_available=self.login_available,
319 login_available=self.login_available,
320 mathjax_url=self.application.ipython_app.mathjax_url,
320 mathjax_url=self.application.ipython_app.mathjax_url,
321 ))
321 ))
322
322
323 #-----------------------------------------------------------------------------
323 #-----------------------------------------------------------------------------
324 # Kernel handlers
324 # Kernel handlers
325 #-----------------------------------------------------------------------------
325 #-----------------------------------------------------------------------------
326
326
327
327
328 class MainKernelHandler(AuthenticatedHandler):
328 class MainKernelHandler(AuthenticatedHandler):
329
329
330 @web.authenticated
330 @web.authenticated
331 def get(self):
331 def get(self):
332 km = self.application.kernel_manager
332 km = self.application.kernel_manager
333 self.finish(jsonapi.dumps(km.list_kernel_ids()))
333 self.finish(jsonapi.dumps(km.list_kernel_ids()))
334
334
335 @web.authenticated
335 @web.authenticated
336 def post(self):
336 def post(self):
337 km = self.application.kernel_manager
337 km = self.application.kernel_manager
338 nbm = self.application.notebook_manager
338 nbm = self.application.notebook_manager
339 notebook_id = self.get_argument('notebook', default=None)
339 notebook_id = self.get_argument('notebook', default=None)
340 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
340 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
341 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
341 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
342 self.set_header('Location', '/'+kernel_id)
342 self.set_header('Location', '/'+kernel_id)
343 self.finish(jsonapi.dumps(data))
343 self.finish(jsonapi.dumps(data))
344
344
345
345
346 class KernelHandler(AuthenticatedHandler):
346 class KernelHandler(AuthenticatedHandler):
347
347
348 SUPPORTED_METHODS = ('DELETE')
348 SUPPORTED_METHODS = ('DELETE')
349
349
350 @web.authenticated
350 @web.authenticated
351 def delete(self, kernel_id):
351 def delete(self, kernel_id):
352 km = self.application.kernel_manager
352 km = self.application.kernel_manager
353 km.shutdown_kernel(kernel_id)
353 km.shutdown_kernel(kernel_id)
354 self.set_status(204)
354 self.set_status(204)
355 self.finish()
355 self.finish()
356
356
357
357
358 class KernelActionHandler(AuthenticatedHandler):
358 class KernelActionHandler(AuthenticatedHandler):
359
359
360 @web.authenticated
360 @web.authenticated
361 def post(self, kernel_id, action):
361 def post(self, kernel_id, action):
362 km = self.application.kernel_manager
362 km = self.application.kernel_manager
363 if action == 'interrupt':
363 if action == 'interrupt':
364 km.interrupt_kernel(kernel_id)
364 km.interrupt_kernel(kernel_id)
365 self.set_status(204)
365 self.set_status(204)
366 if action == 'restart':
366 if action == 'restart':
367 km.restart_kernel(kernel_id)
367 km.restart_kernel(kernel_id)
368 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
368 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
369 self.set_header('Location', '/'+kernel_id)
369 self.set_header('Location', '/'+kernel_id)
370 self.write(jsonapi.dumps(data))
370 self.write(jsonapi.dumps(data))
371 self.finish()
371 self.finish()
372
372
373
373
374 class ZMQStreamHandler(websocket.WebSocketHandler):
374 class ZMQStreamHandler(websocket.WebSocketHandler):
375
375
376 def _reserialize_reply(self, msg_list):
376 def _reserialize_reply(self, msg_list):
377 """Reserialize a reply message using JSON.
377 """Reserialize a reply message using JSON.
378
378
379 This takes the msg list from the ZMQ socket, unserializes it using
379 This takes the msg list from the ZMQ socket, unserializes it using
380 self.session and then serializes the result using JSON. This method
380 self.session and then serializes the result using JSON. This method
381 should be used by self._on_zmq_reply to build messages that can
381 should be used by self._on_zmq_reply to build messages that can
382 be sent back to the browser.
382 be sent back to the browser.
383 """
383 """
384 idents, msg_list = self.session.feed_identities(msg_list)
384 idents, msg_list = self.session.feed_identities(msg_list)
385 msg = self.session.unserialize(msg_list)
385 msg = self.session.unserialize(msg_list)
386 try:
386 try:
387 msg['header'].pop('date')
387 msg['header'].pop('date')
388 except KeyError:
388 except KeyError:
389 pass
389 pass
390 try:
390 try:
391 msg['parent_header'].pop('date')
391 msg['parent_header'].pop('date')
392 except KeyError:
392 except KeyError:
393 pass
393 pass
394 msg.pop('buffers')
394 msg.pop('buffers')
395 return jsonapi.dumps(msg, default=date_default)
395 return jsonapi.dumps(msg, default=date_default)
396
396
397 def _on_zmq_reply(self, msg_list):
397 def _on_zmq_reply(self, msg_list):
398 try:
398 try:
399 msg = self._reserialize_reply(msg_list)
399 msg = self._reserialize_reply(msg_list)
400 except Exception:
400 except Exception:
401 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
401 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
402 else:
402 else:
403 self.write_message(msg)
403 self.write_message(msg)
404
404
405 def allow_draft76(self):
405 def allow_draft76(self):
406 """Allow draft 76, until browsers such as Safari update to RFC 6455.
406 """Allow draft 76, until browsers such as Safari update to RFC 6455.
407
407
408 This has been disabled by default in tornado in release 2.2.0, and
408 This has been disabled by default in tornado in release 2.2.0, and
409 support will be removed in later versions.
409 support will be removed in later versions.
410 """
410 """
411 return True
411 return True
412
412
413
413
414 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
414 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
415
415
416 def open(self, kernel_id):
416 def open(self, kernel_id):
417 self.kernel_id = kernel_id.decode('ascii')
417 self.kernel_id = kernel_id.decode('ascii')
418 try:
418 try:
419 cfg = self.application.config
419 cfg = self.application.config
420 except AttributeError:
420 except AttributeError:
421 # protect from the case where this is run from something other than
421 # protect from the case where this is run from something other than
422 # the notebook app:
422 # the notebook app:
423 cfg = None
423 cfg = None
424 self.session = Session(config=cfg)
424 self.session = Session(config=cfg)
425 self.save_on_message = self.on_message
425 self.save_on_message = self.on_message
426 self.on_message = self.on_first_message
426 self.on_message = self.on_first_message
427
427
428 def get_current_user(self):
428 def get_current_user(self):
429 user_id = self.get_secure_cookie(self.settings['cookie_name'])
429 user_id = self.get_secure_cookie(self.settings['cookie_name'])
430 if user_id == '' or (user_id is None and not self.application.password):
430 if user_id == '' or (user_id is None and not self.application.password):
431 user_id = 'anonymous'
431 user_id = 'anonymous'
432 return user_id
432 return user_id
433
433
434 def _inject_cookie_message(self, msg):
434 def _inject_cookie_message(self, msg):
435 """Inject the first message, which is the document cookie,
435 """Inject the first message, which is the document cookie,
436 for authentication."""
436 for authentication."""
437 if isinstance(msg, unicode):
437 if isinstance(msg, unicode):
438 # Cookie can't constructor doesn't accept unicode strings for some reason
438 # Cookie can't constructor doesn't accept unicode strings for some reason
439 msg = msg.encode('utf8', 'replace')
439 msg = msg.encode('utf8', 'replace')
440 try:
440 try:
441 self.request._cookies = Cookie.SimpleCookie(msg)
441 self.request._cookies = Cookie.SimpleCookie(msg)
442 except:
442 except:
443 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
443 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
444
444
445 def on_first_message(self, msg):
445 def on_first_message(self, msg):
446 self._inject_cookie_message(msg)
446 self._inject_cookie_message(msg)
447 if self.get_current_user() is None:
447 if self.get_current_user() is None:
448 logging.warn("Couldn't authenticate WebSocket connection")
448 logging.warn("Couldn't authenticate WebSocket connection")
449 raise web.HTTPError(403)
449 raise web.HTTPError(403)
450 self.on_message = self.save_on_message
450 self.on_message = self.save_on_message
451
451
452
452
453 class IOPubHandler(AuthenticatedZMQStreamHandler):
453 class IOPubHandler(AuthenticatedZMQStreamHandler):
454
454
455 def initialize(self, *args, **kwargs):
455 def initialize(self, *args, **kwargs):
456 self._kernel_alive = True
456 self._kernel_alive = True
457 self._beating = False
457 self._beating = False
458 self.iopub_stream = None
458 self.iopub_stream = None
459 self.hb_stream = None
459 self.hb_stream = None
460
460
461 def on_first_message(self, msg):
461 def on_first_message(self, msg):
462 try:
462 try:
463 super(IOPubHandler, self).on_first_message(msg)
463 super(IOPubHandler, self).on_first_message(msg)
464 except web.HTTPError:
464 except web.HTTPError:
465 self.close()
465 self.close()
466 return
466 return
467 km = self.application.kernel_manager
467 km = self.application.kernel_manager
468 self.time_to_dead = km.time_to_dead
468 self.time_to_dead = km.time_to_dead
469 self.first_beat = km.first_beat
469 self.first_beat = km.first_beat
470 kernel_id = self.kernel_id
470 kernel_id = self.kernel_id
471 try:
471 try:
472 self.iopub_stream = km.create_iopub_stream(kernel_id)
472 self.iopub_stream = km.create_iopub_stream(kernel_id)
473 self.hb_stream = km.create_hb_stream(kernel_id)
473 self.hb_stream = km.create_hb_stream(kernel_id)
474 except web.HTTPError:
474 except web.HTTPError:
475 # WebSockets don't response to traditional error codes so we
475 # WebSockets don't response to traditional error codes so we
476 # close the connection.
476 # close the connection.
477 if not self.stream.closed():
477 if not self.stream.closed():
478 self.stream.close()
478 self.stream.close()
479 self.close()
479 self.close()
480 else:
480 else:
481 self.iopub_stream.on_recv(self._on_zmq_reply)
481 self.iopub_stream.on_recv(self._on_zmq_reply)
482 self.start_hb(self.kernel_died)
482 self.start_hb(self.kernel_died)
483
483
484 def on_message(self, msg):
484 def on_message(self, msg):
485 pass
485 pass
486
486
487 def on_close(self):
487 def on_close(self):
488 # This method can be called twice, once by self.kernel_died and once
488 # This method can be called twice, once by self.kernel_died and once
489 # from the WebSocket close event. If the WebSocket connection is
489 # from the WebSocket close event. If the WebSocket connection is
490 # closed before the ZMQ streams are setup, they could be None.
490 # closed before the ZMQ streams are setup, they could be None.
491 self.stop_hb()
491 self.stop_hb()
492 if self.iopub_stream is not None and not self.iopub_stream.closed():
492 if self.iopub_stream is not None and not self.iopub_stream.closed():
493 self.iopub_stream.on_recv(None)
493 self.iopub_stream.on_recv(None)
494 self.iopub_stream.close()
494 self.iopub_stream.close()
495 if self.hb_stream is not None and not self.hb_stream.closed():
495 if self.hb_stream is not None and not self.hb_stream.closed():
496 self.hb_stream.close()
496 self.hb_stream.close()
497
497
498 def start_hb(self, callback):
498 def start_hb(self, callback):
499 """Start the heartbeating and call the callback if the kernel dies."""
499 """Start the heartbeating and call the callback if the kernel dies."""
500 if not self._beating:
500 if not self._beating:
501 self._kernel_alive = True
501 self._kernel_alive = True
502
502
503 def ping_or_dead():
503 def ping_or_dead():
504 self.hb_stream.flush()
504 self.hb_stream.flush()
505 if self._kernel_alive:
505 if self._kernel_alive:
506 self._kernel_alive = False
506 self._kernel_alive = False
507 self.hb_stream.send(b'ping')
507 self.hb_stream.send(b'ping')
508 # flush stream to force immediate socket send
508 # flush stream to force immediate socket send
509 self.hb_stream.flush()
509 self.hb_stream.flush()
510 else:
510 else:
511 try:
511 try:
512 callback()
512 callback()
513 except:
513 except:
514 pass
514 pass
515 finally:
515 finally:
516 self.stop_hb()
516 self.stop_hb()
517
517
518 def beat_received(msg):
518 def beat_received(msg):
519 self._kernel_alive = True
519 self._kernel_alive = True
520
520
521 self.hb_stream.on_recv(beat_received)
521 self.hb_stream.on_recv(beat_received)
522 loop = ioloop.IOLoop.instance()
522 loop = ioloop.IOLoop.instance()
523 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
523 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
524 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
524 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
525 self._beating= True
525 self._beating= True
526
526
527 def _really_start_hb(self):
527 def _really_start_hb(self):
528 """callback for delayed heartbeat start
528 """callback for delayed heartbeat start
529
529
530 Only start the hb loop if we haven't been closed during the wait.
530 Only start the hb loop if we haven't been closed during the wait.
531 """
531 """
532 if self._beating and not self.hb_stream.closed():
532 if self._beating and not self.hb_stream.closed():
533 self._hb_periodic_callback.start()
533 self._hb_periodic_callback.start()
534
534
535 def stop_hb(self):
535 def stop_hb(self):
536 """Stop the heartbeating and cancel all related callbacks."""
536 """Stop the heartbeating and cancel all related callbacks."""
537 if self._beating:
537 if self._beating:
538 self._beating = False
538 self._beating = False
539 self._hb_periodic_callback.stop()
539 self._hb_periodic_callback.stop()
540 if not self.hb_stream.closed():
540 if not self.hb_stream.closed():
541 self.hb_stream.on_recv(None)
541 self.hb_stream.on_recv(None)
542
542
543 def kernel_died(self):
543 def _delete_kernel_data(self):
544 """Remove the kernel data and notebook mapping."""
544 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
545 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
545 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
546
547 def kernel_died(self):
548 self._delete_kernel_data()
549 self.application.log.error("Kernel died: %s" % self.kernel_id)
546 self.write_message(
550 self.write_message(
547 {'header': {'msg_type': 'status'},
551 {'header': {'msg_type': 'status'},
548 'parent_header': {},
552 'parent_header': {},
549 'content': {'execution_state':'dead'}
553 'content': {'execution_state':'dead'}
550 }
554 }
551 )
555 )
552 self.on_close()
556 self.on_close()
553
557
554
558
555 class ShellHandler(AuthenticatedZMQStreamHandler):
559 class ShellHandler(AuthenticatedZMQStreamHandler):
556
560
557 def initialize(self, *args, **kwargs):
561 def initialize(self, *args, **kwargs):
558 self.shell_stream = None
562 self.shell_stream = None
559
563
560 def on_first_message(self, msg):
564 def on_first_message(self, msg):
561 try:
565 try:
562 super(ShellHandler, self).on_first_message(msg)
566 super(ShellHandler, self).on_first_message(msg)
563 except web.HTTPError:
567 except web.HTTPError:
564 self.close()
568 self.close()
565 return
569 return
566 km = self.application.kernel_manager
570 km = self.application.kernel_manager
567 self.max_msg_size = km.max_msg_size
571 self.max_msg_size = km.max_msg_size
568 kernel_id = self.kernel_id
572 kernel_id = self.kernel_id
569 try:
573 try:
570 self.shell_stream = km.create_shell_stream(kernel_id)
574 self.shell_stream = km.create_shell_stream(kernel_id)
571 except web.HTTPError:
575 except web.HTTPError:
572 # WebSockets don't response to traditional error codes so we
576 # WebSockets don't response to traditional error codes so we
573 # close the connection.
577 # close the connection.
574 if not self.stream.closed():
578 if not self.stream.closed():
575 self.stream.close()
579 self.stream.close()
576 self.close()
580 self.close()
577 else:
581 else:
578 self.shell_stream.on_recv(self._on_zmq_reply)
582 self.shell_stream.on_recv(self._on_zmq_reply)
579
583
580 def on_message(self, msg):
584 def on_message(self, msg):
581 if len(msg) < self.max_msg_size:
585 if len(msg) < self.max_msg_size:
582 msg = jsonapi.loads(msg)
586 msg = jsonapi.loads(msg)
583 self.session.send(self.shell_stream, msg)
587 self.session.send(self.shell_stream, msg)
584
588
585 def on_close(self):
589 def on_close(self):
586 # Make sure the stream exists and is not already closed.
590 # Make sure the stream exists and is not already closed.
587 if self.shell_stream is not None and not self.shell_stream.closed():
591 if self.shell_stream is not None and not self.shell_stream.closed():
588 self.shell_stream.close()
592 self.shell_stream.close()
589
593
590
594
591 #-----------------------------------------------------------------------------
595 #-----------------------------------------------------------------------------
592 # Notebook web service handlers
596 # Notebook web service handlers
593 #-----------------------------------------------------------------------------
597 #-----------------------------------------------------------------------------
594
598
595 class NotebookRootHandler(AuthenticatedHandler):
599 class NotebookRootHandler(AuthenticatedHandler):
596
600
597 @authenticate_unless_readonly
601 @authenticate_unless_readonly
598 def get(self):
602 def get(self):
599 nbm = self.application.notebook_manager
603 nbm = self.application.notebook_manager
600 km = self.application.kernel_manager
604 km = self.application.kernel_manager
601 files = nbm.list_notebooks()
605 files = nbm.list_notebooks()
602 for f in files :
606 for f in files :
603 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
607 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
604 self.finish(jsonapi.dumps(files))
608 self.finish(jsonapi.dumps(files))
605
609
606 @web.authenticated
610 @web.authenticated
607 def post(self):
611 def post(self):
608 nbm = self.application.notebook_manager
612 nbm = self.application.notebook_manager
609 body = self.request.body.strip()
613 body = self.request.body.strip()
610 format = self.get_argument('format', default='json')
614 format = self.get_argument('format', default='json')
611 name = self.get_argument('name', default=None)
615 name = self.get_argument('name', default=None)
612 if body:
616 if body:
613 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
617 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
614 else:
618 else:
615 notebook_id = nbm.new_notebook()
619 notebook_id = nbm.new_notebook()
616 self.set_header('Location', '/'+notebook_id)
620 self.set_header('Location', '/'+notebook_id)
617 self.finish(jsonapi.dumps(notebook_id))
621 self.finish(jsonapi.dumps(notebook_id))
618
622
619
623
620 class NotebookHandler(AuthenticatedHandler):
624 class NotebookHandler(AuthenticatedHandler):
621
625
622 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
626 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
623
627
624 @authenticate_unless_readonly
628 @authenticate_unless_readonly
625 def get(self, notebook_id):
629 def get(self, notebook_id):
626 nbm = self.application.notebook_manager
630 nbm = self.application.notebook_manager
627 format = self.get_argument('format', default='json')
631 format = self.get_argument('format', default='json')
628 last_mod, name, data = nbm.get_notebook(notebook_id, format)
632 last_mod, name, data = nbm.get_notebook(notebook_id, format)
629
633
630 if format == u'json':
634 if format == u'json':
631 self.set_header('Content-Type', 'application/json')
635 self.set_header('Content-Type', 'application/json')
632 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
636 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
633 elif format == u'py':
637 elif format == u'py':
634 self.set_header('Content-Type', 'application/x-python')
638 self.set_header('Content-Type', 'application/x-python')
635 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
639 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
636 self.set_header('Last-Modified', last_mod)
640 self.set_header('Last-Modified', last_mod)
637 self.finish(data)
641 self.finish(data)
638
642
639 @web.authenticated
643 @web.authenticated
640 def put(self, notebook_id):
644 def put(self, notebook_id):
641 nbm = self.application.notebook_manager
645 nbm = self.application.notebook_manager
642 format = self.get_argument('format', default='json')
646 format = self.get_argument('format', default='json')
643 name = self.get_argument('name', default=None)
647 name = self.get_argument('name', default=None)
644 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
648 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
645 self.set_status(204)
649 self.set_status(204)
646 self.finish()
650 self.finish()
647
651
648 @web.authenticated
652 @web.authenticated
649 def delete(self, notebook_id):
653 def delete(self, notebook_id):
650 nbm = self.application.notebook_manager
654 nbm = self.application.notebook_manager
651 nbm.delete_notebook(notebook_id)
655 nbm.delete_notebook(notebook_id)
652 self.set_status(204)
656 self.set_status(204)
653 self.finish()
657 self.finish()
654
658
655
659
656 class NotebookCopyHandler(AuthenticatedHandler):
660 class NotebookCopyHandler(AuthenticatedHandler):
657
661
658 @web.authenticated
662 @web.authenticated
659 def get(self, notebook_id):
663 def get(self, notebook_id):
660 nbm = self.application.notebook_manager
664 nbm = self.application.notebook_manager
661 project = nbm.notebook_dir
665 project = nbm.notebook_dir
662 notebook_id = nbm.copy_notebook(notebook_id)
666 notebook_id = nbm.copy_notebook(notebook_id)
663 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
667 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
664
668
665
669
666 #-----------------------------------------------------------------------------
670 #-----------------------------------------------------------------------------
667 # Cluster handlers
671 # Cluster handlers
668 #-----------------------------------------------------------------------------
672 #-----------------------------------------------------------------------------
669
673
670
674
671 class MainClusterHandler(AuthenticatedHandler):
675 class MainClusterHandler(AuthenticatedHandler):
672
676
673 @web.authenticated
677 @web.authenticated
674 def get(self):
678 def get(self):
675 cm = self.application.cluster_manager
679 cm = self.application.cluster_manager
676 self.finish(jsonapi.dumps(cm.list_profiles()))
680 self.finish(jsonapi.dumps(cm.list_profiles()))
677
681
678
682
679 class ClusterProfileHandler(AuthenticatedHandler):
683 class ClusterProfileHandler(AuthenticatedHandler):
680
684
681 @web.authenticated
685 @web.authenticated
682 def get(self, profile):
686 def get(self, profile):
683 cm = self.application.cluster_manager
687 cm = self.application.cluster_manager
684 self.finish(jsonapi.dumps(cm.profile_info(profile)))
688 self.finish(jsonapi.dumps(cm.profile_info(profile)))
685
689
686
690
687 class ClusterActionHandler(AuthenticatedHandler):
691 class ClusterActionHandler(AuthenticatedHandler):
688
692
689 @web.authenticated
693 @web.authenticated
690 def post(self, profile, action):
694 def post(self, profile, action):
691 cm = self.application.cluster_manager
695 cm = self.application.cluster_manager
692 if action == 'start':
696 if action == 'start':
693 n = self.get_argument('n',default=None)
697 n = self.get_argument('n',default=None)
694 if n is None:
698 if n is None:
695 data = cm.start_cluster(profile)
699 data = cm.start_cluster(profile)
696 else:
700 else:
697 data = cm.start_cluster(profile,int(n))
701 data = cm.start_cluster(profile,int(n))
698 if action == 'stop':
702 if action == 'stop':
699 data = cm.stop_cluster(profile)
703 data = cm.stop_cluster(profile)
700 self.finish(jsonapi.dumps(data))
704 self.finish(jsonapi.dumps(data))
701
705
702
706
703 #-----------------------------------------------------------------------------
707 #-----------------------------------------------------------------------------
704 # RST web service handlers
708 # RST web service handlers
705 #-----------------------------------------------------------------------------
709 #-----------------------------------------------------------------------------
706
710
707
711
708 class RSTHandler(AuthenticatedHandler):
712 class RSTHandler(AuthenticatedHandler):
709
713
710 @web.authenticated
714 @web.authenticated
711 def post(self):
715 def post(self):
712 if publish_string is None:
716 if publish_string is None:
713 raise web.HTTPError(503, u'docutils not available')
717 raise web.HTTPError(503, u'docutils not available')
714 body = self.request.body.strip()
718 body = self.request.body.strip()
715 source = body
719 source = body
716 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
720 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
717 defaults = {'file_insertion_enabled': 0,
721 defaults = {'file_insertion_enabled': 0,
718 'raw_enabled': 0,
722 'raw_enabled': 0,
719 '_disable_config': 1,
723 '_disable_config': 1,
720 'stylesheet_path': 0
724 'stylesheet_path': 0
721 # 'template': template_path
725 # 'template': template_path
722 }
726 }
723 try:
727 try:
724 html = publish_string(source, writer_name='html',
728 html = publish_string(source, writer_name='html',
725 settings_overrides=defaults
729 settings_overrides=defaults
726 )
730 )
727 except:
731 except:
728 raise web.HTTPError(400, u'Invalid RST')
732 raise web.HTTPError(400, u'Invalid RST')
729 print html
733 print html
730 self.set_header('Content-Type', 'text/html')
734 self.set_header('Content-Type', 'text/html')
731 self.finish(html)
735 self.finish(html)
732
736
733 # to minimize subclass changes:
737 # to minimize subclass changes:
734 HTTPError = web.HTTPError
738 HTTPError = web.HTTPError
735
739
736 class FileFindHandler(web.StaticFileHandler):
740 class FileFindHandler(web.StaticFileHandler):
737 """subclass of StaticFileHandler for serving files from a search path"""
741 """subclass of StaticFileHandler for serving files from a search path"""
738
742
739 _static_paths = {}
743 _static_paths = {}
740 # _lock is needed for tornado < 2.2.0 compat
744 # _lock is needed for tornado < 2.2.0 compat
741 _lock = threading.Lock() # protects _static_hashes
745 _lock = threading.Lock() # protects _static_hashes
742
746
743 def initialize(self, path, default_filename=None):
747 def initialize(self, path, default_filename=None):
744 if isinstance(path, basestring):
748 if isinstance(path, basestring):
745 path = [path]
749 path = [path]
746 self.roots = tuple(
750 self.roots = tuple(
747 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
751 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
748 )
752 )
749 self.default_filename = default_filename
753 self.default_filename = default_filename
750
754
751 @classmethod
755 @classmethod
752 def locate_file(cls, path, roots):
756 def locate_file(cls, path, roots):
753 """locate a file to serve on our static file search path"""
757 """locate a file to serve on our static file search path"""
754 with cls._lock:
758 with cls._lock:
755 if path in cls._static_paths:
759 if path in cls._static_paths:
756 return cls._static_paths[path]
760 return cls._static_paths[path]
757 try:
761 try:
758 abspath = os.path.abspath(filefind(path, roots))
762 abspath = os.path.abspath(filefind(path, roots))
759 except IOError:
763 except IOError:
760 # empty string should always give exists=False
764 # empty string should always give exists=False
761 return ''
765 return ''
762
766
763 # os.path.abspath strips a trailing /
767 # os.path.abspath strips a trailing /
764 # it needs to be temporarily added back for requests to root/
768 # it needs to be temporarily added back for requests to root/
765 if not (abspath + os.path.sep).startswith(roots):
769 if not (abspath + os.path.sep).startswith(roots):
766 raise HTTPError(403, "%s is not in root static directory", path)
770 raise HTTPError(403, "%s is not in root static directory", path)
767
771
768 cls._static_paths[path] = abspath
772 cls._static_paths[path] = abspath
769 return abspath
773 return abspath
770
774
771 def get(self, path, include_body=True):
775 def get(self, path, include_body=True):
772 path = self.parse_url_path(path)
776 path = self.parse_url_path(path)
773
777
774 # begin subclass override
778 # begin subclass override
775 abspath = self.locate_file(path, self.roots)
779 abspath = self.locate_file(path, self.roots)
776 # end subclass override
780 # end subclass override
777
781
778 if os.path.isdir(abspath) and self.default_filename is not None:
782 if os.path.isdir(abspath) and self.default_filename is not None:
779 # need to look at the request.path here for when path is empty
783 # need to look at the request.path here for when path is empty
780 # but there is some prefix to the path that was already
784 # but there is some prefix to the path that was already
781 # trimmed by the routing
785 # trimmed by the routing
782 if not self.request.path.endswith("/"):
786 if not self.request.path.endswith("/"):
783 self.redirect(self.request.path + "/")
787 self.redirect(self.request.path + "/")
784 return
788 return
785 abspath = os.path.join(abspath, self.default_filename)
789 abspath = os.path.join(abspath, self.default_filename)
786 if not os.path.exists(abspath):
790 if not os.path.exists(abspath):
787 raise HTTPError(404)
791 raise HTTPError(404)
788 if not os.path.isfile(abspath):
792 if not os.path.isfile(abspath):
789 raise HTTPError(403, "%s is not a file", path)
793 raise HTTPError(403, "%s is not a file", path)
790
794
791 stat_result = os.stat(abspath)
795 stat_result = os.stat(abspath)
792 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
796 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
793
797
794 self.set_header("Last-Modified", modified)
798 self.set_header("Last-Modified", modified)
795
799
796 mime_type, encoding = mimetypes.guess_type(abspath)
800 mime_type, encoding = mimetypes.guess_type(abspath)
797 if mime_type:
801 if mime_type:
798 self.set_header("Content-Type", mime_type)
802 self.set_header("Content-Type", mime_type)
799
803
800 cache_time = self.get_cache_time(path, modified, mime_type)
804 cache_time = self.get_cache_time(path, modified, mime_type)
801
805
802 if cache_time > 0:
806 if cache_time > 0:
803 self.set_header("Expires", datetime.datetime.utcnow() + \
807 self.set_header("Expires", datetime.datetime.utcnow() + \
804 datetime.timedelta(seconds=cache_time))
808 datetime.timedelta(seconds=cache_time))
805 self.set_header("Cache-Control", "max-age=" + str(cache_time))
809 self.set_header("Cache-Control", "max-age=" + str(cache_time))
806 else:
810 else:
807 self.set_header("Cache-Control", "public")
811 self.set_header("Cache-Control", "public")
808
812
809 self.set_extra_headers(path)
813 self.set_extra_headers(path)
810
814
811 # Check the If-Modified-Since, and don't send the result if the
815 # Check the If-Modified-Since, and don't send the result if the
812 # content has not been modified
816 # content has not been modified
813 ims_value = self.request.headers.get("If-Modified-Since")
817 ims_value = self.request.headers.get("If-Modified-Since")
814 if ims_value is not None:
818 if ims_value is not None:
815 date_tuple = email.utils.parsedate(ims_value)
819 date_tuple = email.utils.parsedate(ims_value)
816 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
820 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
817 if if_since >= modified:
821 if if_since >= modified:
818 self.set_status(304)
822 self.set_status(304)
819 return
823 return
820
824
821 with open(abspath, "rb") as file:
825 with open(abspath, "rb") as file:
822 data = file.read()
826 data = file.read()
823 hasher = hashlib.sha1()
827 hasher = hashlib.sha1()
824 hasher.update(data)
828 hasher.update(data)
825 self.set_header("Etag", '"%s"' % hasher.hexdigest())
829 self.set_header("Etag", '"%s"' % hasher.hexdigest())
826 if include_body:
830 if include_body:
827 self.write(data)
831 self.write(data)
828 else:
832 else:
829 assert self.request.method == "HEAD"
833 assert self.request.method == "HEAD"
830 self.set_header("Content-Length", len(data))
834 self.set_header("Content-Length", len(data))
831
835
832 @classmethod
836 @classmethod
833 def get_version(cls, settings, path):
837 def get_version(cls, settings, path):
834 """Generate the version string to be used in static URLs.
838 """Generate the version string to be used in static URLs.
835
839
836 This method may be overridden in subclasses (but note that it
840 This method may be overridden in subclasses (but note that it
837 is a class method rather than a static method). The default
841 is a class method rather than a static method). The default
838 implementation uses a hash of the file's contents.
842 implementation uses a hash of the file's contents.
839
843
840 ``settings`` is the `Application.settings` dictionary and ``path``
844 ``settings`` is the `Application.settings` dictionary and ``path``
841 is the relative location of the requested asset on the filesystem.
845 is the relative location of the requested asset on the filesystem.
842 The returned value should be a string, or ``None`` if no version
846 The returned value should be a string, or ``None`` if no version
843 could be determined.
847 could be determined.
844 """
848 """
845 # begin subclass override:
849 # begin subclass override:
846 static_paths = settings['static_path']
850 static_paths = settings['static_path']
847 if isinstance(static_paths, basestring):
851 if isinstance(static_paths, basestring):
848 static_paths = [static_paths]
852 static_paths = [static_paths]
849 roots = tuple(
853 roots = tuple(
850 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
854 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
851 )
855 )
852
856
853 try:
857 try:
854 abs_path = filefind(path, roots)
858 abs_path = filefind(path, roots)
855 except IOError:
859 except IOError:
856 logging.error("Could not find static file %r", path)
860 logging.error("Could not find static file %r", path)
857 return None
861 return None
858
862
859 # end subclass override
863 # end subclass override
860
864
861 with cls._lock:
865 with cls._lock:
862 hashes = cls._static_hashes
866 hashes = cls._static_hashes
863 if abs_path not in hashes:
867 if abs_path not in hashes:
864 try:
868 try:
865 f = open(abs_path, "rb")
869 f = open(abs_path, "rb")
866 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
870 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
867 f.close()
871 f.close()
868 except Exception:
872 except Exception:
869 logging.error("Could not open static file %r", path)
873 logging.error("Could not open static file %r", path)
870 hashes[abs_path] = None
874 hashes[abs_path] = None
871 hsh = hashes.get(abs_path)
875 hsh = hashes.get(abs_path)
872 if hsh:
876 if hsh:
873 return hsh[:5]
877 return hsh[:5]
874 return None
878 return None
875
879
876
880
877 def parse_url_path(self, url_path):
881 def parse_url_path(self, url_path):
878 """Converts a static URL path into a filesystem path.
882 """Converts a static URL path into a filesystem path.
879
883
880 ``url_path`` is the path component of the URL with
884 ``url_path`` is the path component of the URL with
881 ``static_url_prefix`` removed. The return value should be
885 ``static_url_prefix`` removed. The return value should be
882 filesystem path relative to ``static_path``.
886 filesystem path relative to ``static_path``.
883 """
887 """
884 if os.path.sep != "/":
888 if os.path.sep != "/":
885 url_path = url_path.replace("/", os.path.sep)
889 url_path = url_path.replace("/", os.path.sep)
886 return url_path
890 return url_path
887
891
888
892
General Comments 0
You need to be logged in to leave comments. Login now