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