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