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