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