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