##// END OF EJS Templates
update custom auth per review...
Min RK -
Show More
@@ -1,86 +1,70 b''
1 """Tornado handlers logging into the notebook.
1 """Tornado handlers for logging into the notebook."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Brian Granger
6 * Phil Elson
7 """
8
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2014 The IPython Development Team
11 #
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19
5
20 import uuid
6 import uuid
21
7
22 from tornado.escape import url_escape
8 from tornado.escape import url_escape
23 from tornado import web
24
9
25 from IPython.config.configurable import Configurable
26 from IPython.lib.security import passwd_check
10 from IPython.lib.security import passwd_check
27
11
28 from ..base.handlers import IPythonHandler
12 from ..base.handlers import IPythonHandler
29
13
30 #-----------------------------------------------------------------------------
31 # Handler
32 #-----------------------------------------------------------------------------
33
14
34 class LoginHandler(IPythonHandler):
15 class LoginHandler(IPythonHandler):
35 """ The basic IPythonWebApplication login handler which authenticates with a
16 """The basic tornado login handler
36 hashed password from the configuration.
37
17
18 authenticates with a hashed password from the configuration.
38 """
19 """
39 def _render(self, message=None):
20 def _render(self, message=None):
40 self.write(self.render_template('login.html',
21 self.write(self.render_template('login.html',
41 next=url_escape(self.get_argument('next', default=self.base_url)),
22 next=url_escape(self.get_argument('next', default=self.base_url)),
42 message=message,
23 message=message,
43 ))
24 ))
44
25
45 def get(self):
26 def get(self):
46 if self.current_user:
27 if self.current_user:
47 self.redirect(self.get_argument('next', default=self.base_url))
28 self.redirect(self.get_argument('next', default=self.base_url))
48 else:
29 else:
49 self._render()
30 self._render()
50
31
32 @property
33 def hashed_password(self):
34 return self.password_from_settings(self.settings)
35
51 def post(self):
36 def post(self):
52 hashed_password = self.password_from_configuration(self.application)
53 typed_password = self.get_argument('password', default=u'')
37 typed_password = self.get_argument('password', default=u'')
54 if self.login_available(self.application):
38 if self.login_available(self.settings):
55 if passwd_check(hashed_password, typed_password):
39 if passwd_check(self.hashed_password, typed_password):
56 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
40 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
57 else:
41 else:
58 self._render(message={'error': 'Invalid password'})
42 self._render(message={'error': 'Invalid password'})
59 return
43 return
60
44
61 self.redirect(self.get_argument('next', default=self.base_url))
45 self.redirect(self.get_argument('next', default=self.base_url))
62
46
63 @classmethod
47 @classmethod
64 def validate_notebook_app_security(cls, notebook_app, ssl_options=None):
48 def validate_notebook_app_security(cls, notebook_app, ssl_options=None):
65 if not notebook_app.ip:
49 if not notebook_app.ip:
66 warning = "WARNING: The notebook server is listening on all IP addresses"
50 warning = "WARNING: The notebook server is listening on all IP addresses"
67 if ssl_options is None:
51 if ssl_options is None:
68 notebook_app.log.critical(warning + " and not using encryption. This "
52 notebook_app.log.critical(warning + " and not using encryption. This "
69 "is not recommended.")
53 "is not recommended.")
70 if not self.password_from_configuration(notebook_app):
54 if not notebook_app.password:
71 notebook_app.log.critical(warning + " and not using authentication. "
55 notebook_app.log.critical(warning + " and not using authentication. "
72 "This is highly insecure and not recommended.")
56 "This is highly insecure and not recommended.")
73
57
74 @staticmethod
58 @staticmethod
75 def password_from_configuration(webapp):
59 def password_from_settings(settings):
76 """ Return the hashed password from the given NotebookWebApplication's configuration.
60 """Return the hashed password from the tornado settings.
77
78 If there is no configured password, None will be returned.
79
61
62 If there is no configured password, an empty string will be returned.
80 """
63 """
81 return webapp.settings['config']['NotebookApp'].get('password', None)
64 return settings.get('password', u'')
82
65
83 @classmethod
66 @classmethod
84 def login_available(cls, webapp):
67 def login_available(cls, settings):
85 """Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
68 """Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
86 return bool(cls.password_from_configuration(webapp))
69 return bool(cls.password_from_settings(settings))
70
@@ -1,514 +1,512 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 import IPython
27 import IPython
28 from IPython.utils.sysinfo import get_sys_info
28 from IPython.utils.sysinfo import get_sys_info
29
29
30 from IPython.config import Application
30 from IPython.config import Application
31 from IPython.utils.path import filefind
31 from IPython.utils.path import filefind
32 from IPython.utils.py3compat import string_types
32 from IPython.utils.py3compat import string_types
33 from IPython.html.utils import is_hidden, url_path_join, url_escape
33 from IPython.html.utils import is_hidden, url_path_join, url_escape
34
34
35 from IPython.html.services.security import csp_report_uri
35 from IPython.html.services.security import csp_report_uri
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Top-level handlers
38 # Top-level handlers
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
41
41
42 sys_info = json.dumps(get_sys_info())
42 sys_info = json.dumps(get_sys_info())
43
43
44 class AuthenticatedHandler(web.RequestHandler):
44 class AuthenticatedHandler(web.RequestHandler):
45 """A RequestHandler with an authenticated user."""
45 """A RequestHandler with an authenticated user."""
46
46
47 def set_default_headers(self):
47 def set_default_headers(self):
48 headers = self.settings.get('headers', {})
48 headers = self.settings.get('headers', {})
49
49
50 if "Content-Security-Policy" not in headers:
50 if "Content-Security-Policy" not in headers:
51 headers["Content-Security-Policy"] = (
51 headers["Content-Security-Policy"] = (
52 "frame-ancestors 'self'; "
52 "frame-ancestors 'self'; "
53 # Make sure the report-uri is relative to the base_url
53 # Make sure the report-uri is relative to the base_url
54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
55 )
55 )
56
56
57 # Allow for overriding headers
57 # Allow for overriding headers
58 for header_name,value in headers.items() :
58 for header_name,value in headers.items() :
59 try:
59 try:
60 self.set_header(header_name, value)
60 self.set_header(header_name, value)
61 except Exception as e:
61 except Exception as e:
62 # tornado raise Exception (not a subclass)
62 # tornado raise Exception (not a subclass)
63 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # if method is unsupported (websocket and Access-Control-Allow-Origin
64 # for example, so just ignore)
64 # for example, so just ignore)
65 self.log.debug(e)
65 self.log.debug(e)
66
66
67 def clear_login_cookie(self):
67 def clear_login_cookie(self):
68 self.clear_cookie(self.cookie_name)
68 self.clear_cookie(self.cookie_name)
69
69
70 def get_current_user(self):
70 def get_current_user(self):
71 user_id = self.get_secure_cookie(self.cookie_name)
71 user_id = self.get_secure_cookie(self.cookie_name)
72 # For now the user_id should not return empty, but it could eventually
72 # For now the user_id should not return empty, but it could eventually
73 if user_id == '':
73 if user_id == '':
74 user_id = 'anonymous'
74 user_id = 'anonymous'
75 if user_id is None:
75 if user_id is None:
76 # prevent extra Invalid cookie sig warnings:
76 # prevent extra Invalid cookie sig warnings:
77 self.clear_login_cookie()
77 self.clear_login_cookie()
78 if not self.login_available:
78 if not self.login_available:
79 user_id = 'anonymous'
79 user_id = 'anonymous'
80 return user_id
80 return user_id
81
81
82 @property
82 @property
83 def cookie_name(self):
83 def cookie_name(self):
84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
85 self.request.host
85 self.request.host
86 ))
86 ))
87 return self.settings.get('cookie_name', default_cookie_name)
87 return self.settings.get('cookie_name', default_cookie_name)
88
88
89 @property
89 @property
90 def logged_in(self):
90 def logged_in(self):
91 """Is a user currently logged in?
91 """Is a user currently logged in?"""
92
93 """
94 user = self.get_current_user()
92 user = self.get_current_user()
95 return (user and not user == 'anonymous')
93 return (user and not user == 'anonymous')
96
94
97 @property
95 @property
98 def _login_handler(self):
96 def login_handler(self):
99 """Return the login handler for this application."""
97 """Return the login handler for this application."""
100 return self.settings['login_handler_class']
98 return self.settings['login_handler_class']
101
99
102 @property
100 @property
103 def login_available(self):
101 def login_available(self):
104 """May a user proceed to log in?
102 """May a user proceed to log in?
105
103
106 This returns True if login capability is available, irrespective of
104 This returns True if login capability is available, irrespective of
107 whether the user is already logged in or not.
105 whether the user is already logged in or not.
108
106
109 """
107 """
110 return bool(self._login_handler.login_available(self.application))
108 return bool(self.login_handler.login_available(self.settings))
111
109
112
110
113 class IPythonHandler(AuthenticatedHandler):
111 class IPythonHandler(AuthenticatedHandler):
114 """IPython-specific extensions to authenticated handling
112 """IPython-specific extensions to authenticated handling
115
113
116 Mostly property shortcuts to IPython-specific settings.
114 Mostly property shortcuts to IPython-specific settings.
117 """
115 """
118
116
119 @property
117 @property
120 def config(self):
118 def config(self):
121 return self.settings.get('config', None)
119 return self.settings.get('config', None)
122
120
123 @property
121 @property
124 def log(self):
122 def log(self):
125 """use the IPython log by default, falling back on tornado's logger"""
123 """use the IPython log by default, falling back on tornado's logger"""
126 if Application.initialized():
124 if Application.initialized():
127 return Application.instance().log
125 return Application.instance().log
128 else:
126 else:
129 return app_log
127 return app_log
130
128
131 #---------------------------------------------------------------
129 #---------------------------------------------------------------
132 # URLs
130 # URLs
133 #---------------------------------------------------------------
131 #---------------------------------------------------------------
134
132
135 @property
133 @property
136 def version_hash(self):
134 def version_hash(self):
137 """The version hash to use for cache hints for static files"""
135 """The version hash to use for cache hints for static files"""
138 return self.settings.get('version_hash', '')
136 return self.settings.get('version_hash', '')
139
137
140 @property
138 @property
141 def mathjax_url(self):
139 def mathjax_url(self):
142 return self.settings.get('mathjax_url', '')
140 return self.settings.get('mathjax_url', '')
143
141
144 @property
142 @property
145 def base_url(self):
143 def base_url(self):
146 return self.settings.get('base_url', '/')
144 return self.settings.get('base_url', '/')
147
145
148 @property
146 @property
149 def ws_url(self):
147 def ws_url(self):
150 return self.settings.get('websocket_url', '')
148 return self.settings.get('websocket_url', '')
151
149
152 @property
150 @property
153 def contents_js_source(self):
151 def contents_js_source(self):
154 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
152 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
155 'services/contents'))
153 'services/contents'))
156 return self.settings.get('contents_js_source', 'services/contents')
154 return self.settings.get('contents_js_source', 'services/contents')
157
155
158 #---------------------------------------------------------------
156 #---------------------------------------------------------------
159 # Manager objects
157 # Manager objects
160 #---------------------------------------------------------------
158 #---------------------------------------------------------------
161
159
162 @property
160 @property
163 def kernel_manager(self):
161 def kernel_manager(self):
164 return self.settings['kernel_manager']
162 return self.settings['kernel_manager']
165
163
166 @property
164 @property
167 def contents_manager(self):
165 def contents_manager(self):
168 return self.settings['contents_manager']
166 return self.settings['contents_manager']
169
167
170 @property
168 @property
171 def cluster_manager(self):
169 def cluster_manager(self):
172 return self.settings['cluster_manager']
170 return self.settings['cluster_manager']
173
171
174 @property
172 @property
175 def session_manager(self):
173 def session_manager(self):
176 return self.settings['session_manager']
174 return self.settings['session_manager']
177
175
178 @property
176 @property
179 def terminal_manager(self):
177 def terminal_manager(self):
180 return self.settings['terminal_manager']
178 return self.settings['terminal_manager']
181
179
182 @property
180 @property
183 def kernel_spec_manager(self):
181 def kernel_spec_manager(self):
184 return self.settings['kernel_spec_manager']
182 return self.settings['kernel_spec_manager']
185
183
186 @property
184 @property
187 def config_manager(self):
185 def config_manager(self):
188 return self.settings['config_manager']
186 return self.settings['config_manager']
189
187
190 #---------------------------------------------------------------
188 #---------------------------------------------------------------
191 # CORS
189 # CORS
192 #---------------------------------------------------------------
190 #---------------------------------------------------------------
193
191
194 @property
192 @property
195 def allow_origin(self):
193 def allow_origin(self):
196 """Normal Access-Control-Allow-Origin"""
194 """Normal Access-Control-Allow-Origin"""
197 return self.settings.get('allow_origin', '')
195 return self.settings.get('allow_origin', '')
198
196
199 @property
197 @property
200 def allow_origin_pat(self):
198 def allow_origin_pat(self):
201 """Regular expression version of allow_origin"""
199 """Regular expression version of allow_origin"""
202 return self.settings.get('allow_origin_pat', None)
200 return self.settings.get('allow_origin_pat', None)
203
201
204 @property
202 @property
205 def allow_credentials(self):
203 def allow_credentials(self):
206 """Whether to set Access-Control-Allow-Credentials"""
204 """Whether to set Access-Control-Allow-Credentials"""
207 return self.settings.get('allow_credentials', False)
205 return self.settings.get('allow_credentials', False)
208
206
209 def set_default_headers(self):
207 def set_default_headers(self):
210 """Add CORS headers, if defined"""
208 """Add CORS headers, if defined"""
211 super(IPythonHandler, self).set_default_headers()
209 super(IPythonHandler, self).set_default_headers()
212 if self.allow_origin:
210 if self.allow_origin:
213 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
211 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
214 elif self.allow_origin_pat:
212 elif self.allow_origin_pat:
215 origin = self.get_origin()
213 origin = self.get_origin()
216 if origin and self.allow_origin_pat.match(origin):
214 if origin and self.allow_origin_pat.match(origin):
217 self.set_header("Access-Control-Allow-Origin", origin)
215 self.set_header("Access-Control-Allow-Origin", origin)
218 if self.allow_credentials:
216 if self.allow_credentials:
219 self.set_header("Access-Control-Allow-Credentials", 'true')
217 self.set_header("Access-Control-Allow-Credentials", 'true')
220
218
221 def get_origin(self):
219 def get_origin(self):
222 # Handle WebSocket Origin naming convention differences
220 # Handle WebSocket Origin naming convention differences
223 # The difference between version 8 and 13 is that in 8 the
221 # The difference between version 8 and 13 is that in 8 the
224 # client sends a "Sec-Websocket-Origin" header and in 13 it's
222 # client sends a "Sec-Websocket-Origin" header and in 13 it's
225 # simply "Origin".
223 # simply "Origin".
226 if "Origin" in self.request.headers:
224 if "Origin" in self.request.headers:
227 origin = self.request.headers.get("Origin")
225 origin = self.request.headers.get("Origin")
228 else:
226 else:
229 origin = self.request.headers.get("Sec-Websocket-Origin", None)
227 origin = self.request.headers.get("Sec-Websocket-Origin", None)
230 return origin
228 return origin
231
229
232 #---------------------------------------------------------------
230 #---------------------------------------------------------------
233 # template rendering
231 # template rendering
234 #---------------------------------------------------------------
232 #---------------------------------------------------------------
235
233
236 def get_template(self, name):
234 def get_template(self, name):
237 """Return the jinja template object for a given name"""
235 """Return the jinja template object for a given name"""
238 return self.settings['jinja2_env'].get_template(name)
236 return self.settings['jinja2_env'].get_template(name)
239
237
240 def render_template(self, name, **ns):
238 def render_template(self, name, **ns):
241 ns.update(self.template_namespace)
239 ns.update(self.template_namespace)
242 template = self.get_template(name)
240 template = self.get_template(name)
243 return template.render(**ns)
241 return template.render(**ns)
244
242
245 @property
243 @property
246 def template_namespace(self):
244 def template_namespace(self):
247 return dict(
245 return dict(
248 base_url=self.base_url,
246 base_url=self.base_url,
249 ws_url=self.ws_url,
247 ws_url=self.ws_url,
250 logged_in=self.logged_in,
248 logged_in=self.logged_in,
251 login_available=self.login_available,
249 login_available=self.login_available,
252 static_url=self.static_url,
250 static_url=self.static_url,
253 sys_info=sys_info,
251 sys_info=sys_info,
254 contents_js_source=self.contents_js_source,
252 contents_js_source=self.contents_js_source,
255 version_hash=self.version_hash,
253 version_hash=self.version_hash,
256 )
254 )
257
255
258 def get_json_body(self):
256 def get_json_body(self):
259 """Return the body of the request as JSON data."""
257 """Return the body of the request as JSON data."""
260 if not self.request.body:
258 if not self.request.body:
261 return None
259 return None
262 # Do we need to call body.decode('utf-8') here?
260 # Do we need to call body.decode('utf-8') here?
263 body = self.request.body.strip().decode(u'utf-8')
261 body = self.request.body.strip().decode(u'utf-8')
264 try:
262 try:
265 model = json.loads(body)
263 model = json.loads(body)
266 except Exception:
264 except Exception:
267 self.log.debug("Bad JSON: %r", body)
265 self.log.debug("Bad JSON: %r", body)
268 self.log.error("Couldn't parse JSON", exc_info=True)
266 self.log.error("Couldn't parse JSON", exc_info=True)
269 raise web.HTTPError(400, u'Invalid JSON in body of request')
267 raise web.HTTPError(400, u'Invalid JSON in body of request')
270 return model
268 return model
271
269
272 def write_error(self, status_code, **kwargs):
270 def write_error(self, status_code, **kwargs):
273 """render custom error pages"""
271 """render custom error pages"""
274 exc_info = kwargs.get('exc_info')
272 exc_info = kwargs.get('exc_info')
275 message = ''
273 message = ''
276 status_message = responses.get(status_code, 'Unknown HTTP Error')
274 status_message = responses.get(status_code, 'Unknown HTTP Error')
277 if exc_info:
275 if exc_info:
278 exception = exc_info[1]
276 exception = exc_info[1]
279 # get the custom message, if defined
277 # get the custom message, if defined
280 try:
278 try:
281 message = exception.log_message % exception.args
279 message = exception.log_message % exception.args
282 except Exception:
280 except Exception:
283 pass
281 pass
284
282
285 # construct the custom reason, if defined
283 # construct the custom reason, if defined
286 reason = getattr(exception, 'reason', '')
284 reason = getattr(exception, 'reason', '')
287 if reason:
285 if reason:
288 status_message = reason
286 status_message = reason
289
287
290 # build template namespace
288 # build template namespace
291 ns = dict(
289 ns = dict(
292 status_code=status_code,
290 status_code=status_code,
293 status_message=status_message,
291 status_message=status_message,
294 message=message,
292 message=message,
295 exception=exception,
293 exception=exception,
296 )
294 )
297
295
298 self.set_header('Content-Type', 'text/html')
296 self.set_header('Content-Type', 'text/html')
299 # render the template
297 # render the template
300 try:
298 try:
301 html = self.render_template('%s.html' % status_code, **ns)
299 html = self.render_template('%s.html' % status_code, **ns)
302 except TemplateNotFound:
300 except TemplateNotFound:
303 self.log.debug("No template for %d", status_code)
301 self.log.debug("No template for %d", status_code)
304 html = self.render_template('error.html', **ns)
302 html = self.render_template('error.html', **ns)
305
303
306 self.write(html)
304 self.write(html)
307
305
308
306
309
307
310 class Template404(IPythonHandler):
308 class Template404(IPythonHandler):
311 """Render our 404 template"""
309 """Render our 404 template"""
312 def prepare(self):
310 def prepare(self):
313 raise web.HTTPError(404)
311 raise web.HTTPError(404)
314
312
315
313
316 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
314 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
317 """static files should only be accessible when logged in"""
315 """static files should only be accessible when logged in"""
318
316
319 @web.authenticated
317 @web.authenticated
320 def get(self, path):
318 def get(self, path):
321 if os.path.splitext(path)[1] == '.ipynb':
319 if os.path.splitext(path)[1] == '.ipynb':
322 name = path.rsplit('/', 1)[-1]
320 name = path.rsplit('/', 1)[-1]
323 self.set_header('Content-Type', 'application/json')
321 self.set_header('Content-Type', 'application/json')
324 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
322 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
325
323
326 return web.StaticFileHandler.get(self, path)
324 return web.StaticFileHandler.get(self, path)
327
325
328 def set_headers(self):
326 def set_headers(self):
329 super(AuthenticatedFileHandler, self).set_headers()
327 super(AuthenticatedFileHandler, self).set_headers()
330 # disable browser caching, rely on 304 replies for savings
328 # disable browser caching, rely on 304 replies for savings
331 if "v" not in self.request.arguments:
329 if "v" not in self.request.arguments:
332 self.add_header("Cache-Control", "no-cache")
330 self.add_header("Cache-Control", "no-cache")
333
331
334 def compute_etag(self):
332 def compute_etag(self):
335 return None
333 return None
336
334
337 def validate_absolute_path(self, root, absolute_path):
335 def validate_absolute_path(self, root, absolute_path):
338 """Validate and return the absolute path.
336 """Validate and return the absolute path.
339
337
340 Requires tornado 3.1
338 Requires tornado 3.1
341
339
342 Adding to tornado's own handling, forbids the serving of hidden files.
340 Adding to tornado's own handling, forbids the serving of hidden files.
343 """
341 """
344 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
342 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
345 abs_root = os.path.abspath(root)
343 abs_root = os.path.abspath(root)
346 if is_hidden(abs_path, abs_root):
344 if is_hidden(abs_path, abs_root):
347 self.log.info("Refusing to serve hidden file, via 404 Error")
345 self.log.info("Refusing to serve hidden file, via 404 Error")
348 raise web.HTTPError(404)
346 raise web.HTTPError(404)
349 return abs_path
347 return abs_path
350
348
351
349
352 def json_errors(method):
350 def json_errors(method):
353 """Decorate methods with this to return GitHub style JSON errors.
351 """Decorate methods with this to return GitHub style JSON errors.
354
352
355 This should be used on any JSON API on any handler method that can raise HTTPErrors.
353 This should be used on any JSON API on any handler method that can raise HTTPErrors.
356
354
357 This will grab the latest HTTPError exception using sys.exc_info
355 This will grab the latest HTTPError exception using sys.exc_info
358 and then:
356 and then:
359
357
360 1. Set the HTTP status code based on the HTTPError
358 1. Set the HTTP status code based on the HTTPError
361 2. Create and return a JSON body with a message field describing
359 2. Create and return a JSON body with a message field describing
362 the error in a human readable form.
360 the error in a human readable form.
363 """
361 """
364 @functools.wraps(method)
362 @functools.wraps(method)
365 def wrapper(self, *args, **kwargs):
363 def wrapper(self, *args, **kwargs):
366 try:
364 try:
367 result = method(self, *args, **kwargs)
365 result = method(self, *args, **kwargs)
368 except web.HTTPError as e:
366 except web.HTTPError as e:
369 status = e.status_code
367 status = e.status_code
370 message = e.log_message
368 message = e.log_message
371 self.log.warn(message)
369 self.log.warn(message)
372 self.set_status(e.status_code)
370 self.set_status(e.status_code)
373 self.finish(json.dumps(dict(message=message)))
371 self.finish(json.dumps(dict(message=message)))
374 except Exception:
372 except Exception:
375 self.log.error("Unhandled error in API request", exc_info=True)
373 self.log.error("Unhandled error in API request", exc_info=True)
376 status = 500
374 status = 500
377 message = "Unknown server error"
375 message = "Unknown server error"
378 t, value, tb = sys.exc_info()
376 t, value, tb = sys.exc_info()
379 self.set_status(status)
377 self.set_status(status)
380 tb_text = ''.join(traceback.format_exception(t, value, tb))
378 tb_text = ''.join(traceback.format_exception(t, value, tb))
381 reply = dict(message=message, traceback=tb_text)
379 reply = dict(message=message, traceback=tb_text)
382 self.finish(json.dumps(reply))
380 self.finish(json.dumps(reply))
383 else:
381 else:
384 return result
382 return result
385 return wrapper
383 return wrapper
386
384
387
385
388
386
389 #-----------------------------------------------------------------------------
387 #-----------------------------------------------------------------------------
390 # File handler
388 # File handler
391 #-----------------------------------------------------------------------------
389 #-----------------------------------------------------------------------------
392
390
393 # to minimize subclass changes:
391 # to minimize subclass changes:
394 HTTPError = web.HTTPError
392 HTTPError = web.HTTPError
395
393
396 class FileFindHandler(web.StaticFileHandler):
394 class FileFindHandler(web.StaticFileHandler):
397 """subclass of StaticFileHandler for serving files from a search path"""
395 """subclass of StaticFileHandler for serving files from a search path"""
398
396
399 # cache search results, don't search for files more than once
397 # cache search results, don't search for files more than once
400 _static_paths = {}
398 _static_paths = {}
401
399
402 def set_headers(self):
400 def set_headers(self):
403 super(FileFindHandler, self).set_headers()
401 super(FileFindHandler, self).set_headers()
404 # disable browser caching, rely on 304 replies for savings
402 # disable browser caching, rely on 304 replies for savings
405 if "v" not in self.request.arguments or \
403 if "v" not in self.request.arguments or \
406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
404 any(self.request.path.startswith(path) for path in self.no_cache_paths):
407 self.add_header("Cache-Control", "no-cache")
405 self.add_header("Cache-Control", "no-cache")
408
406
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
407 def initialize(self, path, default_filename=None, no_cache_paths=None):
410 self.no_cache_paths = no_cache_paths or []
408 self.no_cache_paths = no_cache_paths or []
411
409
412 if isinstance(path, string_types):
410 if isinstance(path, string_types):
413 path = [path]
411 path = [path]
414
412
415 self.root = tuple(
413 self.root = tuple(
416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
414 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
417 )
415 )
418 self.default_filename = default_filename
416 self.default_filename = default_filename
419
417
420 def compute_etag(self):
418 def compute_etag(self):
421 return None
419 return None
422
420
423 @classmethod
421 @classmethod
424 def get_absolute_path(cls, roots, path):
422 def get_absolute_path(cls, roots, path):
425 """locate a file to serve on our static file search path"""
423 """locate a file to serve on our static file search path"""
426 with cls._lock:
424 with cls._lock:
427 if path in cls._static_paths:
425 if path in cls._static_paths:
428 return cls._static_paths[path]
426 return cls._static_paths[path]
429 try:
427 try:
430 abspath = os.path.abspath(filefind(path, roots))
428 abspath = os.path.abspath(filefind(path, roots))
431 except IOError:
429 except IOError:
432 # IOError means not found
430 # IOError means not found
433 return ''
431 return ''
434
432
435 cls._static_paths[path] = abspath
433 cls._static_paths[path] = abspath
436 return abspath
434 return abspath
437
435
438 def validate_absolute_path(self, root, absolute_path):
436 def validate_absolute_path(self, root, absolute_path):
439 """check if the file should be served (raises 404, 403, etc.)"""
437 """check if the file should be served (raises 404, 403, etc.)"""
440 if absolute_path == '':
438 if absolute_path == '':
441 raise web.HTTPError(404)
439 raise web.HTTPError(404)
442
440
443 for root in self.root:
441 for root in self.root:
444 if (absolute_path + os.sep).startswith(root):
442 if (absolute_path + os.sep).startswith(root):
445 break
443 break
446
444
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
445 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
448
446
449
447
450 class ApiVersionHandler(IPythonHandler):
448 class ApiVersionHandler(IPythonHandler):
451
449
452 @json_errors
450 @json_errors
453 def get(self):
451 def get(self):
454 # not authenticated, so give as few info as possible
452 # not authenticated, so give as few info as possible
455 self.finish(json.dumps({"version":IPython.__version__}))
453 self.finish(json.dumps({"version":IPython.__version__}))
456
454
457
455
458 class TrailingSlashHandler(web.RequestHandler):
456 class TrailingSlashHandler(web.RequestHandler):
459 """Simple redirect handler that strips trailing slashes
457 """Simple redirect handler that strips trailing slashes
460
458
461 This should be the first, highest priority handler.
459 This should be the first, highest priority handler.
462 """
460 """
463
461
464 def get(self):
462 def get(self):
465 self.redirect(self.request.uri.rstrip('/'))
463 self.redirect(self.request.uri.rstrip('/'))
466
464
467 post = put = get
465 post = put = get
468
466
469
467
470 class FilesRedirectHandler(IPythonHandler):
468 class FilesRedirectHandler(IPythonHandler):
471 """Handler for redirecting relative URLs to the /files/ handler"""
469 """Handler for redirecting relative URLs to the /files/ handler"""
472 def get(self, path=''):
470 def get(self, path=''):
473 cm = self.contents_manager
471 cm = self.contents_manager
474 if cm.dir_exists(path):
472 if cm.dir_exists(path):
475 # it's a *directory*, redirect to /tree
473 # it's a *directory*, redirect to /tree
476 url = url_path_join(self.base_url, 'tree', path)
474 url = url_path_join(self.base_url, 'tree', path)
477 else:
475 else:
478 orig_path = path
476 orig_path = path
479 # otherwise, redirect to /files
477 # otherwise, redirect to /files
480 parts = path.split('/')
478 parts = path.split('/')
481
479
482 if not cm.file_exists(path=path) and 'files' in parts:
480 if not cm.file_exists(path=path) and 'files' in parts:
483 # redirect without files/ iff it would 404
481 # redirect without files/ iff it would 404
484 # this preserves pre-2.0-style 'files/' links
482 # this preserves pre-2.0-style 'files/' links
485 self.log.warn("Deprecated files/ URL: %s", orig_path)
483 self.log.warn("Deprecated files/ URL: %s", orig_path)
486 parts.remove('files')
484 parts.remove('files')
487 path = '/'.join(parts)
485 path = '/'.join(parts)
488
486
489 if not cm.file_exists(path=path):
487 if not cm.file_exists(path=path):
490 raise web.HTTPError(404)
488 raise web.HTTPError(404)
491
489
492 url = url_path_join(self.base_url, 'files', path)
490 url = url_path_join(self.base_url, 'files', path)
493 url = url_escape(url)
491 url = url_escape(url)
494 self.log.debug("Redirecting %s to %s", self.request.path, url)
492 self.log.debug("Redirecting %s to %s", self.request.path, url)
495 self.redirect(url)
493 self.redirect(url)
496
494
497
495
498 #-----------------------------------------------------------------------------
496 #-----------------------------------------------------------------------------
499 # URL pattern fragments for re-use
497 # URL pattern fragments for re-use
500 #-----------------------------------------------------------------------------
498 #-----------------------------------------------------------------------------
501
499
502 # path matches any number of `/foo[/bar...]` or just `/` or ''
500 # path matches any number of `/foo[/bar...]` or just `/` or ''
503 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
501 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
504 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
502 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
505
503
506 #-----------------------------------------------------------------------------
504 #-----------------------------------------------------------------------------
507 # URL to handler mappings
505 # URL to handler mappings
508 #-----------------------------------------------------------------------------
506 #-----------------------------------------------------------------------------
509
507
510
508
511 default_handlers = [
509 default_handlers = [
512 (r".*/", TrailingSlashHandler),
510 (r".*/", TrailingSlashHandler),
513 (r"api", ApiVersionHandler)
511 (r"api", ApiVersionHandler)
514 ]
512 ]
General Comments 0
You need to be logged in to leave comments. Login now