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