##// END OF EJS Templates
Merge pull request #6854 from takluyver/post-new-terminal...
Min RK -
r18785:2ad28dc5 merge
parent child Browse files
Show More
@@ -1,478 +1,482 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 terminal_manager(self):
168 return self.settings['terminal_manager']
169
170 @property
167 def kernel_spec_manager(self):
171 def kernel_spec_manager(self):
168 return self.settings['kernel_spec_manager']
172 return self.settings['kernel_spec_manager']
169
173
170 #---------------------------------------------------------------
174 #---------------------------------------------------------------
171 # CORS
175 # CORS
172 #---------------------------------------------------------------
176 #---------------------------------------------------------------
173
177
174 @property
178 @property
175 def allow_origin(self):
179 def allow_origin(self):
176 """Normal Access-Control-Allow-Origin"""
180 """Normal Access-Control-Allow-Origin"""
177 return self.settings.get('allow_origin', '')
181 return self.settings.get('allow_origin', '')
178
182
179 @property
183 @property
180 def allow_origin_pat(self):
184 def allow_origin_pat(self):
181 """Regular expression version of allow_origin"""
185 """Regular expression version of allow_origin"""
182 return self.settings.get('allow_origin_pat', None)
186 return self.settings.get('allow_origin_pat', None)
183
187
184 @property
188 @property
185 def allow_credentials(self):
189 def allow_credentials(self):
186 """Whether to set Access-Control-Allow-Credentials"""
190 """Whether to set Access-Control-Allow-Credentials"""
187 return self.settings.get('allow_credentials', False)
191 return self.settings.get('allow_credentials', False)
188
192
189 def set_default_headers(self):
193 def set_default_headers(self):
190 """Add CORS headers, if defined"""
194 """Add CORS headers, if defined"""
191 super(IPythonHandler, self).set_default_headers()
195 super(IPythonHandler, self).set_default_headers()
192 if self.allow_origin:
196 if self.allow_origin:
193 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
197 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
194 elif self.allow_origin_pat:
198 elif self.allow_origin_pat:
195 origin = self.get_origin()
199 origin = self.get_origin()
196 if origin and self.allow_origin_pat.match(origin):
200 if origin and self.allow_origin_pat.match(origin):
197 self.set_header("Access-Control-Allow-Origin", origin)
201 self.set_header("Access-Control-Allow-Origin", origin)
198 if self.allow_credentials:
202 if self.allow_credentials:
199 self.set_header("Access-Control-Allow-Credentials", 'true')
203 self.set_header("Access-Control-Allow-Credentials", 'true')
200
204
201 def get_origin(self):
205 def get_origin(self):
202 # Handle WebSocket Origin naming convention differences
206 # Handle WebSocket Origin naming convention differences
203 # The difference between version 8 and 13 is that in 8 the
207 # 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
208 # client sends a "Sec-Websocket-Origin" header and in 13 it's
205 # simply "Origin".
209 # simply "Origin".
206 if "Origin" in self.request.headers:
210 if "Origin" in self.request.headers:
207 origin = self.request.headers.get("Origin")
211 origin = self.request.headers.get("Origin")
208 else:
212 else:
209 origin = self.request.headers.get("Sec-Websocket-Origin", None)
213 origin = self.request.headers.get("Sec-Websocket-Origin", None)
210 return origin
214 return origin
211
215
212 #---------------------------------------------------------------
216 #---------------------------------------------------------------
213 # template rendering
217 # template rendering
214 #---------------------------------------------------------------
218 #---------------------------------------------------------------
215
219
216 def get_template(self, name):
220 def get_template(self, name):
217 """Return the jinja template object for a given name"""
221 """Return the jinja template object for a given name"""
218 return self.settings['jinja2_env'].get_template(name)
222 return self.settings['jinja2_env'].get_template(name)
219
223
220 def render_template(self, name, **ns):
224 def render_template(self, name, **ns):
221 ns.update(self.template_namespace)
225 ns.update(self.template_namespace)
222 template = self.get_template(name)
226 template = self.get_template(name)
223 return template.render(**ns)
227 return template.render(**ns)
224
228
225 @property
229 @property
226 def template_namespace(self):
230 def template_namespace(self):
227 return dict(
231 return dict(
228 base_url=self.base_url,
232 base_url=self.base_url,
229 ws_url=self.ws_url,
233 ws_url=self.ws_url,
230 logged_in=self.logged_in,
234 logged_in=self.logged_in,
231 login_available=self.login_available,
235 login_available=self.login_available,
232 static_url=self.static_url,
236 static_url=self.static_url,
233 sys_info=sys_info,
237 sys_info=sys_info,
234 contents_js_source=self.contents_js_source,
238 contents_js_source=self.contents_js_source,
235 )
239 )
236
240
237 def get_json_body(self):
241 def get_json_body(self):
238 """Return the body of the request as JSON data."""
242 """Return the body of the request as JSON data."""
239 if not self.request.body:
243 if not self.request.body:
240 return None
244 return None
241 # Do we need to call body.decode('utf-8') here?
245 # Do we need to call body.decode('utf-8') here?
242 body = self.request.body.strip().decode(u'utf-8')
246 body = self.request.body.strip().decode(u'utf-8')
243 try:
247 try:
244 model = json.loads(body)
248 model = json.loads(body)
245 except Exception:
249 except Exception:
246 self.log.debug("Bad JSON: %r", body)
250 self.log.debug("Bad JSON: %r", body)
247 self.log.error("Couldn't parse JSON", exc_info=True)
251 self.log.error("Couldn't parse JSON", exc_info=True)
248 raise web.HTTPError(400, u'Invalid JSON in body of request')
252 raise web.HTTPError(400, u'Invalid JSON in body of request')
249 return model
253 return model
250
254
251 def write_error(self, status_code, **kwargs):
255 def write_error(self, status_code, **kwargs):
252 """render custom error pages"""
256 """render custom error pages"""
253 exc_info = kwargs.get('exc_info')
257 exc_info = kwargs.get('exc_info')
254 message = ''
258 message = ''
255 status_message = responses.get(status_code, 'Unknown HTTP Error')
259 status_message = responses.get(status_code, 'Unknown HTTP Error')
256 if exc_info:
260 if exc_info:
257 exception = exc_info[1]
261 exception = exc_info[1]
258 # get the custom message, if defined
262 # get the custom message, if defined
259 try:
263 try:
260 message = exception.log_message % exception.args
264 message = exception.log_message % exception.args
261 except Exception:
265 except Exception:
262 pass
266 pass
263
267
264 # construct the custom reason, if defined
268 # construct the custom reason, if defined
265 reason = getattr(exception, 'reason', '')
269 reason = getattr(exception, 'reason', '')
266 if reason:
270 if reason:
267 status_message = reason
271 status_message = reason
268
272
269 # build template namespace
273 # build template namespace
270 ns = dict(
274 ns = dict(
271 status_code=status_code,
275 status_code=status_code,
272 status_message=status_message,
276 status_message=status_message,
273 message=message,
277 message=message,
274 exception=exception,
278 exception=exception,
275 )
279 )
276
280
277 self.set_header('Content-Type', 'text/html')
281 self.set_header('Content-Type', 'text/html')
278 # render the template
282 # render the template
279 try:
283 try:
280 html = self.render_template('%s.html' % status_code, **ns)
284 html = self.render_template('%s.html' % status_code, **ns)
281 except TemplateNotFound:
285 except TemplateNotFound:
282 self.log.debug("No template for %d", status_code)
286 self.log.debug("No template for %d", status_code)
283 html = self.render_template('error.html', **ns)
287 html = self.render_template('error.html', **ns)
284
288
285 self.write(html)
289 self.write(html)
286
290
287
291
288
292
289 class Template404(IPythonHandler):
293 class Template404(IPythonHandler):
290 """Render our 404 template"""
294 """Render our 404 template"""
291 def prepare(self):
295 def prepare(self):
292 raise web.HTTPError(404)
296 raise web.HTTPError(404)
293
297
294
298
295 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
299 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
296 """static files should only be accessible when logged in"""
300 """static files should only be accessible when logged in"""
297
301
298 @web.authenticated
302 @web.authenticated
299 def get(self, path):
303 def get(self, path):
300 if os.path.splitext(path)[1] == '.ipynb':
304 if os.path.splitext(path)[1] == '.ipynb':
301 name = path.rsplit('/', 1)[-1]
305 name = path.rsplit('/', 1)[-1]
302 self.set_header('Content-Type', 'application/json')
306 self.set_header('Content-Type', 'application/json')
303 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
307 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
304
308
305 return web.StaticFileHandler.get(self, path)
309 return web.StaticFileHandler.get(self, path)
306
310
307 def compute_etag(self):
311 def compute_etag(self):
308 return None
312 return None
309
313
310 def validate_absolute_path(self, root, absolute_path):
314 def validate_absolute_path(self, root, absolute_path):
311 """Validate and return the absolute path.
315 """Validate and return the absolute path.
312
316
313 Requires tornado 3.1
317 Requires tornado 3.1
314
318
315 Adding to tornado's own handling, forbids the serving of hidden files.
319 Adding to tornado's own handling, forbids the serving of hidden files.
316 """
320 """
317 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
321 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
318 abs_root = os.path.abspath(root)
322 abs_root = os.path.abspath(root)
319 if is_hidden(abs_path, abs_root):
323 if is_hidden(abs_path, abs_root):
320 self.log.info("Refusing to serve hidden file, via 404 Error")
324 self.log.info("Refusing to serve hidden file, via 404 Error")
321 raise web.HTTPError(404)
325 raise web.HTTPError(404)
322 return abs_path
326 return abs_path
323
327
324
328
325 def json_errors(method):
329 def json_errors(method):
326 """Decorate methods with this to return GitHub style JSON errors.
330 """Decorate methods with this to return GitHub style JSON errors.
327
331
328 This should be used on any JSON API on any handler method that can raise HTTPErrors.
332 This should be used on any JSON API on any handler method that can raise HTTPErrors.
329
333
330 This will grab the latest HTTPError exception using sys.exc_info
334 This will grab the latest HTTPError exception using sys.exc_info
331 and then:
335 and then:
332
336
333 1. Set the HTTP status code based on the HTTPError
337 1. Set the HTTP status code based on the HTTPError
334 2. Create and return a JSON body with a message field describing
338 2. Create and return a JSON body with a message field describing
335 the error in a human readable form.
339 the error in a human readable form.
336 """
340 """
337 @functools.wraps(method)
341 @functools.wraps(method)
338 def wrapper(self, *args, **kwargs):
342 def wrapper(self, *args, **kwargs):
339 try:
343 try:
340 result = method(self, *args, **kwargs)
344 result = method(self, *args, **kwargs)
341 except web.HTTPError as e:
345 except web.HTTPError as e:
342 status = e.status_code
346 status = e.status_code
343 message = e.log_message
347 message = e.log_message
344 self.log.warn(message)
348 self.log.warn(message)
345 self.set_status(e.status_code)
349 self.set_status(e.status_code)
346 self.finish(json.dumps(dict(message=message)))
350 self.finish(json.dumps(dict(message=message)))
347 except Exception:
351 except Exception:
348 self.log.error("Unhandled error in API request", exc_info=True)
352 self.log.error("Unhandled error in API request", exc_info=True)
349 status = 500
353 status = 500
350 message = "Unknown server error"
354 message = "Unknown server error"
351 t, value, tb = sys.exc_info()
355 t, value, tb = sys.exc_info()
352 self.set_status(status)
356 self.set_status(status)
353 tb_text = ''.join(traceback.format_exception(t, value, tb))
357 tb_text = ''.join(traceback.format_exception(t, value, tb))
354 reply = dict(message=message, traceback=tb_text)
358 reply = dict(message=message, traceback=tb_text)
355 self.finish(json.dumps(reply))
359 self.finish(json.dumps(reply))
356 else:
360 else:
357 return result
361 return result
358 return wrapper
362 return wrapper
359
363
360
364
361
365
362 #-----------------------------------------------------------------------------
366 #-----------------------------------------------------------------------------
363 # File handler
367 # File handler
364 #-----------------------------------------------------------------------------
368 #-----------------------------------------------------------------------------
365
369
366 # to minimize subclass changes:
370 # to minimize subclass changes:
367 HTTPError = web.HTTPError
371 HTTPError = web.HTTPError
368
372
369 class FileFindHandler(web.StaticFileHandler):
373 class FileFindHandler(web.StaticFileHandler):
370 """subclass of StaticFileHandler for serving files from a search path"""
374 """subclass of StaticFileHandler for serving files from a search path"""
371
375
372 # cache search results, don't search for files more than once
376 # cache search results, don't search for files more than once
373 _static_paths = {}
377 _static_paths = {}
374
378
375 def initialize(self, path, default_filename=None):
379 def initialize(self, path, default_filename=None):
376 if isinstance(path, string_types):
380 if isinstance(path, string_types):
377 path = [path]
381 path = [path]
378
382
379 self.root = tuple(
383 self.root = tuple(
380 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
384 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
381 )
385 )
382 self.default_filename = default_filename
386 self.default_filename = default_filename
383
387
384 def compute_etag(self):
388 def compute_etag(self):
385 return None
389 return None
386
390
387 @classmethod
391 @classmethod
388 def get_absolute_path(cls, roots, path):
392 def get_absolute_path(cls, roots, path):
389 """locate a file to serve on our static file search path"""
393 """locate a file to serve on our static file search path"""
390 with cls._lock:
394 with cls._lock:
391 if path in cls._static_paths:
395 if path in cls._static_paths:
392 return cls._static_paths[path]
396 return cls._static_paths[path]
393 try:
397 try:
394 abspath = os.path.abspath(filefind(path, roots))
398 abspath = os.path.abspath(filefind(path, roots))
395 except IOError:
399 except IOError:
396 # IOError means not found
400 # IOError means not found
397 return ''
401 return ''
398
402
399 cls._static_paths[path] = abspath
403 cls._static_paths[path] = abspath
400 return abspath
404 return abspath
401
405
402 def validate_absolute_path(self, root, absolute_path):
406 def validate_absolute_path(self, root, absolute_path):
403 """check if the file should be served (raises 404, 403, etc.)"""
407 """check if the file should be served (raises 404, 403, etc.)"""
404 if absolute_path == '':
408 if absolute_path == '':
405 raise web.HTTPError(404)
409 raise web.HTTPError(404)
406
410
407 for root in self.root:
411 for root in self.root:
408 if (absolute_path + os.sep).startswith(root):
412 if (absolute_path + os.sep).startswith(root):
409 break
413 break
410
414
411 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
415 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
412
416
413
417
414 class ApiVersionHandler(IPythonHandler):
418 class ApiVersionHandler(IPythonHandler):
415
419
416 @json_errors
420 @json_errors
417 def get(self):
421 def get(self):
418 # not authenticated, so give as few info as possible
422 # not authenticated, so give as few info as possible
419 self.finish(json.dumps({"version":IPython.__version__}))
423 self.finish(json.dumps({"version":IPython.__version__}))
420
424
421
425
422 class TrailingSlashHandler(web.RequestHandler):
426 class TrailingSlashHandler(web.RequestHandler):
423 """Simple redirect handler that strips trailing slashes
427 """Simple redirect handler that strips trailing slashes
424
428
425 This should be the first, highest priority handler.
429 This should be the first, highest priority handler.
426 """
430 """
427
431
428 def get(self):
432 def get(self):
429 self.redirect(self.request.uri.rstrip('/'))
433 self.redirect(self.request.uri.rstrip('/'))
430
434
431 post = put = get
435 post = put = get
432
436
433
437
434 class FilesRedirectHandler(IPythonHandler):
438 class FilesRedirectHandler(IPythonHandler):
435 """Handler for redirecting relative URLs to the /files/ handler"""
439 """Handler for redirecting relative URLs to the /files/ handler"""
436 def get(self, path=''):
440 def get(self, path=''):
437 cm = self.contents_manager
441 cm = self.contents_manager
438 if cm.dir_exists(path):
442 if cm.dir_exists(path):
439 # it's a *directory*, redirect to /tree
443 # it's a *directory*, redirect to /tree
440 url = url_path_join(self.base_url, 'tree', path)
444 url = url_path_join(self.base_url, 'tree', path)
441 else:
445 else:
442 orig_path = path
446 orig_path = path
443 # otherwise, redirect to /files
447 # otherwise, redirect to /files
444 parts = path.split('/')
448 parts = path.split('/')
445
449
446 if not cm.file_exists(path=path) and 'files' in parts:
450 if not cm.file_exists(path=path) and 'files' in parts:
447 # redirect without files/ iff it would 404
451 # redirect without files/ iff it would 404
448 # this preserves pre-2.0-style 'files/' links
452 # this preserves pre-2.0-style 'files/' links
449 self.log.warn("Deprecated files/ URL: %s", orig_path)
453 self.log.warn("Deprecated files/ URL: %s", orig_path)
450 parts.remove('files')
454 parts.remove('files')
451 path = '/'.join(parts)
455 path = '/'.join(parts)
452
456
453 if not cm.file_exists(path=path):
457 if not cm.file_exists(path=path):
454 raise web.HTTPError(404)
458 raise web.HTTPError(404)
455
459
456 url = url_path_join(self.base_url, 'files', path)
460 url = url_path_join(self.base_url, 'files', path)
457 url = url_escape(url)
461 url = url_escape(url)
458 self.log.debug("Redirecting %s to %s", self.request.path, url)
462 self.log.debug("Redirecting %s to %s", self.request.path, url)
459 self.redirect(url)
463 self.redirect(url)
460
464
461
465
462 #-----------------------------------------------------------------------------
466 #-----------------------------------------------------------------------------
463 # URL pattern fragments for re-use
467 # URL pattern fragments for re-use
464 #-----------------------------------------------------------------------------
468 #-----------------------------------------------------------------------------
465
469
466 # path matches any number of `/foo[/bar...]` or just `/` or ''
470 # path matches any number of `/foo[/bar...]` or just `/` or ''
467 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
471 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
468 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
472 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
469
473
470 #-----------------------------------------------------------------------------
474 #-----------------------------------------------------------------------------
471 # URL to handler mappings
475 # URL to handler mappings
472 #-----------------------------------------------------------------------------
476 #-----------------------------------------------------------------------------
473
477
474
478
475 default_handlers = [
479 default_handlers = [
476 (r".*/", TrailingSlashHandler),
480 (r".*/", TrailingSlashHandler),
477 (r"api", ApiVersionHandler)
481 (r"api", ApiVersionHandler)
478 ]
482 ]
@@ -1,103 +1,120 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'base/js/utils',
6 'base/js/utils',
7 'jquery',
7 'jquery',
8 'tree/js/notebooklist',
8 'tree/js/notebooklist',
9 ], function(IPython, utils, $, notebooklist) {
9 ], function(IPython, utils, $, notebooklist) {
10 "use strict";
10 "use strict";
11
11
12 var TerminalList = function (selector, options) {
12 var TerminalList = function (selector, options) {
13 // Constructor
13 // Constructor
14 //
14 //
15 // Parameters:
15 // Parameters:
16 // selector: string
16 // selector: string
17 // options: dictionary
17 // options: dictionary
18 // Dictionary of keyword arguments.
18 // Dictionary of keyword arguments.
19 // base_url: string
19 // base_url: string
20 this.base_url = options.base_url || utils.get_body_data("baseUrl");
20 this.base_url = options.base_url || utils.get_body_data("baseUrl");
21 this.element_name = options.element_name || 'terminal';
21 this.element_name = options.element_name || 'terminal';
22 this.selector = selector;
22 this.selector = selector;
23 this.terminals = [];
23 this.terminals = [];
24 if (this.selector !== undefined) {
24 if (this.selector !== undefined) {
25 this.element = $(selector);
25 this.element = $(selector);
26 this.style();
26 this.style();
27 this.bind_events();
27 this.bind_events();
28 this.load_terminals();
28 this.load_terminals();
29 }
29 }
30 };
30 };
31
31
32 TerminalList.prototype = Object.create(notebooklist.NotebookList.prototype);
32 TerminalList.prototype = Object.create(notebooklist.NotebookList.prototype);
33
33
34 TerminalList.prototype.bind_events = function () {
34 TerminalList.prototype.bind_events = function () {
35 var that = this;
35 var that = this;
36 $('#refresh_' + this.element_name + '_list').click(function () {
36 $('#refresh_' + this.element_name + '_list').click(function () {
37 that.load_terminals();
37 that.load_terminals();
38 });
38 });
39 $('#new_terminal').click($.proxy(this.new_terminal, this));
39 $('#new_terminal').click($.proxy(this.new_terminal, this));
40 };
40 };
41
41
42 TerminalList.prototype.new_terminal = function() {
42 TerminalList.prototype.new_terminal = function () {
43 var url = utils.url_join_encode(this.base_url, 'terminals/new');
43 var w = window.open();
44 window.open(url, '_blank');
44 var base_url = this.base_url;
45 var settings = {
46 type : "POST",
47 dataType: "json",
48 success : function (data, status, xhr) {
49 var name = data.name;
50 w.location = utils.url_join_encode(base_url, 'terminals', name);
51 },
52 error : function(jqXHR, status, error){
53 w.close();
54 utils.log_ajax_error(jqXHR, status, error);
55 },
56 };
57 var url = utils.url_join_encode(
58 this.base_url,
59 'api/terminals'
60 );
61 $.ajax(url, settings);
45 };
62 };
46
63
47 TerminalList.prototype.load_terminals = function() {
64 TerminalList.prototype.load_terminals = function() {
48 var that = this;
65 var that = this;
49 var url = utils.url_join_encode(this.base_url, 'api/terminals');
66 var url = utils.url_join_encode(this.base_url, 'api/terminals');
50 $.ajax(url, {
67 $.ajax(url, {
51 type: "GET",
68 type: "GET",
52 cache: false,
69 cache: false,
53 dataType: "json",
70 dataType: "json",
54 success: $.proxy(this.terminals_loaded, this),
71 success: $.proxy(this.terminals_loaded, this),
55 error : utils.log_ajax_error
72 error : utils.log_ajax_error
56 });
73 });
57 };
74 };
58
75
59 TerminalList.prototype.terminals_loaded = function (data) {
76 TerminalList.prototype.terminals_loaded = function (data) {
60 this.terminals = data;
77 this.terminals = data;
61 this.clear_list();
78 this.clear_list();
62 var item, path_name, term;
79 var item, path_name, term;
63 for (var i=0; i < this.terminals.length; i++) {
80 for (var i=0; i < this.terminals.length; i++) {
64 term = this.terminals[i];
81 term = this.terminals[i];
65 item = this.new_item(-1);
82 item = this.new_item(-1);
66 this.add_link(term.name, item);
83 this.add_link(term.name, item);
67 this.add_shutdown_button(term.name, item);
84 this.add_shutdown_button(term.name, item);
68 }
85 }
69 $('#terminal_list_header').toggle(data.length === 0);
86 $('#terminal_list_header').toggle(data.length === 0);
70 };
87 };
71
88
72 TerminalList.prototype.add_link = function(name, item) {
89 TerminalList.prototype.add_link = function(name, item) {
73 item.data('term-name', name);
90 item.data('term-name', name);
74 item.find(".item_name").text("terminals/" + name);
91 item.find(".item_name").text("terminals/" + name);
75 item.find(".item_icon").addClass("fa fa-terminal");
92 item.find(".item_icon").addClass("fa fa-terminal");
76 var link = item.find("a.item_link")
93 var link = item.find("a.item_link")
77 .attr('href', utils.url_join_encode(this.base_url, "terminals", name));
94 .attr('href', utils.url_join_encode(this.base_url, "terminals", name));
78 link.attr('target', '_blank');
95 link.attr('target', '_blank');
79 this.add_shutdown_button(name, item);
96 this.add_shutdown_button(name, item);
80 };
97 };
81
98
82 TerminalList.prototype.add_shutdown_button = function(name, item) {
99 TerminalList.prototype.add_shutdown_button = function(name, item) {
83 var that = this;
100 var that = this;
84 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
101 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
85 click(function (e) {
102 click(function (e) {
86 var settings = {
103 var settings = {
87 processData : false,
104 processData : false,
88 type : "DELETE",
105 type : "DELETE",
89 dataType : "json",
106 dataType : "json",
90 success : function () {
107 success : function () {
91 that.load_terminals();
108 that.load_terminals();
92 },
109 },
93 error : utils.log_ajax_error,
110 error : utils.log_ajax_error,
94 };
111 };
95 var url = utils.url_join_encode(that.base_url, 'api/terminals', name);
112 var url = utils.url_join_encode(that.base_url, 'api/terminals', name);
96 $.ajax(url, settings);
113 $.ajax(url, settings);
97 return false;
114 return false;
98 });
115 });
99 item.find(".item_buttons").text("").append(shutdown_button);
116 item.find(".item_buttons").text("").append(shutdown_button);
100 };
117 };
101
118
102 return {TerminalList: TerminalList};
119 return {TerminalList: TerminalList};
103 });
120 });
@@ -1,19 +1,18 b''
1 import os
1 import os
2 from terminado import NamedTermManager
2 from terminado import NamedTermManager
3 from IPython.html.utils import url_path_join as ujoin
3 from IPython.html.utils import url_path_join as ujoin
4 from .handlers import TerminalHandler, NewTerminalHandler, TermSocket
4 from .handlers import TerminalHandler, TermSocket
5 from . import api_handlers
5 from . import api_handlers
6
6
7 def initialize(webapp):
7 def initialize(webapp):
8 shell = os.environ.get('SHELL', 'sh')
8 shell = os.environ.get('SHELL', 'sh')
9 webapp.terminal_manager = NamedTermManager(shell_command=[shell])
9 terminal_manager = webapp.settings['terminal_manager'] = NamedTermManager(shell_command=[shell])
10 base_url = webapp.settings['base_url']
10 base_url = webapp.settings['base_url']
11 handlers = [
11 handlers = [
12 (ujoin(base_url, "/terminals/new"), NewTerminalHandler),
13 (ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
12 (ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
14 (ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
13 (ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
15 {'term_manager': webapp.terminal_manager}),
14 {'term_manager': terminal_manager}),
16 (ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
15 (ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
17 (ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
16 (ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
18 ]
17 ]
19 webapp.add_handlers(".*$", handlers) No newline at end of file
18 webapp.add_handlers(".*$", handlers)
@@ -1,35 +1,44 b''
1 import json
1 import json
2 from tornado import web
2 from tornado import web
3 from ..base.handlers import IPythonHandler, json_errors
3 from ..base.handlers import IPythonHandler, json_errors
4 from ..utils import url_path_join
4
5
5 class TerminalRootHandler(IPythonHandler):
6 class TerminalRootHandler(IPythonHandler):
6 @web.authenticated
7 @web.authenticated
7 @json_errors
8 @json_errors
8 def get(self):
9 def get(self):
9 tm = self.application.terminal_manager
10 tm = self.terminal_manager
10 terms = [{'name': name} for name in tm.terminals]
11 terms = [{'name': name} for name in tm.terminals]
11 self.finish(json.dumps(terms))
12 self.finish(json.dumps(terms))
12
13
14 @web.authenticated
15 @json_errors
16 def post(self):
17 """POST /terminals creates a new terminal and redirects to it"""
18 name, _ = self.terminal_manager.new_named_terminal()
19 self.finish(json.dumps({'name': name}))
20
21
13 class TerminalHandler(IPythonHandler):
22 class TerminalHandler(IPythonHandler):
14 SUPPORTED_METHODS = ('GET', 'DELETE')
23 SUPPORTED_METHODS = ('GET', 'DELETE')
15
24
16 @web.authenticated
25 @web.authenticated
17 @json_errors
26 @json_errors
18 def get(self, name):
27 def get(self, name):
19 tm = self.application.terminal_manager
28 tm = self.terminal_manager
20 if name in tm.terminals:
29 if name in tm.terminals:
21 self.finish(json.dumps({'name': name}))
30 self.finish(json.dumps({'name': name}))
22 else:
31 else:
23 raise web.HTTPError(404, "Terminal not found: %r" % name)
32 raise web.HTTPError(404, "Terminal not found: %r" % name)
24
33
25 @web.authenticated
34 @web.authenticated
26 @json_errors
35 @json_errors
27 def delete(self, name):
36 def delete(self, name):
28 tm = self.application.terminal_manager
37 tm = self.terminal_manager
29 if name in tm.terminals:
38 if name in tm.terminals:
30 tm.kill(name)
39 tm.kill(name)
31 # XXX: Should this wait for terminal to finish before returning?
40 # XXX: Should this wait for terminal to finish before returning?
32 self.set_status(204)
41 self.set_status(204)
33 self.finish()
42 self.finish()
34 else:
43 else:
35 raise web.HTTPError(404, "Terminal not found: %r" % name) No newline at end of file
44 raise web.HTTPError(404, "Terminal not found: %r" % name)
@@ -1,48 +1,41 b''
1 #encoding: utf-8
1 #encoding: utf-8
2 """Tornado handlers for the terminal emulator."""
2 """Tornado handlers for the terminal emulator."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 import tornado
7 import tornado
8 from tornado import web
8 from tornado import web
9 import terminado
9 import terminado
10 from ..base.handlers import IPythonHandler
10 from ..base.handlers import IPythonHandler
11
11
12 class TerminalHandler(IPythonHandler):
12 class TerminalHandler(IPythonHandler):
13 """Render the terminal interface."""
13 """Render the terminal interface."""
14 @web.authenticated
14 @web.authenticated
15 def get(self, term_name):
15 def get(self, term_name):
16 self.write(self.render_template('terminal.html',
16 self.write(self.render_template('terminal.html',
17 ws_path="terminals/websocket/%s" % term_name))
17 ws_path="terminals/websocket/%s" % term_name))
18
18
19 class NewTerminalHandler(IPythonHandler):
20 """Redirect to a new terminal."""
21 @web.authenticated
22 def get(self):
23 name, _ = self.application.terminal_manager.new_named_terminal()
24 self.redirect(name, permanent=False)
25
26 class TermSocket(terminado.TermSocket, IPythonHandler):
19 class TermSocket(terminado.TermSocket, IPythonHandler):
27 def get(self, *args, **kwargs):
20 def get(self, *args, **kwargs):
28 if not self.get_current_user():
21 if not self.get_current_user():
29 raise web.HTTPError(403)
22 raise web.HTTPError(403)
30
23
31 # FIXME: only do super get on tornado ≥ 4
24 # FIXME: only do super get on tornado ≥ 4
32 # tornado 3 has no get, will raise 405
25 # tornado 3 has no get, will raise 405
33 if tornado.version_info >= (4,):
26 if tornado.version_info >= (4,):
34 return super(TermSocket, self).get(*args, **kwargs)
27 return super(TermSocket, self).get(*args, **kwargs)
35
28
36 def clear_cookie(self, *args, **kwargs):
29 def clear_cookie(self, *args, **kwargs):
37 """meaningless for websockets"""
30 """meaningless for websockets"""
38 pass
31 pass
39
32
40 def open(self, *args, **kwargs):
33 def open(self, *args, **kwargs):
41 if tornado.version_info < (4,):
34 if tornado.version_info < (4,):
42 try:
35 try:
43 self.get(*self.open_args, **self.open_kwargs)
36 self.get(*self.open_args, **self.open_kwargs)
44 except web.HTTPError:
37 except web.HTTPError:
45 self.close()
38 self.close()
46 raise
39 raise
47
40
48 super(TermSocket, self).open(*args, **kwargs)
41 super(TermSocket, self).open(*args, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now