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