##// END OF EJS Templates
Merge pull request #4715 from minrk/tornado-static-url...
Min RK -
r13985:0fc2a30e merge
parent child Browse files
Show More
@@ -1,372 +1,376 b''
1 1 """Base Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19
20 20 import functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import stat
25 25 import sys
26 26 import traceback
27 27
28 28 from tornado import web
29 29
30 30 try:
31 31 from tornado.log import app_log
32 32 except ImportError:
33 33 app_log = logging.getLogger()
34 34
35 35 from IPython.config import Application
36 36 from IPython.utils.path import filefind
37 37 from IPython.utils.py3compat import string_types
38 38
39 39 # UF_HIDDEN is a stat flag not defined in the stat module.
40 40 # It is used by BSD to indicate hidden files.
41 41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Top-level handlers
45 45 #-----------------------------------------------------------------------------
46 46
47 47 class RequestHandler(web.RequestHandler):
48 48 """RequestHandler with default variable setting."""
49 49
50 50 def render(*args, **kwargs):
51 51 kwargs.setdefault('message', '')
52 52 return web.RequestHandler.render(*args, **kwargs)
53 53
54 54 class AuthenticatedHandler(RequestHandler):
55 55 """A RequestHandler with an authenticated user."""
56 56
57 57 def clear_login_cookie(self):
58 58 self.clear_cookie(self.cookie_name)
59 59
60 60 def get_current_user(self):
61 61 user_id = self.get_secure_cookie(self.cookie_name)
62 62 # For now the user_id should not return empty, but it could eventually
63 63 if user_id == '':
64 64 user_id = 'anonymous'
65 65 if user_id is None:
66 66 # prevent extra Invalid cookie sig warnings:
67 67 self.clear_login_cookie()
68 68 if not self.login_available:
69 69 user_id = 'anonymous'
70 70 return user_id
71 71
72 72 @property
73 73 def cookie_name(self):
74 74 default_cookie_name = 'username-{host}'.format(
75 75 host=self.request.host,
76 76 ).replace(':', '-')
77 77 return self.settings.get('cookie_name', default_cookie_name)
78 78
79 79 @property
80 80 def password(self):
81 81 """our password"""
82 82 return self.settings.get('password', '')
83 83
84 84 @property
85 85 def logged_in(self):
86 86 """Is a user currently logged in?
87 87
88 88 """
89 89 user = self.get_current_user()
90 90 return (user and not user == 'anonymous')
91 91
92 92 @property
93 93 def login_available(self):
94 94 """May a user proceed to log in?
95 95
96 96 This returns True if login capability is available, irrespective of
97 97 whether the user is already logged in or not.
98 98
99 99 """
100 100 return bool(self.settings.get('password', ''))
101 101
102 102
103 103 class IPythonHandler(AuthenticatedHandler):
104 104 """IPython-specific extensions to authenticated handling
105 105
106 106 Mostly property shortcuts to IPython-specific settings.
107 107 """
108 108
109 109 @property
110 110 def config(self):
111 111 return self.settings.get('config', None)
112 112
113 113 @property
114 114 def log(self):
115 115 """use the IPython log by default, falling back on tornado's logger"""
116 116 if Application.initialized():
117 117 return Application.instance().log
118 118 else:
119 119 return app_log
120 120
121 121 @property
122 122 def use_less(self):
123 123 """Use less instead of css in templates"""
124 124 return self.settings.get('use_less', False)
125 125
126 126 #---------------------------------------------------------------
127 127 # URLs
128 128 #---------------------------------------------------------------
129 129
130 130 @property
131 131 def ws_url(self):
132 132 """websocket url matching the current request
133 133
134 134 By default, this is just `''`, indicating that it should match
135 135 the same host, protocol, port, etc.
136 136 """
137 137 return self.settings.get('websocket_url', '')
138 138
139 139 @property
140 140 def mathjax_url(self):
141 141 return self.settings.get('mathjax_url', '')
142 142
143 143 @property
144 144 def base_project_url(self):
145 145 return self.settings.get('base_project_url', '/')
146 146
147 147 @property
148 148 def base_kernel_url(self):
149 149 return self.settings.get('base_kernel_url', '/')
150 150
151 151 #---------------------------------------------------------------
152 152 # Manager objects
153 153 #---------------------------------------------------------------
154 154
155 155 @property
156 156 def kernel_manager(self):
157 157 return self.settings['kernel_manager']
158 158
159 159 @property
160 160 def notebook_manager(self):
161 161 return self.settings['notebook_manager']
162 162
163 163 @property
164 164 def cluster_manager(self):
165 165 return self.settings['cluster_manager']
166 166
167 167 @property
168 168 def session_manager(self):
169 169 return self.settings['session_manager']
170 170
171 171 @property
172 172 def project_dir(self):
173 173 return self.notebook_manager.notebook_dir
174 174
175 175 #---------------------------------------------------------------
176 176 # template rendering
177 177 #---------------------------------------------------------------
178 178
179 179 def get_template(self, name):
180 180 """Return the jinja template object for a given name"""
181 181 return self.settings['jinja2_env'].get_template(name)
182 182
183 183 def render_template(self, name, **ns):
184 184 ns.update(self.template_namespace)
185 185 template = self.get_template(name)
186 186 return template.render(**ns)
187 187
188 188 @property
189 189 def template_namespace(self):
190 190 return dict(
191 191 base_project_url=self.base_project_url,
192 192 base_kernel_url=self.base_kernel_url,
193 193 logged_in=self.logged_in,
194 194 login_available=self.login_available,
195 195 use_less=self.use_less,
196 static_url=self.static_url,
196 197 )
197
198
198 199 def get_json_body(self):
199 200 """Return the body of the request as JSON data."""
200 201 if not self.request.body:
201 202 return None
202 203 # Do we need to call body.decode('utf-8') here?
203 204 body = self.request.body.strip().decode(u'utf-8')
204 205 try:
205 206 model = json.loads(body)
206 207 except Exception:
207 208 self.log.debug("Bad JSON: %r", body)
208 209 self.log.error("Couldn't parse JSON", exc_info=True)
209 210 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 211 return model
211 212
212 213
213 214 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 215 """static files should only be accessible when logged in"""
215 216
216 217 @web.authenticated
217 218 def get(self, path):
218 219 if os.path.splitext(path)[1] == '.ipynb':
219 220 name = os.path.basename(path)
220 221 self.set_header('Content-Type', 'application/json')
221 222 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222 223
223 224 return web.StaticFileHandler.get(self, path)
224 225
225 226 def compute_etag(self):
226 227 return None
227 228
228 229 def validate_absolute_path(self, root, absolute_path):
229 230 """Validate and return the absolute path.
230 231
231 232 Requires tornado 3.1
232 233
233 234 Adding to tornado's own handling, forbids the serving of hidden files.
234 235 """
235 236 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 237 abs_root = os.path.abspath(root)
237 238 self.forbid_hidden(abs_root, abs_path)
238 239 return abs_path
239 240
240 241 def forbid_hidden(self, absolute_root, absolute_path):
241 242 """Raise 403 if a file is hidden or contained in a hidden directory.
242 243
243 244 Hidden is determined by either name starting with '.'
244 245 or the UF_HIDDEN flag as reported by stat
245 246 """
246 247 inside_root = absolute_path[len(absolute_root):]
247 248 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 249 raise web.HTTPError(403)
249 250
250 251 # check UF_HIDDEN on any location up to root
251 252 path = absolute_path
252 253 while path and path.startswith(absolute_root) and path != absolute_root:
253 254 st = os.stat(path)
254 255 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 256 raise web.HTTPError(403)
256 257 path = os.path.dirname(path)
257 258
258 259 return absolute_path
259 260
260 261
261 262 def json_errors(method):
262 263 """Decorate methods with this to return GitHub style JSON errors.
263 264
264 265 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265 266
266 267 This will grab the latest HTTPError exception using sys.exc_info
267 268 and then:
268 269
269 270 1. Set the HTTP status code based on the HTTPError
270 271 2. Create and return a JSON body with a message field describing
271 272 the error in a human readable form.
272 273 """
273 274 @functools.wraps(method)
274 275 def wrapper(self, *args, **kwargs):
275 276 try:
276 277 result = method(self, *args, **kwargs)
277 278 except web.HTTPError as e:
278 279 status = e.status_code
279 280 message = e.log_message
280 281 self.set_status(e.status_code)
281 282 self.finish(json.dumps(dict(message=message)))
282 283 except Exception:
283 284 self.log.error("Unhandled error in API request", exc_info=True)
284 285 status = 500
285 286 message = "Unknown server error"
286 287 t, value, tb = sys.exc_info()
287 288 self.set_status(status)
288 289 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 290 reply = dict(message=message, traceback=tb_text)
290 291 self.finish(json.dumps(reply))
291 292 else:
292 293 return result
293 294 return wrapper
294 295
295 296
296 297
297 298 #-----------------------------------------------------------------------------
298 299 # File handler
299 300 #-----------------------------------------------------------------------------
300 301
301 302 # to minimize subclass changes:
302 303 HTTPError = web.HTTPError
303 304
304 305 class FileFindHandler(web.StaticFileHandler):
305 306 """subclass of StaticFileHandler for serving files from a search path"""
306 307
307 308 # cache search results, don't search for files more than once
308 309 _static_paths = {}
309 310
310 311 def initialize(self, path, default_filename=None):
311 312 if isinstance(path, string_types):
312 313 path = [path]
313 314
314 315 self.root = tuple(
315 316 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 317 )
317 318 self.default_filename = default_filename
318 319
319 320 def compute_etag(self):
320 321 return None
321 322
322 323 @classmethod
323 324 def get_absolute_path(cls, roots, path):
324 325 """locate a file to serve on our static file search path"""
325 326 with cls._lock:
326 327 if path in cls._static_paths:
327 328 return cls._static_paths[path]
328 329 try:
329 330 abspath = os.path.abspath(filefind(path, roots))
330 331 except IOError:
331 332 # IOError means not found
332 raise web.HTTPError(404)
333 return ''
333 334
334 335 cls._static_paths[path] = abspath
335 336 return abspath
336 337
337 338 def validate_absolute_path(self, root, absolute_path):
338 339 """check if the file should be served (raises 404, 403, etc.)"""
340 if absolute_path == '':
341 raise web.HTTPError(404)
342
339 343 for root in self.root:
340 344 if (absolute_path + os.sep).startswith(root):
341 345 break
342 346
343 347 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
344 348
345 349
346 350 class TrailingSlashHandler(web.RequestHandler):
347 351 """Simple redirect handler that strips trailing slashes
348 352
349 353 This should be the first, highest priority handler.
350 354 """
351 355
352 356 SUPPORTED_METHODS = ['GET']
353 357
354 358 def get(self):
355 359 self.redirect(self.request.uri.rstrip('/'))
356 360
357 361 #-----------------------------------------------------------------------------
358 362 # URL pattern fragments for re-use
359 363 #-----------------------------------------------------------------------------
360 364
361 365 path_regex = r"(?P<path>(?:/.*)*)"
362 366 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
363 367 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
364 368
365 369 #-----------------------------------------------------------------------------
366 370 # URL to handler mappings
367 371 #-----------------------------------------------------------------------------
368 372
369 373
370 374 default_handlers = [
371 375 (r".*/", TrailingSlashHandler)
372 376 ]
@@ -1,97 +1,94 b''
1 1
2 2
3 3 <!DOCTYPE HTML>
4 {% macro static_url(name) -%}
5 {{ base_project_url }}static/{{ name }}
6 {%- endmacro %}
7 4 <html>
8 5
9 6 <head>
10 7 <meta charset="utf-8">
11 8
12 9 <title>{% block title %}IPython Notebook{% endblock %}</title>
13 10 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
14 11 <meta http-equiv="X-UA-Compatible" content="chrome=1">
15 12 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
16 13 <meta name="viewport" content="width=device-width, initial-scale=1.0">
17 14
18 15 {% block stylesheet %}
19 16 {% block lesscss %}
20 17 {% if use_less %}
21 18 <link rel="stylesheet/less" href="{{ static_url("style/style.less") }}" type="text/css" />
22 19 {% else %}
23 20 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
24 21 {% endif %}
25 22 {% endblock %}
26 23 {% endblock %}
27 24 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
28 25 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
29 26 <script>
30 27 require.config({
31 baseUrl: '{{static_url()}}',
28 baseUrl: '{{static_url("")}}',
32 29 paths: {
33 30 nbextensions : '{{ base_project_url }}nbextensions'
34 31 }
35 32 });
36 33 </script>
37 34
38 35 {% block meta %}
39 36 {% endblock %}
40 37
41 38 </head>
42 39
43 40 <body {% block params %}{% endblock %}>
44 41
45 42 <noscript>
46 43 <div id='noscript'>
47 44 IPython Notebook requires JavaScript.<br>
48 45 Please enable it to proceed.
49 46 </div>
50 47 </noscript>
51 48
52 49 <div id="header" class="navbar navbar-static-top">
53 50 <div class="navbar-inner navbar-nobg">
54 51 <div class="container">
55 52 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
56 53
57 54 {% block login_widget %}
58 55
59 56 <span id="login_widget">
60 57 {% if logged_in %}
61 58 <button id="logout">Logout</button>
62 59 {% elif login_available and not logged_in %}
63 60 <button id="login">Login</button>
64 61 {% endif %}
65 62 </span>
66 63
67 64 {% endblock %}
68 65
69 66 {% block header %}
70 67 {% endblock %}
71 68 </div>
72 69 </div>
73 70 </div>
74 71
75 72 <div id="site">
76 73 {% block site %}
77 74 {% endblock %}
78 75 </div>
79 76
80 77 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
81 78 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
82 79 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
83 80 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
84 81 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
85 82 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
86 83
87 84 {% block script %}
88 85 {% if use_less %}
89 86 <script src="{{ static_url("components/less.js/dist/less-1.3.3.min.js") }}" charset="utf-8"></script>
90 87 {% endif %}
91 88 {% endblock %}
92 89
93 90 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
94 91
95 92 </body>
96 93
97 94 </html>
General Comments 0
You need to be logged in to leave comments. Login now