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