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