##// END OF EJS Templates
Merge branch csp into 3.x...
Min RK -
r21487:7222bd53 merge
parent child Browse files
Show More
@@ -1,528 +1,553 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 from tornado import gen
22 from tornado import gen
23 from tornado.log import app_log
23 from tornado.log import app_log
24
24
25
25
26 import IPython
26 import IPython
27 from IPython.utils.sysinfo import get_sys_info
27 from IPython.utils.sysinfo import get_sys_info
28
28
29 from IPython.config import Application
29 from IPython.config import Application
30 from IPython.utils.path import filefind
30 from IPython.utils.path import filefind
31 from IPython.utils.py3compat import string_types
31 from IPython.utils.py3compat import string_types
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
33
33
34 from IPython.html.services.security import csp_report_uri
34 from IPython.html.services.security import csp_report_uri
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Top-level handlers
37 # Top-level handlers
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40
40
41 sys_info = json.dumps(get_sys_info())
41 sys_info = json.dumps(get_sys_info())
42
42
43 class AuthenticatedHandler(web.RequestHandler):
43 class AuthenticatedHandler(web.RequestHandler):
44 """A RequestHandler with an authenticated user."""
44 """A RequestHandler with an authenticated user."""
45
45
46 @property
47 def content_security_policy(self):
48 """The default Content-Security-Policy header
49
50 Can be overridden by defining Content-Security-Policy in settings['headers']
51 """
52 return '; '.join([
53 "frame-ancestors 'self'",
54 # Make sure the report-uri is relative to the base_url
55 "report-uri " + url_path_join(self.base_url, csp_report_uri),
56 ])
57
46 def set_default_headers(self):
58 def set_default_headers(self):
47 headers = self.settings.get('headers', {})
59 headers = self.settings.get('headers', {})
48
60
49 if "Content-Security-Policy" not in headers:
61 if "Content-Security-Policy" not in headers:
50 headers["Content-Security-Policy"] = (
62 headers["Content-Security-Policy"] = self.content_security_policy
51 "frame-ancestors 'self'; "
52 # Make sure the report-uri is relative to the base_url
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 )
55
63
56 # Allow for overriding headers
64 # Allow for overriding headers
57 for header_name,value in headers.items() :
65 for header_name,value in headers.items() :
58 try:
66 try:
59 self.set_header(header_name, value)
67 self.set_header(header_name, value)
60 except Exception as e:
68 except Exception as e:
61 # tornado raise Exception (not a subclass)
69 # tornado raise Exception (not a subclass)
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
70 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # for example, so just ignore)
71 # for example, so just ignore)
64 self.log.debug(e)
72 self.log.debug(e)
65
73
66 def clear_login_cookie(self):
74 def clear_login_cookie(self):
67 self.clear_cookie(self.cookie_name)
75 self.clear_cookie(self.cookie_name)
68
76
69 def get_current_user(self):
77 def get_current_user(self):
70 if self.login_handler is None:
78 if self.login_handler is None:
71 return 'anonymous'
79 return 'anonymous'
72 return self.login_handler.get_user(self)
80 return self.login_handler.get_user(self)
73
81
74 @property
82 @property
75 def cookie_name(self):
83 def cookie_name(self):
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 self.request.host
85 self.request.host
78 ))
86 ))
79 return self.settings.get('cookie_name', default_cookie_name)
87 return self.settings.get('cookie_name', default_cookie_name)
80
88
81 @property
89 @property
82 def logged_in(self):
90 def logged_in(self):
83 """Is a user currently logged in?"""
91 """Is a user currently logged in?"""
84 user = self.get_current_user()
92 user = self.get_current_user()
85 return (user and not user == 'anonymous')
93 return (user and not user == 'anonymous')
86
94
87 @property
95 @property
88 def login_handler(self):
96 def login_handler(self):
89 """Return the login handler for this application, if any."""
97 """Return the login handler for this application, if any."""
90 return self.settings.get('login_handler_class', None)
98 return self.settings.get('login_handler_class', None)
91
99
92 @property
100 @property
93 def login_available(self):
101 def login_available(self):
94 """May a user proceed to log in?
102 """May a user proceed to log in?
95
103
96 This returns True if login capability is available, irrespective of
104 This returns True if login capability is available, irrespective of
97 whether the user is already logged in or not.
105 whether the user is already logged in or not.
98
106
99 """
107 """
100 if self.login_handler is None:
108 if self.login_handler is None:
101 return False
109 return False
102 return bool(self.login_handler.login_available(self.settings))
110 return bool(self.login_handler.login_available(self.settings))
103
111
104
112
105 class IPythonHandler(AuthenticatedHandler):
113 class IPythonHandler(AuthenticatedHandler):
106 """IPython-specific extensions to authenticated handling
114 """IPython-specific extensions to authenticated handling
107
115
108 Mostly property shortcuts to IPython-specific settings.
116 Mostly property shortcuts to IPython-specific settings.
109 """
117 """
110
118
111 @property
119 @property
112 def config(self):
120 def config(self):
113 return self.settings.get('config', None)
121 return self.settings.get('config', None)
114
122
115 @property
123 @property
116 def log(self):
124 def log(self):
117 """use the IPython log by default, falling back on tornado's logger"""
125 """use the IPython log by default, falling back on tornado's logger"""
118 if Application.initialized():
126 if Application.initialized():
119 return Application.instance().log
127 return Application.instance().log
120 else:
128 else:
121 return app_log
129 return app_log
122
130
123 @property
131 @property
124 def jinja_template_vars(self):
132 def jinja_template_vars(self):
125 """User-supplied values to supply to jinja templates."""
133 """User-supplied values to supply to jinja templates."""
126 return self.settings.get('jinja_template_vars', {})
134 return self.settings.get('jinja_template_vars', {})
127
135
128 #---------------------------------------------------------------
136 #---------------------------------------------------------------
129 # URLs
137 # URLs
130 #---------------------------------------------------------------
138 #---------------------------------------------------------------
131
139
132 @property
140 @property
133 def version_hash(self):
141 def version_hash(self):
134 """The version hash to use for cache hints for static files"""
142 """The version hash to use for cache hints for static files"""
135 return self.settings.get('version_hash', '')
143 return self.settings.get('version_hash', '')
136
144
137 @property
145 @property
138 def mathjax_url(self):
146 def mathjax_url(self):
139 return self.settings.get('mathjax_url', '')
147 return self.settings.get('mathjax_url', '')
140
148
141 @property
149 @property
142 def base_url(self):
150 def base_url(self):
143 return self.settings.get('base_url', '/')
151 return self.settings.get('base_url', '/')
144
152
145 @property
153 @property
146 def default_url(self):
154 def default_url(self):
147 return self.settings.get('default_url', '')
155 return self.settings.get('default_url', '')
148
156
149 @property
157 @property
150 def ws_url(self):
158 def ws_url(self):
151 return self.settings.get('websocket_url', '')
159 return self.settings.get('websocket_url', '')
152
160
153 @property
161 @property
154 def contents_js_source(self):
162 def contents_js_source(self):
155 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
163 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
156 'services/contents'))
164 'services/contents'))
157 return self.settings.get('contents_js_source', 'services/contents')
165 return self.settings.get('contents_js_source', 'services/contents')
158
166
159 #---------------------------------------------------------------
167 #---------------------------------------------------------------
160 # Manager objects
168 # Manager objects
161 #---------------------------------------------------------------
169 #---------------------------------------------------------------
162
170
163 @property
171 @property
164 def kernel_manager(self):
172 def kernel_manager(self):
165 return self.settings['kernel_manager']
173 return self.settings['kernel_manager']
166
174
167 @property
175 @property
168 def contents_manager(self):
176 def contents_manager(self):
169 return self.settings['contents_manager']
177 return self.settings['contents_manager']
170
178
171 @property
179 @property
172 def cluster_manager(self):
180 def cluster_manager(self):
173 return self.settings['cluster_manager']
181 return self.settings['cluster_manager']
174
182
175 @property
183 @property
176 def session_manager(self):
184 def session_manager(self):
177 return self.settings['session_manager']
185 return self.settings['session_manager']
178
186
179 @property
187 @property
180 def terminal_manager(self):
188 def terminal_manager(self):
181 return self.settings['terminal_manager']
189 return self.settings['terminal_manager']
182
190
183 @property
191 @property
184 def kernel_spec_manager(self):
192 def kernel_spec_manager(self):
185 return self.settings['kernel_spec_manager']
193 return self.settings['kernel_spec_manager']
186
194
187 @property
195 @property
188 def config_manager(self):
196 def config_manager(self):
189 return self.settings['config_manager']
197 return self.settings['config_manager']
190
198
191 #---------------------------------------------------------------
199 #---------------------------------------------------------------
192 # CORS
200 # CORS
193 #---------------------------------------------------------------
201 #---------------------------------------------------------------
194
202
195 @property
203 @property
196 def allow_origin(self):
204 def allow_origin(self):
197 """Normal Access-Control-Allow-Origin"""
205 """Normal Access-Control-Allow-Origin"""
198 return self.settings.get('allow_origin', '')
206 return self.settings.get('allow_origin', '')
199
207
200 @property
208 @property
201 def allow_origin_pat(self):
209 def allow_origin_pat(self):
202 """Regular expression version of allow_origin"""
210 """Regular expression version of allow_origin"""
203 return self.settings.get('allow_origin_pat', None)
211 return self.settings.get('allow_origin_pat', None)
204
212
205 @property
213 @property
206 def allow_credentials(self):
214 def allow_credentials(self):
207 """Whether to set Access-Control-Allow-Credentials"""
215 """Whether to set Access-Control-Allow-Credentials"""
208 return self.settings.get('allow_credentials', False)
216 return self.settings.get('allow_credentials', False)
209
217
210 def set_default_headers(self):
218 def set_default_headers(self):
211 """Add CORS headers, if defined"""
219 """Add CORS headers, if defined"""
212 super(IPythonHandler, self).set_default_headers()
220 super(IPythonHandler, self).set_default_headers()
213 if self.allow_origin:
221 if self.allow_origin:
214 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
222 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
215 elif self.allow_origin_pat:
223 elif self.allow_origin_pat:
216 origin = self.get_origin()
224 origin = self.get_origin()
217 if origin and self.allow_origin_pat.match(origin):
225 if origin and self.allow_origin_pat.match(origin):
218 self.set_header("Access-Control-Allow-Origin", origin)
226 self.set_header("Access-Control-Allow-Origin", origin)
219 if self.allow_credentials:
227 if self.allow_credentials:
220 self.set_header("Access-Control-Allow-Credentials", 'true')
228 self.set_header("Access-Control-Allow-Credentials", 'true')
221
229
222 def get_origin(self):
230 def get_origin(self):
223 # Handle WebSocket Origin naming convention differences
231 # Handle WebSocket Origin naming convention differences
224 # The difference between version 8 and 13 is that in 8 the
232 # The difference between version 8 and 13 is that in 8 the
225 # client sends a "Sec-Websocket-Origin" header and in 13 it's
233 # client sends a "Sec-Websocket-Origin" header and in 13 it's
226 # simply "Origin".
234 # simply "Origin".
227 if "Origin" in self.request.headers:
235 if "Origin" in self.request.headers:
228 origin = self.request.headers.get("Origin")
236 origin = self.request.headers.get("Origin")
229 else:
237 else:
230 origin = self.request.headers.get("Sec-Websocket-Origin", None)
238 origin = self.request.headers.get("Sec-Websocket-Origin", None)
231 return origin
239 return origin
232
240
233 #---------------------------------------------------------------
241 #---------------------------------------------------------------
234 # template rendering
242 # template rendering
235 #---------------------------------------------------------------
243 #---------------------------------------------------------------
236
244
237 def get_template(self, name):
245 def get_template(self, name):
238 """Return the jinja template object for a given name"""
246 """Return the jinja template object for a given name"""
239 return self.settings['jinja2_env'].get_template(name)
247 return self.settings['jinja2_env'].get_template(name)
240
248
241 def render_template(self, name, **ns):
249 def render_template(self, name, **ns):
242 ns.update(self.template_namespace)
250 ns.update(self.template_namespace)
243 template = self.get_template(name)
251 template = self.get_template(name)
244 return template.render(**ns)
252 return template.render(**ns)
245
253
246 @property
254 @property
247 def template_namespace(self):
255 def template_namespace(self):
248 return dict(
256 return dict(
249 base_url=self.base_url,
257 base_url=self.base_url,
250 default_url=self.default_url,
258 default_url=self.default_url,
251 ws_url=self.ws_url,
259 ws_url=self.ws_url,
252 logged_in=self.logged_in,
260 logged_in=self.logged_in,
253 login_available=self.login_available,
261 login_available=self.login_available,
254 static_url=self.static_url,
262 static_url=self.static_url,
255 sys_info=sys_info,
263 sys_info=sys_info,
256 contents_js_source=self.contents_js_source,
264 contents_js_source=self.contents_js_source,
257 version_hash=self.version_hash,
265 version_hash=self.version_hash,
258 **self.jinja_template_vars
266 **self.jinja_template_vars
259 )
267 )
260
268
261 def get_json_body(self):
269 def get_json_body(self):
262 """Return the body of the request as JSON data."""
270 """Return the body of the request as JSON data."""
263 if not self.request.body:
271 if not self.request.body:
264 return None
272 return None
265 # Do we need to call body.decode('utf-8') here?
273 # Do we need to call body.decode('utf-8') here?
266 body = self.request.body.strip().decode(u'utf-8')
274 body = self.request.body.strip().decode(u'utf-8')
267 try:
275 try:
268 model = json.loads(body)
276 model = json.loads(body)
269 except Exception:
277 except Exception:
270 self.log.debug("Bad JSON: %r", body)
278 self.log.debug("Bad JSON: %r", body)
271 self.log.error("Couldn't parse JSON", exc_info=True)
279 self.log.error("Couldn't parse JSON", exc_info=True)
272 raise web.HTTPError(400, u'Invalid JSON in body of request')
280 raise web.HTTPError(400, u'Invalid JSON in body of request')
273 return model
281 return model
274
282
275 def write_error(self, status_code, **kwargs):
283 def write_error(self, status_code, **kwargs):
276 """render custom error pages"""
284 """render custom error pages"""
277 exc_info = kwargs.get('exc_info')
285 exc_info = kwargs.get('exc_info')
278 message = ''
286 message = ''
279 status_message = responses.get(status_code, 'Unknown HTTP Error')
287 status_message = responses.get(status_code, 'Unknown HTTP Error')
280 if exc_info:
288 if exc_info:
281 exception = exc_info[1]
289 exception = exc_info[1]
282 # get the custom message, if defined
290 # get the custom message, if defined
283 try:
291 try:
284 message = exception.log_message % exception.args
292 message = exception.log_message % exception.args
285 except Exception:
293 except Exception:
286 pass
294 pass
287
295
288 # construct the custom reason, if defined
296 # construct the custom reason, if defined
289 reason = getattr(exception, 'reason', '')
297 reason = getattr(exception, 'reason', '')
290 if reason:
298 if reason:
291 status_message = reason
299 status_message = reason
292
300
293 # build template namespace
301 # build template namespace
294 ns = dict(
302 ns = dict(
295 status_code=status_code,
303 status_code=status_code,
296 status_message=status_message,
304 status_message=status_message,
297 message=message,
305 message=message,
298 exception=exception,
306 exception=exception,
299 )
307 )
300
308
301 self.set_header('Content-Type', 'text/html')
309 self.set_header('Content-Type', 'text/html')
302 # render the template
310 # render the template
303 try:
311 try:
304 html = self.render_template('%s.html' % status_code, **ns)
312 html = self.render_template('%s.html' % status_code, **ns)
305 except TemplateNotFound:
313 except TemplateNotFound:
306 self.log.debug("No template for %d", status_code)
314 self.log.debug("No template for %d", status_code)
307 html = self.render_template('error.html', **ns)
315 html = self.render_template('error.html', **ns)
308
316
309 self.write(html)
317 self.write(html)
310
318
311
319
320 class APIHandler(IPythonHandler):
321 """Base class for API handlers"""
322
323 @property
324 def content_security_policy(self):
325 csp = '; '.join([
326 super(APIHandler, self).content_security_policy,
327 "default-src 'none'",
328 ])
329 return csp
330
331 def finish(self, *args, **kwargs):
332 self.set_header('Content-Type', 'application/json')
333 return super(APIHandler, self).finish(*args, **kwargs)
334
312
335
313 class Template404(IPythonHandler):
336 class Template404(IPythonHandler):
314 """Render our 404 template"""
337 """Render our 404 template"""
315 def prepare(self):
338 def prepare(self):
316 raise web.HTTPError(404)
339 raise web.HTTPError(404)
317
340
318
341
319 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
342 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
320 """static files should only be accessible when logged in"""
343 """static files should only be accessible when logged in"""
321
344
322 @web.authenticated
345 @web.authenticated
323 def get(self, path):
346 def get(self, path):
324 if os.path.splitext(path)[1] == '.ipynb':
347 if os.path.splitext(path)[1] == '.ipynb':
325 name = path.rsplit('/', 1)[-1]
348 name = path.rsplit('/', 1)[-1]
326 self.set_header('Content-Type', 'application/json')
349 self.set_header('Content-Type', 'application/json')
327 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
350 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
328
351
329 return web.StaticFileHandler.get(self, path)
352 return web.StaticFileHandler.get(self, path)
330
353
331 def set_headers(self):
354 def set_headers(self):
332 super(AuthenticatedFileHandler, self).set_headers()
355 super(AuthenticatedFileHandler, self).set_headers()
333 # disable browser caching, rely on 304 replies for savings
356 # disable browser caching, rely on 304 replies for savings
334 if "v" not in self.request.arguments:
357 if "v" not in self.request.arguments:
335 self.add_header("Cache-Control", "no-cache")
358 self.add_header("Cache-Control", "no-cache")
336
359
337 def compute_etag(self):
360 def compute_etag(self):
338 return None
361 return None
339
362
340 def validate_absolute_path(self, root, absolute_path):
363 def validate_absolute_path(self, root, absolute_path):
341 """Validate and return the absolute path.
364 """Validate and return the absolute path.
342
365
343 Requires tornado 3.1
366 Requires tornado 3.1
344
367
345 Adding to tornado's own handling, forbids the serving of hidden files.
368 Adding to tornado's own handling, forbids the serving of hidden files.
346 """
369 """
347 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
370 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
348 abs_root = os.path.abspath(root)
371 abs_root = os.path.abspath(root)
349 if is_hidden(abs_path, abs_root):
372 if is_hidden(abs_path, abs_root):
350 self.log.info("Refusing to serve hidden file, via 404 Error")
373 self.log.info("Refusing to serve hidden file, via 404 Error")
351 raise web.HTTPError(404)
374 raise web.HTTPError(404)
352 return abs_path
375 return abs_path
353
376
354
377
355 def json_errors(method):
378 def json_errors(method):
356 """Decorate methods with this to return GitHub style JSON errors.
379 """Decorate methods with this to return GitHub style JSON errors.
357
380
358 This should be used on any JSON API on any handler method that can raise HTTPErrors.
381 This should be used on any JSON API on any handler method that can raise HTTPErrors.
359
382
360 This will grab the latest HTTPError exception using sys.exc_info
383 This will grab the latest HTTPError exception using sys.exc_info
361 and then:
384 and then:
362
385
363 1. Set the HTTP status code based on the HTTPError
386 1. Set the HTTP status code based on the HTTPError
364 2. Create and return a JSON body with a message field describing
387 2. Create and return a JSON body with a message field describing
365 the error in a human readable form.
388 the error in a human readable form.
366 """
389 """
367 @functools.wraps(method)
390 @functools.wraps(method)
368 @gen.coroutine
391 @gen.coroutine
369 def wrapper(self, *args, **kwargs):
392 def wrapper(self, *args, **kwargs):
370 try:
393 try:
371 result = yield gen.maybe_future(method(self, *args, **kwargs))
394 result = yield gen.maybe_future(method(self, *args, **kwargs))
372 except web.HTTPError as e:
395 except web.HTTPError as e:
396 self.set_header('Content-Type', 'application/json')
373 status = e.status_code
397 status = e.status_code
374 message = e.log_message
398 message = e.log_message
375 self.log.warn(message)
399 self.log.warn(message)
376 self.set_status(e.status_code)
400 self.set_status(e.status_code)
377 reply = dict(message=message, reason=e.reason)
401 reply = dict(message=message, reason=e.reason)
378 self.finish(json.dumps(reply))
402 self.finish(json.dumps(reply))
379 except Exception:
403 except Exception:
404 self.set_header('Content-Type', 'application/json')
380 self.log.error("Unhandled error in API request", exc_info=True)
405 self.log.error("Unhandled error in API request", exc_info=True)
381 status = 500
406 status = 500
382 message = "Unknown server error"
407 message = "Unknown server error"
383 t, value, tb = sys.exc_info()
408 t, value, tb = sys.exc_info()
384 self.set_status(status)
409 self.set_status(status)
385 tb_text = ''.join(traceback.format_exception(t, value, tb))
410 tb_text = ''.join(traceback.format_exception(t, value, tb))
386 reply = dict(message=message, reason=None, traceback=tb_text)
411 reply = dict(message=message, reason=None, traceback=tb_text)
387 self.finish(json.dumps(reply))
412 self.finish(json.dumps(reply))
388 else:
413 else:
389 # FIXME: can use regular return in generators in py3
414 # FIXME: can use regular return in generators in py3
390 raise gen.Return(result)
415 raise gen.Return(result)
391 return wrapper
416 return wrapper
392
417
393
418
394
419
395 #-----------------------------------------------------------------------------
420 #-----------------------------------------------------------------------------
396 # File handler
421 # File handler
397 #-----------------------------------------------------------------------------
422 #-----------------------------------------------------------------------------
398
423
399 # to minimize subclass changes:
424 # to minimize subclass changes:
400 HTTPError = web.HTTPError
425 HTTPError = web.HTTPError
401
426
402 class FileFindHandler(web.StaticFileHandler):
427 class FileFindHandler(IPythonHandler, web.StaticFileHandler):
403 """subclass of StaticFileHandler for serving files from a search path"""
428 """subclass of StaticFileHandler for serving files from a search path"""
404
429
405 # cache search results, don't search for files more than once
430 # cache search results, don't search for files more than once
406 _static_paths = {}
431 _static_paths = {}
407
432
408 def set_headers(self):
433 def set_headers(self):
409 super(FileFindHandler, self).set_headers()
434 super(FileFindHandler, self).set_headers()
410 # disable browser caching, rely on 304 replies for savings
435 # disable browser caching, rely on 304 replies for savings
411 if "v" not in self.request.arguments or \
436 if "v" not in self.request.arguments or \
412 any(self.request.path.startswith(path) for path in self.no_cache_paths):
437 any(self.request.path.startswith(path) for path in self.no_cache_paths):
413 self.set_header("Cache-Control", "no-cache")
438 self.set_header("Cache-Control", "no-cache")
414
439
415 def initialize(self, path, default_filename=None, no_cache_paths=None):
440 def initialize(self, path, default_filename=None, no_cache_paths=None):
416 self.no_cache_paths = no_cache_paths or []
441 self.no_cache_paths = no_cache_paths or []
417
442
418 if isinstance(path, string_types):
443 if isinstance(path, string_types):
419 path = [path]
444 path = [path]
420
445
421 self.root = tuple(
446 self.root = tuple(
422 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
447 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
423 )
448 )
424 self.default_filename = default_filename
449 self.default_filename = default_filename
425
450
426 def compute_etag(self):
451 def compute_etag(self):
427 return None
452 return None
428
453
429 @classmethod
454 @classmethod
430 def get_absolute_path(cls, roots, path):
455 def get_absolute_path(cls, roots, path):
431 """locate a file to serve on our static file search path"""
456 """locate a file to serve on our static file search path"""
432 with cls._lock:
457 with cls._lock:
433 if path in cls._static_paths:
458 if path in cls._static_paths:
434 return cls._static_paths[path]
459 return cls._static_paths[path]
435 try:
460 try:
436 abspath = os.path.abspath(filefind(path, roots))
461 abspath = os.path.abspath(filefind(path, roots))
437 except IOError:
462 except IOError:
438 # IOError means not found
463 # IOError means not found
439 return ''
464 return ''
440
465
441 cls._static_paths[path] = abspath
466 cls._static_paths[path] = abspath
442 return abspath
467 return abspath
443
468
444 def validate_absolute_path(self, root, absolute_path):
469 def validate_absolute_path(self, root, absolute_path):
445 """check if the file should be served (raises 404, 403, etc.)"""
470 """check if the file should be served (raises 404, 403, etc.)"""
446 if absolute_path == '':
471 if absolute_path == '':
447 raise web.HTTPError(404)
472 raise web.HTTPError(404)
448
473
449 for root in self.root:
474 for root in self.root:
450 if (absolute_path + os.sep).startswith(root):
475 if (absolute_path + os.sep).startswith(root):
451 break
476 break
452
477
453 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
478 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
454
479
455
480
456 class ApiVersionHandler(IPythonHandler):
481 class APIVersionHandler(APIHandler):
457
482
458 @json_errors
483 @json_errors
459 def get(self):
484 def get(self):
460 # not authenticated, so give as few info as possible
485 # not authenticated, so give as few info as possible
461 self.finish(json.dumps({"version":IPython.__version__}))
486 self.finish(json.dumps({"version":IPython.__version__}))
462
487
463
488
464 class TrailingSlashHandler(web.RequestHandler):
489 class TrailingSlashHandler(web.RequestHandler):
465 """Simple redirect handler that strips trailing slashes
490 """Simple redirect handler that strips trailing slashes
466
491
467 This should be the first, highest priority handler.
492 This should be the first, highest priority handler.
468 """
493 """
469
494
470 def get(self):
495 def get(self):
471 self.redirect(self.request.uri.rstrip('/'))
496 self.redirect(self.request.uri.rstrip('/'))
472
497
473 post = put = get
498 post = put = get
474
499
475
500
476 class FilesRedirectHandler(IPythonHandler):
501 class FilesRedirectHandler(IPythonHandler):
477 """Handler for redirecting relative URLs to the /files/ handler"""
502 """Handler for redirecting relative URLs to the /files/ handler"""
478
503
479 @staticmethod
504 @staticmethod
480 def redirect_to_files(self, path):
505 def redirect_to_files(self, path):
481 """make redirect logic a reusable static method
506 """make redirect logic a reusable static method
482
507
483 so it can be called from other handlers.
508 so it can be called from other handlers.
484 """
509 """
485 cm = self.contents_manager
510 cm = self.contents_manager
486 if cm.dir_exists(path):
511 if cm.dir_exists(path):
487 # it's a *directory*, redirect to /tree
512 # it's a *directory*, redirect to /tree
488 url = url_path_join(self.base_url, 'tree', path)
513 url = url_path_join(self.base_url, 'tree', path)
489 else:
514 else:
490 orig_path = path
515 orig_path = path
491 # otherwise, redirect to /files
516 # otherwise, redirect to /files
492 parts = path.split('/')
517 parts = path.split('/')
493
518
494 if not cm.file_exists(path=path) and 'files' in parts:
519 if not cm.file_exists(path=path) and 'files' in parts:
495 # redirect without files/ iff it would 404
520 # redirect without files/ iff it would 404
496 # this preserves pre-2.0-style 'files/' links
521 # this preserves pre-2.0-style 'files/' links
497 self.log.warn("Deprecated files/ URL: %s", orig_path)
522 self.log.warn("Deprecated files/ URL: %s", orig_path)
498 parts.remove('files')
523 parts.remove('files')
499 path = '/'.join(parts)
524 path = '/'.join(parts)
500
525
501 if not cm.file_exists(path=path):
526 if not cm.file_exists(path=path):
502 raise web.HTTPError(404)
527 raise web.HTTPError(404)
503
528
504 url = url_path_join(self.base_url, 'files', path)
529 url = url_path_join(self.base_url, 'files', path)
505 url = url_escape(url)
530 url = url_escape(url)
506 self.log.debug("Redirecting %s to %s", self.request.path, url)
531 self.log.debug("Redirecting %s to %s", self.request.path, url)
507 self.redirect(url)
532 self.redirect(url)
508
533
509 def get(self, path=''):
534 def get(self, path=''):
510 return self.redirect_to_files(self, path)
535 return self.redirect_to_files(self, path)
511
536
512
537
513 #-----------------------------------------------------------------------------
538 #-----------------------------------------------------------------------------
514 # URL pattern fragments for re-use
539 # URL pattern fragments for re-use
515 #-----------------------------------------------------------------------------
540 #-----------------------------------------------------------------------------
516
541
517 # path matches any number of `/foo[/bar...]` or just `/` or ''
542 # path matches any number of `/foo[/bar...]` or just `/` or ''
518 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
543 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
519
544
520 #-----------------------------------------------------------------------------
545 #-----------------------------------------------------------------------------
521 # URL to handler mappings
546 # URL to handler mappings
522 #-----------------------------------------------------------------------------
547 #-----------------------------------------------------------------------------
523
548
524
549
525 default_handlers = [
550 default_handlers = [
526 (r".*/", TrailingSlashHandler),
551 (r".*/", TrailingSlashHandler),
527 (r"api", ApiVersionHandler)
552 (r"api", APIVersionHandler)
528 ]
553 ]
@@ -1,59 +1,59 b''
1 """Tornado handlers for cluster web service."""
1 """Tornado handlers for cluster web service."""
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 json
6 import json
7
7
8 from tornado import web
8 from tornado import web
9
9
10 from ...base.handlers import IPythonHandler
10 from ...base.handlers import APIHandler
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Cluster handlers
13 # Cluster handlers
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16
16
17 class MainClusterHandler(IPythonHandler):
17 class MainClusterHandler(APIHandler):
18
18
19 @web.authenticated
19 @web.authenticated
20 def get(self):
20 def get(self):
21 self.finish(json.dumps(self.cluster_manager.list_profiles()))
21 self.finish(json.dumps(self.cluster_manager.list_profiles()))
22
22
23
23
24 class ClusterProfileHandler(IPythonHandler):
24 class ClusterProfileHandler(APIHandler):
25
25
26 @web.authenticated
26 @web.authenticated
27 def get(self, profile):
27 def get(self, profile):
28 self.finish(json.dumps(self.cluster_manager.profile_info(profile)))
28 self.finish(json.dumps(self.cluster_manager.profile_info(profile)))
29
29
30
30
31 class ClusterActionHandler(IPythonHandler):
31 class ClusterActionHandler(APIHandler):
32
32
33 @web.authenticated
33 @web.authenticated
34 def post(self, profile, action):
34 def post(self, profile, action):
35 cm = self.cluster_manager
35 cm = self.cluster_manager
36 if action == 'start':
36 if action == 'start':
37 n = self.get_argument('n', default=None)
37 n = self.get_argument('n', default=None)
38 if not n:
38 if not n:
39 data = cm.start_cluster(profile)
39 data = cm.start_cluster(profile)
40 else:
40 else:
41 data = cm.start_cluster(profile, int(n))
41 data = cm.start_cluster(profile, int(n))
42 if action == 'stop':
42 if action == 'stop':
43 data = cm.stop_cluster(profile)
43 data = cm.stop_cluster(profile)
44 self.finish(json.dumps(data))
44 self.finish(json.dumps(data))
45
45
46
46
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48 # URL to handler mappings
48 # URL to handler mappings
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50
50
51
51
52 _cluster_action_regex = r"(?P<action>start|stop)"
52 _cluster_action_regex = r"(?P<action>start|stop)"
53 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
53 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
54
54
55 default_handlers = [
55 default_handlers = [
56 (r"/clusters", MainClusterHandler),
56 (r"/clusters", MainClusterHandler),
57 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
57 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
58 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
58 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
59 ]
59 ]
@@ -1,44 +1,44 b''
1 """Tornado handlers for frontend config storage."""
1 """Tornado handlers for frontend config storage."""
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 import json
5 import json
6 import os
6 import os
7 import io
7 import io
8 import errno
8 import errno
9 from tornado import web
9 from tornado import web
10
10
11 from IPython.utils.py3compat import PY3
11 from IPython.utils.py3compat import PY3
12 from ...base.handlers import IPythonHandler, json_errors
12 from ...base.handlers import APIHandler, json_errors
13
13
14 class ConfigHandler(IPythonHandler):
14 class ConfigHandler(APIHandler):
15 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH')
15 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH')
16
16
17 @web.authenticated
17 @web.authenticated
18 @json_errors
18 @json_errors
19 def get(self, section_name):
19 def get(self, section_name):
20 self.set_header("Content-Type", 'application/json')
20 self.set_header("Content-Type", 'application/json')
21 self.finish(json.dumps(self.config_manager.get(section_name)))
21 self.finish(json.dumps(self.config_manager.get(section_name)))
22
22
23 @web.authenticated
23 @web.authenticated
24 @json_errors
24 @json_errors
25 def put(self, section_name):
25 def put(self, section_name):
26 data = self.get_json_body() # Will raise 400 if content is not valid JSON
26 data = self.get_json_body() # Will raise 400 if content is not valid JSON
27 self.config_manager.set(section_name, data)
27 self.config_manager.set(section_name, data)
28 self.set_status(204)
28 self.set_status(204)
29
29
30 @web.authenticated
30 @web.authenticated
31 @json_errors
31 @json_errors
32 def patch(self, section_name):
32 def patch(self, section_name):
33 new_data = self.get_json_body()
33 new_data = self.get_json_body()
34 section = self.config_manager.update(section_name, new_data)
34 section = self.config_manager.update(section_name, new_data)
35 self.finish(json.dumps(section))
35 self.finish(json.dumps(section))
36
36
37
37
38 # URL to handler mappings
38 # URL to handler mappings
39
39
40 section_name_regex = r"(?P<section_name>\w+)"
40 section_name_regex = r"(?P<section_name>\w+)"
41
41
42 default_handlers = [
42 default_handlers = [
43 (r"/api/config/%s" % section_name_regex, ConfigHandler),
43 (r"/api/config/%s" % section_name_regex, ConfigHandler),
44 ]
44 ]
@@ -1,342 +1,342 b''
1 """Tornado handlers for the contents web service."""
1 """Tornado handlers for the contents web service."""
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 json
6 import json
7
7
8 from tornado import gen, web
8 from tornado import gen, web
9
9
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (
13 from IPython.html.base.handlers import (
14 IPythonHandler, json_errors, path_regex,
14 IPythonHandler, APIHandler, json_errors, path_regex,
15 )
15 )
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
19 """key function for case-insensitive sort by name and type"""
19 """key function for case-insensitive sort by name and type"""
20 iname = model['name'].lower()
20 iname = model['name'].lower()
21 type_key = {
21 type_key = {
22 'directory' : '0',
22 'directory' : '0',
23 'notebook' : '1',
23 'notebook' : '1',
24 'file' : '2',
24 'file' : '2',
25 }.get(model['type'], '9')
25 }.get(model['type'], '9')
26 return u'%s%s' % (type_key, iname)
26 return u'%s%s' % (type_key, iname)
27
27
28
28
29 def validate_model(model, expect_content):
29 def validate_model(model, expect_content):
30 """
30 """
31 Validate a model returned by a ContentsManager method.
31 Validate a model returned by a ContentsManager method.
32
32
33 If expect_content is True, then we expect non-null entries for 'content'
33 If expect_content is True, then we expect non-null entries for 'content'
34 and 'format'.
34 and 'format'.
35 """
35 """
36 required_keys = {
36 required_keys = {
37 "name",
37 "name",
38 "path",
38 "path",
39 "type",
39 "type",
40 "writable",
40 "writable",
41 "created",
41 "created",
42 "last_modified",
42 "last_modified",
43 "mimetype",
43 "mimetype",
44 "content",
44 "content",
45 "format",
45 "format",
46 }
46 }
47 missing = required_keys - set(model.keys())
47 missing = required_keys - set(model.keys())
48 if missing:
48 if missing:
49 raise web.HTTPError(
49 raise web.HTTPError(
50 500,
50 500,
51 u"Missing Model Keys: {missing}".format(missing=missing),
51 u"Missing Model Keys: {missing}".format(missing=missing),
52 )
52 )
53
53
54 maybe_none_keys = ['content', 'format']
54 maybe_none_keys = ['content', 'format']
55 if model['type'] == 'file':
55 if model['type'] == 'file':
56 # mimetype should be populated only for file models
56 # mimetype should be populated only for file models
57 maybe_none_keys.append('mimetype')
57 maybe_none_keys.append('mimetype')
58 if expect_content:
58 if expect_content:
59 errors = [key for key in maybe_none_keys if model[key] is None]
59 errors = [key for key in maybe_none_keys if model[key] is None]
60 if errors:
60 if errors:
61 raise web.HTTPError(
61 raise web.HTTPError(
62 500,
62 500,
63 u"Keys unexpectedly None: {keys}".format(keys=errors),
63 u"Keys unexpectedly None: {keys}".format(keys=errors),
64 )
64 )
65 else:
65 else:
66 errors = {
66 errors = {
67 key: model[key]
67 key: model[key]
68 for key in maybe_none_keys
68 for key in maybe_none_keys
69 if model[key] is not None
69 if model[key] is not None
70 }
70 }
71 if errors:
71 if errors:
72 raise web.HTTPError(
72 raise web.HTTPError(
73 500,
73 500,
74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
75 )
75 )
76
76
77
77
78 class ContentsHandler(IPythonHandler):
78 class ContentsHandler(APIHandler):
79
79
80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
81
81
82 def location_url(self, path):
82 def location_url(self, path):
83 """Return the full URL location of a file.
83 """Return the full URL location of a file.
84
84
85 Parameters
85 Parameters
86 ----------
86 ----------
87 path : unicode
87 path : unicode
88 The API path of the file, such as "foo/bar.txt".
88 The API path of the file, such as "foo/bar.txt".
89 """
89 """
90 return url_escape(url_path_join(
90 return url_escape(url_path_join(
91 self.base_url, 'api', 'contents', path
91 self.base_url, 'api', 'contents', path
92 ))
92 ))
93
93
94 def _finish_model(self, model, location=True):
94 def _finish_model(self, model, location=True):
95 """Finish a JSON request with a model, setting relevant headers, etc."""
95 """Finish a JSON request with a model, setting relevant headers, etc."""
96 if location:
96 if location:
97 location = self.location_url(model['path'])
97 location = self.location_url(model['path'])
98 self.set_header('Location', location)
98 self.set_header('Location', location)
99 self.set_header('Last-Modified', model['last_modified'])
99 self.set_header('Last-Modified', model['last_modified'])
100 self.set_header('Content-Type', 'application/json')
100 self.set_header('Content-Type', 'application/json')
101 self.finish(json.dumps(model, default=date_default))
101 self.finish(json.dumps(model, default=date_default))
102
102
103 @web.authenticated
103 @web.authenticated
104 @json_errors
104 @json_errors
105 @gen.coroutine
105 @gen.coroutine
106 def get(self, path=''):
106 def get(self, path=''):
107 """Return a model for a file or directory.
107 """Return a model for a file or directory.
108
108
109 A directory model contains a list of models (without content)
109 A directory model contains a list of models (without content)
110 of the files and directories it contains.
110 of the files and directories it contains.
111 """
111 """
112 path = path or ''
112 path = path or ''
113 type = self.get_query_argument('type', default=None)
113 type = self.get_query_argument('type', default=None)
114 if type not in {None, 'directory', 'file', 'notebook'}:
114 if type not in {None, 'directory', 'file', 'notebook'}:
115 raise web.HTTPError(400, u'Type %r is invalid' % type)
115 raise web.HTTPError(400, u'Type %r is invalid' % type)
116
116
117 format = self.get_query_argument('format', default=None)
117 format = self.get_query_argument('format', default=None)
118 if format not in {None, 'text', 'base64'}:
118 if format not in {None, 'text', 'base64'}:
119 raise web.HTTPError(400, u'Format %r is invalid' % format)
119 raise web.HTTPError(400, u'Format %r is invalid' % format)
120 content = self.get_query_argument('content', default='1')
120 content = self.get_query_argument('content', default='1')
121 if content not in {'0', '1'}:
121 if content not in {'0', '1'}:
122 raise web.HTTPError(400, u'Content %r is invalid' % content)
122 raise web.HTTPError(400, u'Content %r is invalid' % content)
123 content = int(content)
123 content = int(content)
124
124
125 model = yield gen.maybe_future(self.contents_manager.get(
125 model = yield gen.maybe_future(self.contents_manager.get(
126 path=path, type=type, format=format, content=content,
126 path=path, type=type, format=format, content=content,
127 ))
127 ))
128 if model['type'] == 'directory' and content:
128 if model['type'] == 'directory' and content:
129 # group listing by type, then by name (case-insensitive)
129 # group listing by type, then by name (case-insensitive)
130 # FIXME: sorting should be done in the frontends
130 # FIXME: sorting should be done in the frontends
131 model['content'].sort(key=sort_key)
131 model['content'].sort(key=sort_key)
132 validate_model(model, expect_content=content)
132 validate_model(model, expect_content=content)
133 self._finish_model(model, location=False)
133 self._finish_model(model, location=False)
134
134
135 @web.authenticated
135 @web.authenticated
136 @json_errors
136 @json_errors
137 @gen.coroutine
137 @gen.coroutine
138 def patch(self, path=''):
138 def patch(self, path=''):
139 """PATCH renames a file or directory without re-uploading content."""
139 """PATCH renames a file or directory without re-uploading content."""
140 cm = self.contents_manager
140 cm = self.contents_manager
141 model = self.get_json_body()
141 model = self.get_json_body()
142 if model is None:
142 if model is None:
143 raise web.HTTPError(400, u'JSON body missing')
143 raise web.HTTPError(400, u'JSON body missing')
144 model = yield gen.maybe_future(cm.update(model, path))
144 model = yield gen.maybe_future(cm.update(model, path))
145 validate_model(model, expect_content=False)
145 validate_model(model, expect_content=False)
146 self._finish_model(model)
146 self._finish_model(model)
147
147
148 @gen.coroutine
148 @gen.coroutine
149 def _copy(self, copy_from, copy_to=None):
149 def _copy(self, copy_from, copy_to=None):
150 """Copy a file, optionally specifying a target directory."""
150 """Copy a file, optionally specifying a target directory."""
151 self.log.info(u"Copying {copy_from} to {copy_to}".format(
151 self.log.info(u"Copying {copy_from} to {copy_to}".format(
152 copy_from=copy_from,
152 copy_from=copy_from,
153 copy_to=copy_to or '',
153 copy_to=copy_to or '',
154 ))
154 ))
155 model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
155 model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
156 self.set_status(201)
156 self.set_status(201)
157 validate_model(model, expect_content=False)
157 validate_model(model, expect_content=False)
158 self._finish_model(model)
158 self._finish_model(model)
159
159
160 @gen.coroutine
160 @gen.coroutine
161 def _upload(self, model, path):
161 def _upload(self, model, path):
162 """Handle upload of a new file to path"""
162 """Handle upload of a new file to path"""
163 self.log.info(u"Uploading file to %s", path)
163 self.log.info(u"Uploading file to %s", path)
164 model = yield gen.maybe_future(self.contents_manager.new(model, path))
164 model = yield gen.maybe_future(self.contents_manager.new(model, path))
165 self.set_status(201)
165 self.set_status(201)
166 validate_model(model, expect_content=False)
166 validate_model(model, expect_content=False)
167 self._finish_model(model)
167 self._finish_model(model)
168
168
169 @gen.coroutine
169 @gen.coroutine
170 def _new_untitled(self, path, type='', ext=''):
170 def _new_untitled(self, path, type='', ext=''):
171 """Create a new, empty untitled entity"""
171 """Create a new, empty untitled entity"""
172 self.log.info(u"Creating new %s in %s", type or 'file', path)
172 self.log.info(u"Creating new %s in %s", type or 'file', path)
173 model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
173 model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
174 self.set_status(201)
174 self.set_status(201)
175 validate_model(model, expect_content=False)
175 validate_model(model, expect_content=False)
176 self._finish_model(model)
176 self._finish_model(model)
177
177
178 @gen.coroutine
178 @gen.coroutine
179 def _save(self, model, path):
179 def _save(self, model, path):
180 """Save an existing file."""
180 """Save an existing file."""
181 self.log.info(u"Saving file at %s", path)
181 self.log.info(u"Saving file at %s", path)
182 model = yield gen.maybe_future(self.contents_manager.save(model, path))
182 model = yield gen.maybe_future(self.contents_manager.save(model, path))
183 validate_model(model, expect_content=False)
183 validate_model(model, expect_content=False)
184 self._finish_model(model)
184 self._finish_model(model)
185
185
186 @web.authenticated
186 @web.authenticated
187 @json_errors
187 @json_errors
188 @gen.coroutine
188 @gen.coroutine
189 def post(self, path=''):
189 def post(self, path=''):
190 """Create a new file in the specified path.
190 """Create a new file in the specified path.
191
191
192 POST creates new files. The server always decides on the name.
192 POST creates new files. The server always decides on the name.
193
193
194 POST /api/contents/path
194 POST /api/contents/path
195 New untitled, empty file or directory.
195 New untitled, empty file or directory.
196 POST /api/contents/path
196 POST /api/contents/path
197 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
197 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
198 New copy of OtherNotebook in path
198 New copy of OtherNotebook in path
199 """
199 """
200
200
201 cm = self.contents_manager
201 cm = self.contents_manager
202
202
203 if cm.file_exists(path):
203 if cm.file_exists(path):
204 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
204 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
205
205
206 if not cm.dir_exists(path):
206 if not cm.dir_exists(path):
207 raise web.HTTPError(404, "No such directory: %s" % path)
207 raise web.HTTPError(404, "No such directory: %s" % path)
208
208
209 model = self.get_json_body()
209 model = self.get_json_body()
210
210
211 if model is not None:
211 if model is not None:
212 copy_from = model.get('copy_from')
212 copy_from = model.get('copy_from')
213 ext = model.get('ext', '')
213 ext = model.get('ext', '')
214 type = model.get('type', '')
214 type = model.get('type', '')
215 if copy_from:
215 if copy_from:
216 yield self._copy(copy_from, path)
216 yield self._copy(copy_from, path)
217 else:
217 else:
218 yield self._new_untitled(path, type=type, ext=ext)
218 yield self._new_untitled(path, type=type, ext=ext)
219 else:
219 else:
220 yield self._new_untitled(path)
220 yield self._new_untitled(path)
221
221
222 @web.authenticated
222 @web.authenticated
223 @json_errors
223 @json_errors
224 @gen.coroutine
224 @gen.coroutine
225 def put(self, path=''):
225 def put(self, path=''):
226 """Saves the file in the location specified by name and path.
226 """Saves the file in the location specified by name and path.
227
227
228 PUT is very similar to POST, but the requester specifies the name,
228 PUT is very similar to POST, but the requester specifies the name,
229 whereas with POST, the server picks the name.
229 whereas with POST, the server picks the name.
230
230
231 PUT /api/contents/path/Name.ipynb
231 PUT /api/contents/path/Name.ipynb
232 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
232 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
233 in `content` key of JSON request body. If content is not specified,
233 in `content` key of JSON request body. If content is not specified,
234 create a new empty notebook.
234 create a new empty notebook.
235 """
235 """
236 model = self.get_json_body()
236 model = self.get_json_body()
237 if model:
237 if model:
238 if model.get('copy_from'):
238 if model.get('copy_from'):
239 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
239 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
240 exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
240 exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
241 if exists:
241 if exists:
242 yield gen.maybe_future(self._save(model, path))
242 yield gen.maybe_future(self._save(model, path))
243 else:
243 else:
244 yield gen.maybe_future(self._upload(model, path))
244 yield gen.maybe_future(self._upload(model, path))
245 else:
245 else:
246 yield gen.maybe_future(self._new_untitled(path))
246 yield gen.maybe_future(self._new_untitled(path))
247
247
248 @web.authenticated
248 @web.authenticated
249 @json_errors
249 @json_errors
250 @gen.coroutine
250 @gen.coroutine
251 def delete(self, path=''):
251 def delete(self, path=''):
252 """delete a file in the given path"""
252 """delete a file in the given path"""
253 cm = self.contents_manager
253 cm = self.contents_manager
254 self.log.warn('delete %s', path)
254 self.log.warn('delete %s', path)
255 yield gen.maybe_future(cm.delete(path))
255 yield gen.maybe_future(cm.delete(path))
256 self.set_status(204)
256 self.set_status(204)
257 self.finish()
257 self.finish()
258
258
259
259
260 class CheckpointsHandler(IPythonHandler):
260 class CheckpointsHandler(APIHandler):
261
261
262 SUPPORTED_METHODS = ('GET', 'POST')
262 SUPPORTED_METHODS = ('GET', 'POST')
263
263
264 @web.authenticated
264 @web.authenticated
265 @json_errors
265 @json_errors
266 @gen.coroutine
266 @gen.coroutine
267 def get(self, path=''):
267 def get(self, path=''):
268 """get lists checkpoints for a file"""
268 """get lists checkpoints for a file"""
269 cm = self.contents_manager
269 cm = self.contents_manager
270 checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
270 checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
271 data = json.dumps(checkpoints, default=date_default)
271 data = json.dumps(checkpoints, default=date_default)
272 self.finish(data)
272 self.finish(data)
273
273
274 @web.authenticated
274 @web.authenticated
275 @json_errors
275 @json_errors
276 @gen.coroutine
276 @gen.coroutine
277 def post(self, path=''):
277 def post(self, path=''):
278 """post creates a new checkpoint"""
278 """post creates a new checkpoint"""
279 cm = self.contents_manager
279 cm = self.contents_manager
280 checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
280 checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
281 data = json.dumps(checkpoint, default=date_default)
281 data = json.dumps(checkpoint, default=date_default)
282 location = url_path_join(self.base_url, 'api/contents',
282 location = url_path_join(self.base_url, 'api/contents',
283 path, 'checkpoints', checkpoint['id'])
283 path, 'checkpoints', checkpoint['id'])
284 self.set_header('Location', url_escape(location))
284 self.set_header('Location', url_escape(location))
285 self.set_status(201)
285 self.set_status(201)
286 self.finish(data)
286 self.finish(data)
287
287
288
288
289 class ModifyCheckpointsHandler(IPythonHandler):
289 class ModifyCheckpointsHandler(APIHandler):
290
290
291 SUPPORTED_METHODS = ('POST', 'DELETE')
291 SUPPORTED_METHODS = ('POST', 'DELETE')
292
292
293 @web.authenticated
293 @web.authenticated
294 @json_errors
294 @json_errors
295 @gen.coroutine
295 @gen.coroutine
296 def post(self, path, checkpoint_id):
296 def post(self, path, checkpoint_id):
297 """post restores a file from a checkpoint"""
297 """post restores a file from a checkpoint"""
298 cm = self.contents_manager
298 cm = self.contents_manager
299 yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
299 yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
300 self.set_status(204)
300 self.set_status(204)
301 self.finish()
301 self.finish()
302
302
303 @web.authenticated
303 @web.authenticated
304 @json_errors
304 @json_errors
305 @gen.coroutine
305 @gen.coroutine
306 def delete(self, path, checkpoint_id):
306 def delete(self, path, checkpoint_id):
307 """delete clears a checkpoint for a given file"""
307 """delete clears a checkpoint for a given file"""
308 cm = self.contents_manager
308 cm = self.contents_manager
309 yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
309 yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
310 self.set_status(204)
310 self.set_status(204)
311 self.finish()
311 self.finish()
312
312
313
313
314 class NotebooksRedirectHandler(IPythonHandler):
314 class NotebooksRedirectHandler(IPythonHandler):
315 """Redirect /api/notebooks to /api/contents"""
315 """Redirect /api/notebooks to /api/contents"""
316 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
316 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
317
317
318 def get(self, path):
318 def get(self, path):
319 self.log.warn("/api/notebooks is deprecated, use /api/contents")
319 self.log.warn("/api/notebooks is deprecated, use /api/contents")
320 self.redirect(url_path_join(
320 self.redirect(url_path_join(
321 self.base_url,
321 self.base_url,
322 'api/contents',
322 'api/contents',
323 path
323 path
324 ))
324 ))
325
325
326 put = patch = post = delete = get
326 put = patch = post = delete = get
327
327
328
328
329 #-----------------------------------------------------------------------------
329 #-----------------------------------------------------------------------------
330 # URL to handler mappings
330 # URL to handler mappings
331 #-----------------------------------------------------------------------------
331 #-----------------------------------------------------------------------------
332
332
333
333
334 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
334 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
335
335
336 default_handlers = [
336 default_handlers = [
337 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
337 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
338 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
338 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
339 ModifyCheckpointsHandler),
339 ModifyCheckpointsHandler),
340 (r"/api/contents%s" % path_regex, ContentsHandler),
340 (r"/api/contents%s" % path_regex, ContentsHandler),
341 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
341 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
342 ]
342 ]
@@ -1,288 +1,288 b''
1 """Tornado handlers for kernels."""
1 """Tornado handlers for kernels."""
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 json
6 import json
7 import logging
7 import logging
8 from tornado import gen, web
8 from tornado import gen, web
9 from tornado.concurrent import Future
9 from tornado.concurrent import Future
10 from tornado.ioloop import IOLoop
10 from tornado.ioloop import IOLoop
11
11
12 from IPython.utils.jsonutil import date_default
12 from IPython.utils.jsonutil import date_default
13 from IPython.utils.py3compat import cast_unicode
13 from IPython.utils.py3compat import cast_unicode
14 from IPython.html.utils import url_path_join, url_escape
14 from IPython.html.utils import url_path_join, url_escape
15
15
16 from ...base.handlers import IPythonHandler, json_errors
16 from ...base.handlers import IPythonHandler, APIHandler, json_errors
17 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
17 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
18
18
19 from IPython.core.release import kernel_protocol_version
19 from IPython.core.release import kernel_protocol_version
20
20
21 class MainKernelHandler(IPythonHandler):
21 class MainKernelHandler(APIHandler):
22
22
23 @web.authenticated
23 @web.authenticated
24 @json_errors
24 @json_errors
25 def get(self):
25 def get(self):
26 km = self.kernel_manager
26 km = self.kernel_manager
27 self.finish(json.dumps(km.list_kernels()))
27 self.finish(json.dumps(km.list_kernels()))
28
28
29 @web.authenticated
29 @web.authenticated
30 @json_errors
30 @json_errors
31 def post(self):
31 def post(self):
32 km = self.kernel_manager
32 km = self.kernel_manager
33 model = self.get_json_body()
33 model = self.get_json_body()
34 if model is None:
34 if model is None:
35 model = {
35 model = {
36 'name': km.default_kernel_name
36 'name': km.default_kernel_name
37 }
37 }
38 else:
38 else:
39 model.setdefault('name', km.default_kernel_name)
39 model.setdefault('name', km.default_kernel_name)
40
40
41 kernel_id = km.start_kernel(kernel_name=model['name'])
41 kernel_id = km.start_kernel(kernel_name=model['name'])
42 model = km.kernel_model(kernel_id)
42 model = km.kernel_model(kernel_id)
43 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
43 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
44 self.set_header('Location', url_escape(location))
44 self.set_header('Location', url_escape(location))
45 self.set_status(201)
45 self.set_status(201)
46 self.finish(json.dumps(model))
46 self.finish(json.dumps(model))
47
47
48
48
49 class KernelHandler(IPythonHandler):
49 class KernelHandler(APIHandler):
50
50
51 SUPPORTED_METHODS = ('DELETE', 'GET')
51 SUPPORTED_METHODS = ('DELETE', 'GET')
52
52
53 @web.authenticated
53 @web.authenticated
54 @json_errors
54 @json_errors
55 def get(self, kernel_id):
55 def get(self, kernel_id):
56 km = self.kernel_manager
56 km = self.kernel_manager
57 km._check_kernel_id(kernel_id)
57 km._check_kernel_id(kernel_id)
58 model = km.kernel_model(kernel_id)
58 model = km.kernel_model(kernel_id)
59 self.finish(json.dumps(model))
59 self.finish(json.dumps(model))
60
60
61 @web.authenticated
61 @web.authenticated
62 @json_errors
62 @json_errors
63 def delete(self, kernel_id):
63 def delete(self, kernel_id):
64 km = self.kernel_manager
64 km = self.kernel_manager
65 km.shutdown_kernel(kernel_id)
65 km.shutdown_kernel(kernel_id)
66 self.set_status(204)
66 self.set_status(204)
67 self.finish()
67 self.finish()
68
68
69
69
70 class KernelActionHandler(IPythonHandler):
70 class KernelActionHandler(APIHandler):
71
71
72 @web.authenticated
72 @web.authenticated
73 @json_errors
73 @json_errors
74 def post(self, kernel_id, action):
74 def post(self, kernel_id, action):
75 km = self.kernel_manager
75 km = self.kernel_manager
76 if action == 'interrupt':
76 if action == 'interrupt':
77 km.interrupt_kernel(kernel_id)
77 km.interrupt_kernel(kernel_id)
78 self.set_status(204)
78 self.set_status(204)
79 if action == 'restart':
79 if action == 'restart':
80 km.restart_kernel(kernel_id)
80 km.restart_kernel(kernel_id)
81 model = km.kernel_model(kernel_id)
81 model = km.kernel_model(kernel_id)
82 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
82 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
83 self.write(json.dumps(model))
83 self.write(json.dumps(model))
84 self.finish()
84 self.finish()
85
85
86
86
87 class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
87 class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
88
88
89 @property
89 @property
90 def kernel_info_timeout(self):
90 def kernel_info_timeout(self):
91 return self.settings.get('kernel_info_timeout', 10)
91 return self.settings.get('kernel_info_timeout', 10)
92
92
93 def __repr__(self):
93 def __repr__(self):
94 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
94 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
95
95
96 def create_stream(self):
96 def create_stream(self):
97 km = self.kernel_manager
97 km = self.kernel_manager
98 identity = self.session.bsession
98 identity = self.session.bsession
99 for channel in ('shell', 'iopub', 'stdin'):
99 for channel in ('shell', 'iopub', 'stdin'):
100 meth = getattr(km, 'connect_' + channel)
100 meth = getattr(km, 'connect_' + channel)
101 self.channels[channel] = stream = meth(self.kernel_id, identity=identity)
101 self.channels[channel] = stream = meth(self.kernel_id, identity=identity)
102 stream.channel = channel
102 stream.channel = channel
103 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
103 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
104 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
104 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
105
105
106 def request_kernel_info(self):
106 def request_kernel_info(self):
107 """send a request for kernel_info"""
107 """send a request for kernel_info"""
108 km = self.kernel_manager
108 km = self.kernel_manager
109 kernel = km.get_kernel(self.kernel_id)
109 kernel = km.get_kernel(self.kernel_id)
110 try:
110 try:
111 # check for previous request
111 # check for previous request
112 future = kernel._kernel_info_future
112 future = kernel._kernel_info_future
113 except AttributeError:
113 except AttributeError:
114 self.log.debug("Requesting kernel info from %s", self.kernel_id)
114 self.log.debug("Requesting kernel info from %s", self.kernel_id)
115 # Create a kernel_info channel to query the kernel protocol version.
115 # Create a kernel_info channel to query the kernel protocol version.
116 # This channel will be closed after the kernel_info reply is received.
116 # This channel will be closed after the kernel_info reply is received.
117 if self.kernel_info_channel is None:
117 if self.kernel_info_channel is None:
118 self.kernel_info_channel = km.connect_shell(self.kernel_id)
118 self.kernel_info_channel = km.connect_shell(self.kernel_id)
119 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
119 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
120 self.session.send(self.kernel_info_channel, "kernel_info_request")
120 self.session.send(self.kernel_info_channel, "kernel_info_request")
121 # store the future on the kernel, so only one request is sent
121 # store the future on the kernel, so only one request is sent
122 kernel._kernel_info_future = self._kernel_info_future
122 kernel._kernel_info_future = self._kernel_info_future
123 else:
123 else:
124 if not future.done():
124 if not future.done():
125 self.log.debug("Waiting for pending kernel_info request")
125 self.log.debug("Waiting for pending kernel_info request")
126 future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
126 future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
127 return self._kernel_info_future
127 return self._kernel_info_future
128
128
129 def _handle_kernel_info_reply(self, msg):
129 def _handle_kernel_info_reply(self, msg):
130 """process the kernel_info_reply
130 """process the kernel_info_reply
131
131
132 enabling msg spec adaptation, if necessary
132 enabling msg spec adaptation, if necessary
133 """
133 """
134 idents,msg = self.session.feed_identities(msg)
134 idents,msg = self.session.feed_identities(msg)
135 try:
135 try:
136 msg = self.session.deserialize(msg)
136 msg = self.session.deserialize(msg)
137 except:
137 except:
138 self.log.error("Bad kernel_info reply", exc_info=True)
138 self.log.error("Bad kernel_info reply", exc_info=True)
139 self._kernel_info_future.set_result({})
139 self._kernel_info_future.set_result({})
140 return
140 return
141 else:
141 else:
142 info = msg['content']
142 info = msg['content']
143 self.log.debug("Received kernel info: %s", info)
143 self.log.debug("Received kernel info: %s", info)
144 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
144 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
145 self.log.error("Kernel info request failed, assuming current %s", info)
145 self.log.error("Kernel info request failed, assuming current %s", info)
146 info = {}
146 info = {}
147 self._finish_kernel_info(info)
147 self._finish_kernel_info(info)
148
148
149 # close the kernel_info channel, we don't need it anymore
149 # close the kernel_info channel, we don't need it anymore
150 if self.kernel_info_channel:
150 if self.kernel_info_channel:
151 self.kernel_info_channel.close()
151 self.kernel_info_channel.close()
152 self.kernel_info_channel = None
152 self.kernel_info_channel = None
153
153
154 def _finish_kernel_info(self, info):
154 def _finish_kernel_info(self, info):
155 """Finish handling kernel_info reply
155 """Finish handling kernel_info reply
156
156
157 Set up protocol adaptation, if needed,
157 Set up protocol adaptation, if needed,
158 and signal that connection can continue.
158 and signal that connection can continue.
159 """
159 """
160 protocol_version = info.get('protocol_version', kernel_protocol_version)
160 protocol_version = info.get('protocol_version', kernel_protocol_version)
161 if protocol_version != kernel_protocol_version:
161 if protocol_version != kernel_protocol_version:
162 self.session.adapt_version = int(protocol_version.split('.')[0])
162 self.session.adapt_version = int(protocol_version.split('.')[0])
163 self.log.info("Adapting to protocol v%s for kernel %s", protocol_version, self.kernel_id)
163 self.log.info("Adapting to protocol v%s for kernel %s", protocol_version, self.kernel_id)
164 if not self._kernel_info_future.done():
164 if not self._kernel_info_future.done():
165 self._kernel_info_future.set_result(info)
165 self._kernel_info_future.set_result(info)
166
166
167 def initialize(self):
167 def initialize(self):
168 super(ZMQChannelsHandler, self).initialize()
168 super(ZMQChannelsHandler, self).initialize()
169 self.zmq_stream = None
169 self.zmq_stream = None
170 self.channels = {}
170 self.channels = {}
171 self.kernel_id = None
171 self.kernel_id = None
172 self.kernel_info_channel = None
172 self.kernel_info_channel = None
173 self._kernel_info_future = Future()
173 self._kernel_info_future = Future()
174
174
175 @gen.coroutine
175 @gen.coroutine
176 def pre_get(self):
176 def pre_get(self):
177 # authenticate first
177 # authenticate first
178 super(ZMQChannelsHandler, self).pre_get()
178 super(ZMQChannelsHandler, self).pre_get()
179 # then request kernel info, waiting up to a certain time before giving up.
179 # then request kernel info, waiting up to a certain time before giving up.
180 # We don't want to wait forever, because browsers don't take it well when
180 # We don't want to wait forever, because browsers don't take it well when
181 # servers never respond to websocket connection requests.
181 # servers never respond to websocket connection requests.
182 kernel = self.kernel_manager.get_kernel(self.kernel_id)
182 kernel = self.kernel_manager.get_kernel(self.kernel_id)
183 self.session.key = kernel.session.key
183 self.session.key = kernel.session.key
184 future = self.request_kernel_info()
184 future = self.request_kernel_info()
185
185
186 def give_up():
186 def give_up():
187 """Don't wait forever for the kernel to reply"""
187 """Don't wait forever for the kernel to reply"""
188 if future.done():
188 if future.done():
189 return
189 return
190 self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
190 self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
191 future.set_result({})
191 future.set_result({})
192 loop = IOLoop.current()
192 loop = IOLoop.current()
193 loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
193 loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
194 # actually wait for it
194 # actually wait for it
195 yield future
195 yield future
196
196
197 @gen.coroutine
197 @gen.coroutine
198 def get(self, kernel_id):
198 def get(self, kernel_id):
199 self.kernel_id = cast_unicode(kernel_id, 'ascii')
199 self.kernel_id = cast_unicode(kernel_id, 'ascii')
200 yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
200 yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
201
201
202 def open(self, kernel_id):
202 def open(self, kernel_id):
203 super(ZMQChannelsHandler, self).open()
203 super(ZMQChannelsHandler, self).open()
204 try:
204 try:
205 self.create_stream()
205 self.create_stream()
206 except web.HTTPError as e:
206 except web.HTTPError as e:
207 self.log.error("Error opening stream: %s", e)
207 self.log.error("Error opening stream: %s", e)
208 # WebSockets don't response to traditional error codes so we
208 # WebSockets don't response to traditional error codes so we
209 # close the connection.
209 # close the connection.
210 for channel, stream in self.channels.items():
210 for channel, stream in self.channels.items():
211 if not stream.closed():
211 if not stream.closed():
212 stream.close()
212 stream.close()
213 self.close()
213 self.close()
214 else:
214 else:
215 for channel, stream in self.channels.items():
215 for channel, stream in self.channels.items():
216 stream.on_recv_stream(self._on_zmq_reply)
216 stream.on_recv_stream(self._on_zmq_reply)
217
217
218 def on_message(self, msg):
218 def on_message(self, msg):
219 if not self.channels:
219 if not self.channels:
220 # already closed, ignore the message
220 # already closed, ignore the message
221 self.log.debug("Received message on closed websocket %r", msg)
221 self.log.debug("Received message on closed websocket %r", msg)
222 return
222 return
223 if isinstance(msg, bytes):
223 if isinstance(msg, bytes):
224 msg = deserialize_binary_message(msg)
224 msg = deserialize_binary_message(msg)
225 else:
225 else:
226 msg = json.loads(msg)
226 msg = json.loads(msg)
227 channel = msg.pop('channel', None)
227 channel = msg.pop('channel', None)
228 if channel is None:
228 if channel is None:
229 self.log.warn("No channel specified, assuming shell: %s", msg)
229 self.log.warn("No channel specified, assuming shell: %s", msg)
230 channel = 'shell'
230 channel = 'shell'
231 if channel not in self.channels:
231 if channel not in self.channels:
232 self.log.warn("No such channel: %r", channel)
232 self.log.warn("No such channel: %r", channel)
233 return
233 return
234 stream = self.channels[channel]
234 stream = self.channels[channel]
235 self.session.send(stream, msg)
235 self.session.send(stream, msg)
236
236
237 def on_close(self):
237 def on_close(self):
238 km = self.kernel_manager
238 km = self.kernel_manager
239 if self.kernel_id in km:
239 if self.kernel_id in km:
240 km.remove_restart_callback(
240 km.remove_restart_callback(
241 self.kernel_id, self.on_kernel_restarted,
241 self.kernel_id, self.on_kernel_restarted,
242 )
242 )
243 km.remove_restart_callback(
243 km.remove_restart_callback(
244 self.kernel_id, self.on_restart_failed, 'dead',
244 self.kernel_id, self.on_restart_failed, 'dead',
245 )
245 )
246 # This method can be called twice, once by self.kernel_died and once
246 # This method can be called twice, once by self.kernel_died and once
247 # from the WebSocket close event. If the WebSocket connection is
247 # from the WebSocket close event. If the WebSocket connection is
248 # closed before the ZMQ streams are setup, they could be None.
248 # closed before the ZMQ streams are setup, they could be None.
249 for channel, stream in self.channels.items():
249 for channel, stream in self.channels.items():
250 if stream is not None and not stream.closed():
250 if stream is not None and not stream.closed():
251 stream.on_recv(None)
251 stream.on_recv(None)
252 # close the socket directly, don't wait for the stream
252 # close the socket directly, don't wait for the stream
253 socket = stream.socket
253 socket = stream.socket
254 stream.close()
254 stream.close()
255 socket.close()
255 socket.close()
256
256
257 self.channels = {}
257 self.channels = {}
258
258
259 def _send_status_message(self, status):
259 def _send_status_message(self, status):
260 msg = self.session.msg("status",
260 msg = self.session.msg("status",
261 {'execution_state': status}
261 {'execution_state': status}
262 )
262 )
263 msg['channel'] = 'iopub'
263 msg['channel'] = 'iopub'
264 self.write_message(json.dumps(msg, default=date_default))
264 self.write_message(json.dumps(msg, default=date_default))
265
265
266 def on_kernel_restarted(self):
266 def on_kernel_restarted(self):
267 logging.warn("kernel %s restarted", self.kernel_id)
267 logging.warn("kernel %s restarted", self.kernel_id)
268 self._send_status_message('restarting')
268 self._send_status_message('restarting')
269
269
270 def on_restart_failed(self):
270 def on_restart_failed(self):
271 logging.error("kernel %s restarted failed!", self.kernel_id)
271 logging.error("kernel %s restarted failed!", self.kernel_id)
272 self._send_status_message('dead')
272 self._send_status_message('dead')
273
273
274
274
275 #-----------------------------------------------------------------------------
275 #-----------------------------------------------------------------------------
276 # URL to handler mappings
276 # URL to handler mappings
277 #-----------------------------------------------------------------------------
277 #-----------------------------------------------------------------------------
278
278
279
279
280 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
280 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
281 _kernel_action_regex = r"(?P<action>restart|interrupt)"
281 _kernel_action_regex = r"(?P<action>restart|interrupt)"
282
282
283 default_handlers = [
283 default_handlers = [
284 (r"/api/kernels", MainKernelHandler),
284 (r"/api/kernels", MainKernelHandler),
285 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
285 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
286 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
286 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
287 (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler),
287 (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler),
288 ]
288 ]
@@ -1,139 +1,141 b''
1 """Test the kernels service API."""
1 """Test the kernels service API."""
2
2
3 import json
3 import json
4 import requests
4 import requests
5
5
6 from IPython.html.utils import url_path_join
6 from IPython.html.utils import url_path_join
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
8
8
9 class KernelAPI(object):
9 class KernelAPI(object):
10 """Wrapper for kernel REST API requests"""
10 """Wrapper for kernel REST API requests"""
11 def __init__(self, base_url):
11 def __init__(self, base_url):
12 self.base_url = base_url
12 self.base_url = base_url
13
13
14 def _req(self, verb, path, body=None):
14 def _req(self, verb, path, body=None):
15 response = requests.request(verb,
15 response = requests.request(verb,
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
17
17
18 if 400 <= response.status_code < 600:
18 if 400 <= response.status_code < 600:
19 try:
19 try:
20 response.reason = response.json()['message']
20 response.reason = response.json()['message']
21 except:
21 except:
22 pass
22 pass
23 response.raise_for_status()
23 response.raise_for_status()
24
24
25 return response
25 return response
26
26
27 def list(self):
27 def list(self):
28 return self._req('GET', '')
28 return self._req('GET', '')
29
29
30 def get(self, id):
30 def get(self, id):
31 return self._req('GET', id)
31 return self._req('GET', id)
32
32
33 def start(self, name='python'):
33 def start(self, name='python'):
34 body = json.dumps({'name': name})
34 body = json.dumps({'name': name})
35 return self._req('POST', '', body)
35 return self._req('POST', '', body)
36
36
37 def shutdown(self, id):
37 def shutdown(self, id):
38 return self._req('DELETE', id)
38 return self._req('DELETE', id)
39
39
40 def interrupt(self, id):
40 def interrupt(self, id):
41 return self._req('POST', url_path_join(id, 'interrupt'))
41 return self._req('POST', url_path_join(id, 'interrupt'))
42
42
43 def restart(self, id):
43 def restart(self, id):
44 return self._req('POST', url_path_join(id, 'restart'))
44 return self._req('POST', url_path_join(id, 'restart'))
45
45
46 class KernelAPITest(NotebookTestBase):
46 class KernelAPITest(NotebookTestBase):
47 """Test the kernels web service API"""
47 """Test the kernels web service API"""
48 def setUp(self):
48 def setUp(self):
49 self.kern_api = KernelAPI(self.base_url())
49 self.kern_api = KernelAPI(self.base_url())
50
50
51 def tearDown(self):
51 def tearDown(self):
52 for k in self.kern_api.list().json():
52 for k in self.kern_api.list().json():
53 self.kern_api.shutdown(k['id'])
53 self.kern_api.shutdown(k['id'])
54
54
55 def test__no_kernels(self):
55 def test__no_kernels(self):
56 """Make sure there are no kernels running at the start"""
56 """Make sure there are no kernels running at the start"""
57 kernels = self.kern_api.list().json()
57 kernels = self.kern_api.list().json()
58 self.assertEqual(kernels, [])
58 self.assertEqual(kernels, [])
59
59
60 def test_default_kernel(self):
60 def test_default_kernel(self):
61 # POST request
61 # POST request
62 r = self.kern_api._req('POST', '')
62 r = self.kern_api._req('POST', '')
63 kern1 = r.json()
63 kern1 = r.json()
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 self.assertEqual(r.status_code, 201)
65 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
66 self.assertIsInstance(kern1, dict)
67
67
68 self.assertEqual(r.headers['Content-Security-Policy'], (
68 self.assertEqual(r.headers['Content-Security-Policy'], (
69 "frame-ancestors 'self'; "
69 "frame-ancestors 'self'; "
70 "report-uri /api/security/csp-report;"
70 "report-uri /api/security/csp-report; "
71 "default-src 'none'"
71 ))
72 ))
72
73
73 def test_main_kernel_handler(self):
74 def test_main_kernel_handler(self):
74 # POST request
75 # POST request
75 r = self.kern_api.start()
76 r = self.kern_api.start()
76 kern1 = r.json()
77 kern1 = r.json()
77 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
78 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
78 self.assertEqual(r.status_code, 201)
79 self.assertEqual(r.status_code, 201)
79 self.assertIsInstance(kern1, dict)
80 self.assertIsInstance(kern1, dict)
80
81
81 self.assertEqual(r.headers['Content-Security-Policy'], (
82 self.assertEqual(r.headers['Content-Security-Policy'], (
82 "frame-ancestors 'self'; "
83 "frame-ancestors 'self'; "
83 "report-uri /api/security/csp-report;"
84 "report-uri /api/security/csp-report; "
85 "default-src 'none'"
84 ))
86 ))
85
87
86 # GET request
88 # GET request
87 r = self.kern_api.list()
89 r = self.kern_api.list()
88 self.assertEqual(r.status_code, 200)
90 self.assertEqual(r.status_code, 200)
89 assert isinstance(r.json(), list)
91 assert isinstance(r.json(), list)
90 self.assertEqual(r.json()[0]['id'], kern1['id'])
92 self.assertEqual(r.json()[0]['id'], kern1['id'])
91 self.assertEqual(r.json()[0]['name'], kern1['name'])
93 self.assertEqual(r.json()[0]['name'], kern1['name'])
92
94
93 # create another kernel and check that they both are added to the
95 # create another kernel and check that they both are added to the
94 # list of kernels from a GET request
96 # list of kernels from a GET request
95 kern2 = self.kern_api.start().json()
97 kern2 = self.kern_api.start().json()
96 assert isinstance(kern2, dict)
98 assert isinstance(kern2, dict)
97 r = self.kern_api.list()
99 r = self.kern_api.list()
98 kernels = r.json()
100 kernels = r.json()
99 self.assertEqual(r.status_code, 200)
101 self.assertEqual(r.status_code, 200)
100 assert isinstance(kernels, list)
102 assert isinstance(kernels, list)
101 self.assertEqual(len(kernels), 2)
103 self.assertEqual(len(kernels), 2)
102
104
103 # Interrupt a kernel
105 # Interrupt a kernel
104 r = self.kern_api.interrupt(kern2['id'])
106 r = self.kern_api.interrupt(kern2['id'])
105 self.assertEqual(r.status_code, 204)
107 self.assertEqual(r.status_code, 204)
106
108
107 # Restart a kernel
109 # Restart a kernel
108 r = self.kern_api.restart(kern2['id'])
110 r = self.kern_api.restart(kern2['id'])
109 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
111 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
110 rekern = r.json()
112 rekern = r.json()
111 self.assertEqual(rekern['id'], kern2['id'])
113 self.assertEqual(rekern['id'], kern2['id'])
112 self.assertEqual(rekern['name'], kern2['name'])
114 self.assertEqual(rekern['name'], kern2['name'])
113
115
114 def test_kernel_handler(self):
116 def test_kernel_handler(self):
115 # GET kernel with given id
117 # GET kernel with given id
116 kid = self.kern_api.start().json()['id']
118 kid = self.kern_api.start().json()['id']
117 r = self.kern_api.get(kid)
119 r = self.kern_api.get(kid)
118 kern1 = r.json()
120 kern1 = r.json()
119 self.assertEqual(r.status_code, 200)
121 self.assertEqual(r.status_code, 200)
120 assert isinstance(kern1, dict)
122 assert isinstance(kern1, dict)
121 self.assertIn('id', kern1)
123 self.assertIn('id', kern1)
122 self.assertEqual(kern1['id'], kid)
124 self.assertEqual(kern1['id'], kid)
123
125
124 # Request a bad kernel id and check that a JSON
126 # Request a bad kernel id and check that a JSON
125 # message is returned!
127 # message is returned!
126 bad_id = '111-111-111-111-111'
128 bad_id = '111-111-111-111-111'
127 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
129 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
128 self.kern_api.get(bad_id)
130 self.kern_api.get(bad_id)
129
131
130 # DELETE kernel with id
132 # DELETE kernel with id
131 r = self.kern_api.shutdown(kid)
133 r = self.kern_api.shutdown(kid)
132 self.assertEqual(r.status_code, 204)
134 self.assertEqual(r.status_code, 204)
133 kernels = self.kern_api.list().json()
135 kernels = self.kern_api.list().json()
134 self.assertEqual(kernels, [])
136 self.assertEqual(kernels, [])
135
137
136 # Request to delete a non-existent kernel id
138 # Request to delete a non-existent kernel id
137 bad_id = '111-111-111-111-111'
139 bad_id = '111-111-111-111-111'
138 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
140 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
139 self.kern_api.shutdown(bad_id)
141 self.kern_api.shutdown(bad_id)
@@ -1,86 +1,86 b''
1 """Tornado handlers for kernel specifications."""
1 """Tornado handlers for kernel specifications."""
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 glob
6 import glob
7 import json
7 import json
8 import os
8 import os
9 pjoin = os.path.join
9 pjoin = os.path.join
10
10
11 from tornado import web
11 from tornado import web
12
12
13 from ...base.handlers import IPythonHandler, json_errors
13 from ...base.handlers import APIHandler, json_errors
14 from ...utils import url_path_join
14 from ...utils import url_path_join
15
15
16 def kernelspec_model(handler, name):
16 def kernelspec_model(handler, name):
17 """Load a KernelSpec by name and return the REST API model"""
17 """Load a KernelSpec by name and return the REST API model"""
18 ksm = handler.kernel_spec_manager
18 ksm = handler.kernel_spec_manager
19 spec = ksm.get_kernel_spec(name)
19 spec = ksm.get_kernel_spec(name)
20 d = {'name': name}
20 d = {'name': name}
21 d['spec'] = spec.to_dict()
21 d['spec'] = spec.to_dict()
22 d['resources'] = resources = {}
22 d['resources'] = resources = {}
23 resource_dir = spec.resource_dir
23 resource_dir = spec.resource_dir
24 for resource in ['kernel.js', 'kernel.css']:
24 for resource in ['kernel.js', 'kernel.css']:
25 if os.path.exists(pjoin(resource_dir, resource)):
25 if os.path.exists(pjoin(resource_dir, resource)):
26 resources[resource] = url_path_join(
26 resources[resource] = url_path_join(
27 handler.base_url,
27 handler.base_url,
28 'kernelspecs',
28 'kernelspecs',
29 name,
29 name,
30 resource
30 resource
31 )
31 )
32 for logo_file in glob.glob(pjoin(resource_dir, 'logo-*')):
32 for logo_file in glob.glob(pjoin(resource_dir, 'logo-*')):
33 fname = os.path.basename(logo_file)
33 fname = os.path.basename(logo_file)
34 no_ext, _ = os.path.splitext(fname)
34 no_ext, _ = os.path.splitext(fname)
35 resources[no_ext] = url_path_join(
35 resources[no_ext] = url_path_join(
36 handler.base_url,
36 handler.base_url,
37 'kernelspecs',
37 'kernelspecs',
38 name,
38 name,
39 fname
39 fname
40 )
40 )
41 return d
41 return d
42
42
43 class MainKernelSpecHandler(IPythonHandler):
43 class MainKernelSpecHandler(APIHandler):
44 SUPPORTED_METHODS = ('GET',)
44 SUPPORTED_METHODS = ('GET',)
45
45
46 @web.authenticated
46 @web.authenticated
47 @json_errors
47 @json_errors
48 def get(self):
48 def get(self):
49 ksm = self.kernel_spec_manager
49 ksm = self.kernel_spec_manager
50 km = self.kernel_manager
50 km = self.kernel_manager
51 model = {}
51 model = {}
52 model['default'] = km.default_kernel_name
52 model['default'] = km.default_kernel_name
53 model['kernelspecs'] = specs = {}
53 model['kernelspecs'] = specs = {}
54 for kernel_name in ksm.find_kernel_specs():
54 for kernel_name in ksm.find_kernel_specs():
55 try:
55 try:
56 d = kernelspec_model(self, kernel_name)
56 d = kernelspec_model(self, kernel_name)
57 except Exception:
57 except Exception:
58 self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True)
58 self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True)
59 continue
59 continue
60 specs[kernel_name] = d
60 specs[kernel_name] = d
61 self.set_header("Content-Type", 'application/json')
61 self.set_header("Content-Type", 'application/json')
62 self.finish(json.dumps(model))
62 self.finish(json.dumps(model))
63
63
64
64
65 class KernelSpecHandler(IPythonHandler):
65 class KernelSpecHandler(APIHandler):
66 SUPPORTED_METHODS = ('GET',)
66 SUPPORTED_METHODS = ('GET',)
67
67
68 @web.authenticated
68 @web.authenticated
69 @json_errors
69 @json_errors
70 def get(self, kernel_name):
70 def get(self, kernel_name):
71 try:
71 try:
72 model = kernelspec_model(self, kernel_name)
72 model = kernelspec_model(self, kernel_name)
73 except KeyError:
73 except KeyError:
74 raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
74 raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
75 self.set_header("Content-Type", 'application/json')
75 self.set_header("Content-Type", 'application/json')
76 self.finish(json.dumps(model))
76 self.finish(json.dumps(model))
77
77
78
78
79 # URL to handler mappings
79 # URL to handler mappings
80
80
81 kernel_name_regex = r"(?P<kernel_name>\w+)"
81 kernel_name_regex = r"(?P<kernel_name>\w+)"
82
82
83 default_handlers = [
83 default_handlers = [
84 (r"/api/kernelspecs", MainKernelSpecHandler),
84 (r"/api/kernelspecs", MainKernelSpecHandler),
85 (r"/api/kernelspecs/%s" % kernel_name_regex, KernelSpecHandler),
85 (r"/api/kernelspecs/%s" % kernel_name_regex, KernelSpecHandler),
86 ]
86 ]
@@ -1,26 +1,26 b''
1 import json
1 import json
2
2
3 from tornado import web
3 from tornado import web
4
4
5 from ...base.handlers import IPythonHandler, json_errors
5 from ...base.handlers import APIHandler, json_errors
6
6
7 class NbconvertRootHandler(IPythonHandler):
7 class NbconvertRootHandler(APIHandler):
8 SUPPORTED_METHODS = ('GET',)
8 SUPPORTED_METHODS = ('GET',)
9
9
10 @web.authenticated
10 @web.authenticated
11 @json_errors
11 @json_errors
12 def get(self):
12 def get(self):
13 try:
13 try:
14 from IPython.nbconvert.exporters.export import exporter_map
14 from IPython.nbconvert.exporters.export import exporter_map
15 except ImportError as e:
15 except ImportError as e:
16 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
16 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
17 res = {}
17 res = {}
18 for format, exporter in exporter_map.items():
18 for format, exporter in exporter_map.items():
19 res[format] = info = {}
19 res[format] = info = {}
20 info['output_mimetype'] = exporter.output_mimetype
20 info['output_mimetype'] = exporter.output_mimetype
21
21
22 self.finish(json.dumps(res))
22 self.finish(json.dumps(res))
23
23
24 default_handlers = [
24 default_handlers = [
25 (r"/api/nbconvert", NbconvertRootHandler),
25 (r"/api/nbconvert", NbconvertRootHandler),
26 ] No newline at end of file
26 ]
@@ -1,23 +1,23 b''
1 """Tornado handlers for security logging."""
1 """Tornado handlers for security logging."""
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 from tornado import gen, web
6 from tornado import gen, web
7
7
8 from ...base.handlers import IPythonHandler, json_errors
8 from ...base.handlers import APIHandler, json_errors
9 from . import csp_report_uri
9 from . import csp_report_uri
10
10
11 class CSPReportHandler(IPythonHandler):
11 class CSPReportHandler(APIHandler):
12 '''Accepts a content security policy violation report'''
12 '''Accepts a content security policy violation report'''
13 @web.authenticated
13 @web.authenticated
14 @json_errors
14 @json_errors
15 def post(self):
15 def post(self):
16 '''Log a content security policy violation report'''
16 '''Log a content security policy violation report'''
17 csp_report = self.get_json_body()
17 csp_report = self.get_json_body()
18 self.log.warn("Content security violation: %s",
18 self.log.warn("Content security violation: %s",
19 self.request.body.decode('utf8', 'replace'))
19 self.request.body.decode('utf8', 'replace'))
20
20
21 default_handlers = [
21 default_handlers = [
22 (csp_report_uri, CSPReportHandler)
22 (csp_report_uri, CSPReportHandler)
23 ]
23 ]
@@ -1,122 +1,122 b''
1 """Tornado handlers for the sessions web service."""
1 """Tornado handlers for the sessions web service."""
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 json
6 import json
7
7
8 from tornado import web
8 from tornado import web
9
9
10 from ...base.handlers import IPythonHandler, json_errors
10 from ...base.handlers import APIHandler, json_errors
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12 from IPython.html.utils import url_path_join, url_escape
12 from IPython.html.utils import url_path_join, url_escape
13 from IPython.kernel.kernelspec import NoSuchKernel
13 from IPython.kernel.kernelspec import NoSuchKernel
14
14
15
15
16 class SessionRootHandler(IPythonHandler):
16 class SessionRootHandler(APIHandler):
17
17
18 @web.authenticated
18 @web.authenticated
19 @json_errors
19 @json_errors
20 def get(self):
20 def get(self):
21 # Return a list of running sessions
21 # Return a list of running sessions
22 sm = self.session_manager
22 sm = self.session_manager
23 sessions = sm.list_sessions()
23 sessions = sm.list_sessions()
24 self.finish(json.dumps(sessions, default=date_default))
24 self.finish(json.dumps(sessions, default=date_default))
25
25
26 @web.authenticated
26 @web.authenticated
27 @json_errors
27 @json_errors
28 def post(self):
28 def post(self):
29 # Creates a new session
29 # Creates a new session
30 #(unless a session already exists for the named nb)
30 #(unless a session already exists for the named nb)
31 sm = self.session_manager
31 sm = self.session_manager
32 cm = self.contents_manager
32 cm = self.contents_manager
33 km = self.kernel_manager
33 km = self.kernel_manager
34
34
35 model = self.get_json_body()
35 model = self.get_json_body()
36 if model is None:
36 if model is None:
37 raise web.HTTPError(400, "No JSON data provided")
37 raise web.HTTPError(400, "No JSON data provided")
38 try:
38 try:
39 path = model['notebook']['path']
39 path = model['notebook']['path']
40 except KeyError:
40 except KeyError:
41 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
41 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
42 try:
42 try:
43 kernel_name = model['kernel']['name']
43 kernel_name = model['kernel']['name']
44 except KeyError:
44 except KeyError:
45 self.log.debug("No kernel name specified, using default kernel")
45 self.log.debug("No kernel name specified, using default kernel")
46 kernel_name = None
46 kernel_name = None
47
47
48 # Check to see if session exists
48 # Check to see if session exists
49 if sm.session_exists(path=path):
49 if sm.session_exists(path=path):
50 model = sm.get_session(path=path)
50 model = sm.get_session(path=path)
51 else:
51 else:
52 try:
52 try:
53 model = sm.create_session(path=path, kernel_name=kernel_name)
53 model = sm.create_session(path=path, kernel_name=kernel_name)
54 except NoSuchKernel:
54 except NoSuchKernel:
55 msg = ("The '%s' kernel is not available. Please pick another "
55 msg = ("The '%s' kernel is not available. Please pick another "
56 "suitable kernel instead, or install that kernel." % kernel_name)
56 "suitable kernel instead, or install that kernel." % kernel_name)
57 status_msg = '%s not found' % kernel_name
57 status_msg = '%s not found' % kernel_name
58 self.log.warn('Kernel not found: %s' % kernel_name)
58 self.log.warn('Kernel not found: %s' % kernel_name)
59 self.set_status(501)
59 self.set_status(501)
60 self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
60 self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
61 return
61 return
62
62
63 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
63 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
64 self.set_header('Location', url_escape(location))
64 self.set_header('Location', url_escape(location))
65 self.set_status(201)
65 self.set_status(201)
66 self.finish(json.dumps(model, default=date_default))
66 self.finish(json.dumps(model, default=date_default))
67
67
68 class SessionHandler(IPythonHandler):
68 class SessionHandler(APIHandler):
69
69
70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
71
71
72 @web.authenticated
72 @web.authenticated
73 @json_errors
73 @json_errors
74 def get(self, session_id):
74 def get(self, session_id):
75 # Returns the JSON model for a single session
75 # Returns the JSON model for a single session
76 sm = self.session_manager
76 sm = self.session_manager
77 model = sm.get_session(session_id=session_id)
77 model = sm.get_session(session_id=session_id)
78 self.finish(json.dumps(model, default=date_default))
78 self.finish(json.dumps(model, default=date_default))
79
79
80 @web.authenticated
80 @web.authenticated
81 @json_errors
81 @json_errors
82 def patch(self, session_id):
82 def patch(self, session_id):
83 # Currently, this handler is strictly for renaming notebooks
83 # Currently, this handler is strictly for renaming notebooks
84 sm = self.session_manager
84 sm = self.session_manager
85 model = self.get_json_body()
85 model = self.get_json_body()
86 if model is None:
86 if model is None:
87 raise web.HTTPError(400, "No JSON data provided")
87 raise web.HTTPError(400, "No JSON data provided")
88 changes = {}
88 changes = {}
89 if 'notebook' in model:
89 if 'notebook' in model:
90 notebook = model['notebook']
90 notebook = model['notebook']
91 if 'path' in notebook:
91 if 'path' in notebook:
92 changes['path'] = notebook['path']
92 changes['path'] = notebook['path']
93
93
94 sm.update_session(session_id, **changes)
94 sm.update_session(session_id, **changes)
95 model = sm.get_session(session_id=session_id)
95 model = sm.get_session(session_id=session_id)
96 self.finish(json.dumps(model, default=date_default))
96 self.finish(json.dumps(model, default=date_default))
97
97
98 @web.authenticated
98 @web.authenticated
99 @json_errors
99 @json_errors
100 def delete(self, session_id):
100 def delete(self, session_id):
101 # Deletes the session with given session_id
101 # Deletes the session with given session_id
102 sm = self.session_manager
102 sm = self.session_manager
103 try:
103 try:
104 sm.delete_session(session_id)
104 sm.delete_session(session_id)
105 except KeyError:
105 except KeyError:
106 # the kernel was deleted but the session wasn't!
106 # the kernel was deleted but the session wasn't!
107 raise web.HTTPError(410, "Kernel deleted before session")
107 raise web.HTTPError(410, "Kernel deleted before session")
108 self.set_status(204)
108 self.set_status(204)
109 self.finish()
109 self.finish()
110
110
111
111
112 #-----------------------------------------------------------------------------
112 #-----------------------------------------------------------------------------
113 # URL to handler mappings
113 # URL to handler mappings
114 #-----------------------------------------------------------------------------
114 #-----------------------------------------------------------------------------
115
115
116 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
116 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
117
117
118 default_handlers = [
118 default_handlers = [
119 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
119 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
120 (r"/api/sessions", SessionRootHandler)
120 (r"/api/sessions", SessionRootHandler)
121 ]
121 ]
122
122
@@ -1,44 +1,44 b''
1 import json
1 import json
2 from tornado import web, gen
2 from tornado import web, gen
3 from ..base.handlers import IPythonHandler, json_errors
3 from ..base.handlers import APIHandler, json_errors
4 from ..utils import url_path_join
4 from ..utils import url_path_join
5
5
6 class TerminalRootHandler(IPythonHandler):
6 class TerminalRootHandler(APIHandler):
7 @web.authenticated
7 @web.authenticated
8 @json_errors
8 @json_errors
9 def get(self):
9 def get(self):
10 tm = self.terminal_manager
10 tm = self.terminal_manager
11 terms = [{'name': name} for name in tm.terminals]
11 terms = [{'name': name} for name in tm.terminals]
12 self.finish(json.dumps(terms))
12 self.finish(json.dumps(terms))
13
13
14 @web.authenticated
14 @web.authenticated
15 @json_errors
15 @json_errors
16 def post(self):
16 def post(self):
17 """POST /terminals creates a new terminal and redirects to it"""
17 """POST /terminals creates a new terminal and redirects to it"""
18 name, _ = self.terminal_manager.new_named_terminal()
18 name, _ = self.terminal_manager.new_named_terminal()
19 self.finish(json.dumps({'name': name}))
19 self.finish(json.dumps({'name': name}))
20
20
21
21
22 class TerminalHandler(IPythonHandler):
22 class TerminalHandler(APIHandler):
23 SUPPORTED_METHODS = ('GET', 'DELETE')
23 SUPPORTED_METHODS = ('GET', 'DELETE')
24
24
25 @web.authenticated
25 @web.authenticated
26 @json_errors
26 @json_errors
27 def get(self, name):
27 def get(self, name):
28 tm = self.terminal_manager
28 tm = self.terminal_manager
29 if name in tm.terminals:
29 if name in tm.terminals:
30 self.finish(json.dumps({'name': name}))
30 self.finish(json.dumps({'name': name}))
31 else:
31 else:
32 raise web.HTTPError(404, "Terminal not found: %r" % name)
32 raise web.HTTPError(404, "Terminal not found: %r" % name)
33
33
34 @web.authenticated
34 @web.authenticated
35 @json_errors
35 @json_errors
36 @gen.coroutine
36 @gen.coroutine
37 def delete(self, name):
37 def delete(self, name):
38 tm = self.terminal_manager
38 tm = self.terminal_manager
39 if name in tm.terminals:
39 if name in tm.terminals:
40 yield tm.terminate(name, force=True)
40 yield tm.terminate(name, force=True)
41 self.set_status(204)
41 self.set_status(204)
42 self.finish()
42 self.finish()
43 else:
43 else:
44 raise web.HTTPError(404, "Terminal not found: %r" % name)
44 raise web.HTTPError(404, "Terminal not found: %r" % name)
General Comments 0
You need to be logged in to leave comments. Login now