##// END OF EJS Templates
s/cors_/allow_/...
MinRK -
Show More
@@ -1,418 +1,418 b''
1 """Base Tornado handlers for the notebook."""
1 """Base Tornado handlers for the notebook."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 from IPython.config import Application
27 from IPython.config import Application
28 from IPython.utils.path import filefind
28 from IPython.utils.path import filefind
29 from IPython.utils.py3compat import string_types
29 from IPython.utils.py3compat import string_types
30 from IPython.html.utils import is_hidden
30 from IPython.html.utils import is_hidden
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Top-level handlers
33 # Top-level handlers
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36
36
37 class AuthenticatedHandler(web.RequestHandler):
37 class AuthenticatedHandler(web.RequestHandler):
38 """A RequestHandler with an authenticated user."""
38 """A RequestHandler with an authenticated user."""
39
39
40 def set_default_headers(self):
40 def set_default_headers(self):
41 headers = self.settings.get('headers', {})
41 headers = self.settings.get('headers', {})
42 for header_name,value in headers.items() :
42 for header_name,value in headers.items() :
43 try:
43 try:
44 self.set_header(header_name, value)
44 self.set_header(header_name, value)
45 except Exception:
45 except Exception:
46 # tornado raise Exception (not a subclass)
46 # tornado raise Exception (not a subclass)
47 # if method is unsupported (websocket and Access-Control-Allow-Origin
47 # if method is unsupported (websocket and Access-Control-Allow-Origin
48 # for example, so just ignore)
48 # for example, so just ignore)
49 pass
49 pass
50
50
51 def clear_login_cookie(self):
51 def clear_login_cookie(self):
52 self.clear_cookie(self.cookie_name)
52 self.clear_cookie(self.cookie_name)
53
53
54 def get_current_user(self):
54 def get_current_user(self):
55 user_id = self.get_secure_cookie(self.cookie_name)
55 user_id = self.get_secure_cookie(self.cookie_name)
56 # For now the user_id should not return empty, but it could eventually
56 # For now the user_id should not return empty, but it could eventually
57 if user_id == '':
57 if user_id == '':
58 user_id = 'anonymous'
58 user_id = 'anonymous'
59 if user_id is None:
59 if user_id is None:
60 # prevent extra Invalid cookie sig warnings:
60 # prevent extra Invalid cookie sig warnings:
61 self.clear_login_cookie()
61 self.clear_login_cookie()
62 if not self.login_available:
62 if not self.login_available:
63 user_id = 'anonymous'
63 user_id = 'anonymous'
64 return user_id
64 return user_id
65
65
66 @property
66 @property
67 def cookie_name(self):
67 def cookie_name(self):
68 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
68 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
69 self.request.host
69 self.request.host
70 ))
70 ))
71 return self.settings.get('cookie_name', default_cookie_name)
71 return self.settings.get('cookie_name', default_cookie_name)
72
72
73 @property
73 @property
74 def password(self):
74 def password(self):
75 """our password"""
75 """our password"""
76 return self.settings.get('password', '')
76 return self.settings.get('password', '')
77
77
78 @property
78 @property
79 def logged_in(self):
79 def logged_in(self):
80 """Is a user currently logged in?
80 """Is a user currently logged in?
81
81
82 """
82 """
83 user = self.get_current_user()
83 user = self.get_current_user()
84 return (user and not user == 'anonymous')
84 return (user and not user == 'anonymous')
85
85
86 @property
86 @property
87 def login_available(self):
87 def login_available(self):
88 """May a user proceed to log in?
88 """May a user proceed to log in?
89
89
90 This returns True if login capability is available, irrespective of
90 This returns True if login capability is available, irrespective of
91 whether the user is already logged in or not.
91 whether the user is already logged in or not.
92
92
93 """
93 """
94 return bool(self.settings.get('password', ''))
94 return bool(self.settings.get('password', ''))
95
95
96
96
97 class IPythonHandler(AuthenticatedHandler):
97 class IPythonHandler(AuthenticatedHandler):
98 """IPython-specific extensions to authenticated handling
98 """IPython-specific extensions to authenticated handling
99
99
100 Mostly property shortcuts to IPython-specific settings.
100 Mostly property shortcuts to IPython-specific settings.
101 """
101 """
102
102
103 @property
103 @property
104 def config(self):
104 def config(self):
105 return self.settings.get('config', None)
105 return self.settings.get('config', None)
106
106
107 @property
107 @property
108 def log(self):
108 def log(self):
109 """use the IPython log by default, falling back on tornado's logger"""
109 """use the IPython log by default, falling back on tornado's logger"""
110 if Application.initialized():
110 if Application.initialized():
111 return Application.instance().log
111 return Application.instance().log
112 else:
112 else:
113 return app_log
113 return app_log
114
114
115 #---------------------------------------------------------------
115 #---------------------------------------------------------------
116 # URLs
116 # URLs
117 #---------------------------------------------------------------
117 #---------------------------------------------------------------
118
118
119 @property
119 @property
120 def mathjax_url(self):
120 def mathjax_url(self):
121 return self.settings.get('mathjax_url', '')
121 return self.settings.get('mathjax_url', '')
122
122
123 @property
123 @property
124 def base_url(self):
124 def base_url(self):
125 return self.settings.get('base_url', '/')
125 return self.settings.get('base_url', '/')
126
126
127 #---------------------------------------------------------------
127 #---------------------------------------------------------------
128 # Manager objects
128 # Manager objects
129 #---------------------------------------------------------------
129 #---------------------------------------------------------------
130
130
131 @property
131 @property
132 def kernel_manager(self):
132 def kernel_manager(self):
133 return self.settings['kernel_manager']
133 return self.settings['kernel_manager']
134
134
135 @property
135 @property
136 def notebook_manager(self):
136 def notebook_manager(self):
137 return self.settings['notebook_manager']
137 return self.settings['notebook_manager']
138
138
139 @property
139 @property
140 def cluster_manager(self):
140 def cluster_manager(self):
141 return self.settings['cluster_manager']
141 return self.settings['cluster_manager']
142
142
143 @property
143 @property
144 def session_manager(self):
144 def session_manager(self):
145 return self.settings['session_manager']
145 return self.settings['session_manager']
146
146
147 @property
147 @property
148 def kernel_spec_manager(self):
148 def kernel_spec_manager(self):
149 return self.settings['kernel_spec_manager']
149 return self.settings['kernel_spec_manager']
150
150
151 @property
151 @property
152 def project_dir(self):
152 def project_dir(self):
153 return self.notebook_manager.notebook_dir
153 return self.notebook_manager.notebook_dir
154
154
155 #---------------------------------------------------------------
155 #---------------------------------------------------------------
156 # CORS
156 # CORS
157 #---------------------------------------------------------------
157 #---------------------------------------------------------------
158
158
159 @property
159 @property
160 def cors_origin(self):
160 def allow_origin(self):
161 """Normal Access-Control-Allow-Origin"""
161 """Normal Access-Control-Allow-Origin"""
162 return self.settings.get('cors_origin', '')
162 return self.settings.get('allow_origin', '')
163
163
164 @property
164 @property
165 def cors_origin_pat(self):
165 def allow_origin_pat(self):
166 """Regular expression version of cors_origin"""
166 """Regular expression version of allow_origin"""
167 return self.settings.get('cors_origin_pat', None)
167 return self.settings.get('allow_origin_pat', None)
168
168
169 @property
169 @property
170 def cors_credentials(self):
170 def allow_credentials(self):
171 """Whether to set Access-Control-Allow-Credentials"""
171 """Whether to set Access-Control-Allow-Credentials"""
172 return self.settings.get('cors_credentials', False)
172 return self.settings.get('allow_credentials', False)
173
173
174 def set_default_headers(self):
174 def set_default_headers(self):
175 """Add CORS headers, if defined"""
175 """Add CORS headers, if defined"""
176 super(IPythonHandler, self).set_default_headers()
176 super(IPythonHandler, self).set_default_headers()
177 if self.cors_origin:
177 if self.allow_origin:
178 self.set_header("Access-Control-Allow-Origin", self.cors_origin)
178 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
179 elif self.cors_origin_pat:
179 elif self.allow_origin_pat:
180 origin = self.get_origin()
180 origin = self.get_origin()
181 if origin and self.cors_origin_pat.match(origin):
181 if origin and self.allow_origin_pat.match(origin):
182 self.set_header("Access-Control-Allow-Origin", origin)
182 self.set_header("Access-Control-Allow-Origin", origin)
183 if self.cors_credentials:
183 if self.allow_credentials:
184 self.set_header("Access-Control-Allow-Credentials", 'true')
184 self.set_header("Access-Control-Allow-Credentials", 'true')
185
185
186 def get_origin(self):
186 def get_origin(self):
187 # Handle WebSocket Origin naming convention differences
187 # Handle WebSocket Origin naming convention differences
188 # The difference between version 8 and 13 is that in 8 the
188 # The difference between version 8 and 13 is that in 8 the
189 # client sends a "Sec-Websocket-Origin" header and in 13 it's
189 # client sends a "Sec-Websocket-Origin" header and in 13 it's
190 # simply "Origin".
190 # simply "Origin".
191 if "Origin" in self.request.headers:
191 if "Origin" in self.request.headers:
192 origin = self.request.headers.get("Origin")
192 origin = self.request.headers.get("Origin")
193 else:
193 else:
194 origin = self.request.headers.get("Sec-Websocket-Origin", None)
194 origin = self.request.headers.get("Sec-Websocket-Origin", None)
195 return origin
195 return origin
196
196
197 #---------------------------------------------------------------
197 #---------------------------------------------------------------
198 # template rendering
198 # template rendering
199 #---------------------------------------------------------------
199 #---------------------------------------------------------------
200
200
201 def get_template(self, name):
201 def get_template(self, name):
202 """Return the jinja template object for a given name"""
202 """Return the jinja template object for a given name"""
203 return self.settings['jinja2_env'].get_template(name)
203 return self.settings['jinja2_env'].get_template(name)
204
204
205 def render_template(self, name, **ns):
205 def render_template(self, name, **ns):
206 ns.update(self.template_namespace)
206 ns.update(self.template_namespace)
207 template = self.get_template(name)
207 template = self.get_template(name)
208 return template.render(**ns)
208 return template.render(**ns)
209
209
210 @property
210 @property
211 def template_namespace(self):
211 def template_namespace(self):
212 return dict(
212 return dict(
213 base_url=self.base_url,
213 base_url=self.base_url,
214 logged_in=self.logged_in,
214 logged_in=self.logged_in,
215 login_available=self.login_available,
215 login_available=self.login_available,
216 static_url=self.static_url,
216 static_url=self.static_url,
217 )
217 )
218
218
219 def get_json_body(self):
219 def get_json_body(self):
220 """Return the body of the request as JSON data."""
220 """Return the body of the request as JSON data."""
221 if not self.request.body:
221 if not self.request.body:
222 return None
222 return None
223 # Do we need to call body.decode('utf-8') here?
223 # Do we need to call body.decode('utf-8') here?
224 body = self.request.body.strip().decode(u'utf-8')
224 body = self.request.body.strip().decode(u'utf-8')
225 try:
225 try:
226 model = json.loads(body)
226 model = json.loads(body)
227 except Exception:
227 except Exception:
228 self.log.debug("Bad JSON: %r", body)
228 self.log.debug("Bad JSON: %r", body)
229 self.log.error("Couldn't parse JSON", exc_info=True)
229 self.log.error("Couldn't parse JSON", exc_info=True)
230 raise web.HTTPError(400, u'Invalid JSON in body of request')
230 raise web.HTTPError(400, u'Invalid JSON in body of request')
231 return model
231 return model
232
232
233 def get_error_html(self, status_code, **kwargs):
233 def get_error_html(self, status_code, **kwargs):
234 """render custom error pages"""
234 """render custom error pages"""
235 exception = kwargs.get('exception')
235 exception = kwargs.get('exception')
236 message = ''
236 message = ''
237 status_message = responses.get(status_code, 'Unknown HTTP Error')
237 status_message = responses.get(status_code, 'Unknown HTTP Error')
238 if exception:
238 if exception:
239 # get the custom message, if defined
239 # get the custom message, if defined
240 try:
240 try:
241 message = exception.log_message % exception.args
241 message = exception.log_message % exception.args
242 except Exception:
242 except Exception:
243 pass
243 pass
244
244
245 # construct the custom reason, if defined
245 # construct the custom reason, if defined
246 reason = getattr(exception, 'reason', '')
246 reason = getattr(exception, 'reason', '')
247 if reason:
247 if reason:
248 status_message = reason
248 status_message = reason
249
249
250 # build template namespace
250 # build template namespace
251 ns = dict(
251 ns = dict(
252 status_code=status_code,
252 status_code=status_code,
253 status_message=status_message,
253 status_message=status_message,
254 message=message,
254 message=message,
255 exception=exception,
255 exception=exception,
256 )
256 )
257
257
258 # render the template
258 # render the template
259 try:
259 try:
260 html = self.render_template('%s.html' % status_code, **ns)
260 html = self.render_template('%s.html' % status_code, **ns)
261 except TemplateNotFound:
261 except TemplateNotFound:
262 self.log.debug("No template for %d", status_code)
262 self.log.debug("No template for %d", status_code)
263 html = self.render_template('error.html', **ns)
263 html = self.render_template('error.html', **ns)
264 return html
264 return html
265
265
266
266
267 class Template404(IPythonHandler):
267 class Template404(IPythonHandler):
268 """Render our 404 template"""
268 """Render our 404 template"""
269 def prepare(self):
269 def prepare(self):
270 raise web.HTTPError(404)
270 raise web.HTTPError(404)
271
271
272
272
273 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
273 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
274 """static files should only be accessible when logged in"""
274 """static files should only be accessible when logged in"""
275
275
276 @web.authenticated
276 @web.authenticated
277 def get(self, path):
277 def get(self, path):
278 if os.path.splitext(path)[1] == '.ipynb':
278 if os.path.splitext(path)[1] == '.ipynb':
279 name = os.path.basename(path)
279 name = os.path.basename(path)
280 self.set_header('Content-Type', 'application/json')
280 self.set_header('Content-Type', 'application/json')
281 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
281 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
282
282
283 return web.StaticFileHandler.get(self, path)
283 return web.StaticFileHandler.get(self, path)
284
284
285 def compute_etag(self):
285 def compute_etag(self):
286 return None
286 return None
287
287
288 def validate_absolute_path(self, root, absolute_path):
288 def validate_absolute_path(self, root, absolute_path):
289 """Validate and return the absolute path.
289 """Validate and return the absolute path.
290
290
291 Requires tornado 3.1
291 Requires tornado 3.1
292
292
293 Adding to tornado's own handling, forbids the serving of hidden files.
293 Adding to tornado's own handling, forbids the serving of hidden files.
294 """
294 """
295 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
295 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
296 abs_root = os.path.abspath(root)
296 abs_root = os.path.abspath(root)
297 if is_hidden(abs_path, abs_root):
297 if is_hidden(abs_path, abs_root):
298 self.log.info("Refusing to serve hidden file, via 404 Error")
298 self.log.info("Refusing to serve hidden file, via 404 Error")
299 raise web.HTTPError(404)
299 raise web.HTTPError(404)
300 return abs_path
300 return abs_path
301
301
302
302
303 def json_errors(method):
303 def json_errors(method):
304 """Decorate methods with this to return GitHub style JSON errors.
304 """Decorate methods with this to return GitHub style JSON errors.
305
305
306 This should be used on any JSON API on any handler method that can raise HTTPErrors.
306 This should be used on any JSON API on any handler method that can raise HTTPErrors.
307
307
308 This will grab the latest HTTPError exception using sys.exc_info
308 This will grab the latest HTTPError exception using sys.exc_info
309 and then:
309 and then:
310
310
311 1. Set the HTTP status code based on the HTTPError
311 1. Set the HTTP status code based on the HTTPError
312 2. Create and return a JSON body with a message field describing
312 2. Create and return a JSON body with a message field describing
313 the error in a human readable form.
313 the error in a human readable form.
314 """
314 """
315 @functools.wraps(method)
315 @functools.wraps(method)
316 def wrapper(self, *args, **kwargs):
316 def wrapper(self, *args, **kwargs):
317 try:
317 try:
318 result = method(self, *args, **kwargs)
318 result = method(self, *args, **kwargs)
319 except web.HTTPError as e:
319 except web.HTTPError as e:
320 status = e.status_code
320 status = e.status_code
321 message = e.log_message
321 message = e.log_message
322 self.log.warn(message)
322 self.log.warn(message)
323 self.set_status(e.status_code)
323 self.set_status(e.status_code)
324 self.finish(json.dumps(dict(message=message)))
324 self.finish(json.dumps(dict(message=message)))
325 except Exception:
325 except Exception:
326 self.log.error("Unhandled error in API request", exc_info=True)
326 self.log.error("Unhandled error in API request", exc_info=True)
327 status = 500
327 status = 500
328 message = "Unknown server error"
328 message = "Unknown server error"
329 t, value, tb = sys.exc_info()
329 t, value, tb = sys.exc_info()
330 self.set_status(status)
330 self.set_status(status)
331 tb_text = ''.join(traceback.format_exception(t, value, tb))
331 tb_text = ''.join(traceback.format_exception(t, value, tb))
332 reply = dict(message=message, traceback=tb_text)
332 reply = dict(message=message, traceback=tb_text)
333 self.finish(json.dumps(reply))
333 self.finish(json.dumps(reply))
334 else:
334 else:
335 return result
335 return result
336 return wrapper
336 return wrapper
337
337
338
338
339
339
340 #-----------------------------------------------------------------------------
340 #-----------------------------------------------------------------------------
341 # File handler
341 # File handler
342 #-----------------------------------------------------------------------------
342 #-----------------------------------------------------------------------------
343
343
344 # to minimize subclass changes:
344 # to minimize subclass changes:
345 HTTPError = web.HTTPError
345 HTTPError = web.HTTPError
346
346
347 class FileFindHandler(web.StaticFileHandler):
347 class FileFindHandler(web.StaticFileHandler):
348 """subclass of StaticFileHandler for serving files from a search path"""
348 """subclass of StaticFileHandler for serving files from a search path"""
349
349
350 # cache search results, don't search for files more than once
350 # cache search results, don't search for files more than once
351 _static_paths = {}
351 _static_paths = {}
352
352
353 def initialize(self, path, default_filename=None):
353 def initialize(self, path, default_filename=None):
354 if isinstance(path, string_types):
354 if isinstance(path, string_types):
355 path = [path]
355 path = [path]
356
356
357 self.root = tuple(
357 self.root = tuple(
358 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
358 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
359 )
359 )
360 self.default_filename = default_filename
360 self.default_filename = default_filename
361
361
362 def compute_etag(self):
362 def compute_etag(self):
363 return None
363 return None
364
364
365 @classmethod
365 @classmethod
366 def get_absolute_path(cls, roots, path):
366 def get_absolute_path(cls, roots, path):
367 """locate a file to serve on our static file search path"""
367 """locate a file to serve on our static file search path"""
368 with cls._lock:
368 with cls._lock:
369 if path in cls._static_paths:
369 if path in cls._static_paths:
370 return cls._static_paths[path]
370 return cls._static_paths[path]
371 try:
371 try:
372 abspath = os.path.abspath(filefind(path, roots))
372 abspath = os.path.abspath(filefind(path, roots))
373 except IOError:
373 except IOError:
374 # IOError means not found
374 # IOError means not found
375 return ''
375 return ''
376
376
377 cls._static_paths[path] = abspath
377 cls._static_paths[path] = abspath
378 return abspath
378 return abspath
379
379
380 def validate_absolute_path(self, root, absolute_path):
380 def validate_absolute_path(self, root, absolute_path):
381 """check if the file should be served (raises 404, 403, etc.)"""
381 """check if the file should be served (raises 404, 403, etc.)"""
382 if absolute_path == '':
382 if absolute_path == '':
383 raise web.HTTPError(404)
383 raise web.HTTPError(404)
384
384
385 for root in self.root:
385 for root in self.root:
386 if (absolute_path + os.sep).startswith(root):
386 if (absolute_path + os.sep).startswith(root):
387 break
387 break
388
388
389 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
389 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
390
390
391
391
392 class TrailingSlashHandler(web.RequestHandler):
392 class TrailingSlashHandler(web.RequestHandler):
393 """Simple redirect handler that strips trailing slashes
393 """Simple redirect handler that strips trailing slashes
394
394
395 This should be the first, highest priority handler.
395 This should be the first, highest priority handler.
396 """
396 """
397
397
398 SUPPORTED_METHODS = ['GET']
398 SUPPORTED_METHODS = ['GET']
399
399
400 def get(self):
400 def get(self):
401 self.redirect(self.request.uri.rstrip('/'))
401 self.redirect(self.request.uri.rstrip('/'))
402
402
403 #-----------------------------------------------------------------------------
403 #-----------------------------------------------------------------------------
404 # URL pattern fragments for re-use
404 # URL pattern fragments for re-use
405 #-----------------------------------------------------------------------------
405 #-----------------------------------------------------------------------------
406
406
407 path_regex = r"(?P<path>(?:/.*)*)"
407 path_regex = r"(?P<path>(?:/.*)*)"
408 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
408 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
409 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
409 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
410
410
411 #-----------------------------------------------------------------------------
411 #-----------------------------------------------------------------------------
412 # URL to handler mappings
412 # URL to handler mappings
413 #-----------------------------------------------------------------------------
413 #-----------------------------------------------------------------------------
414
414
415
415
416 default_handlers = [
416 default_handlers = [
417 (r".*/", TrailingSlashHandler)
417 (r".*/", TrailingSlashHandler)
418 ]
418 ]
@@ -1,150 +1,151 b''
1 """Tornado handlers for WebSocket <-> ZMQ sockets."""
1 """Tornado handlers for WebSocket <-> ZMQ sockets."""
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 try:
8 try:
9 from urllib.parse import urlparse # Py 3
9 from urllib.parse import urlparse # Py 3
10 except ImportError:
10 except ImportError:
11 from urlparse import urlparse # Py 2
11 from urlparse import urlparse # Py 2
12
12
13 try:
13 try:
14 from http.cookies import SimpleCookie # Py 3
14 from http.cookies import SimpleCookie # Py 3
15 except ImportError:
15 except ImportError:
16 from Cookie import SimpleCookie # Py 2
16 from Cookie import SimpleCookie # Py 2
17 import logging
17 import logging
18
18
19 import tornado
19 import tornado
20 from tornado import web
20 from tornado import web
21 from tornado import websocket
21 from tornado import websocket
22
22
23 from IPython.kernel.zmq.session import Session
23 from IPython.kernel.zmq.session import Session
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.utils.py3compat import PY3, cast_unicode
25 from IPython.utils.py3compat import PY3, cast_unicode
26
26
27 from .handlers import IPythonHandler
27 from .handlers import IPythonHandler
28
28
29
29
30 class ZMQStreamHandler(websocket.WebSocketHandler):
30 class ZMQStreamHandler(websocket.WebSocketHandler):
31
31
32 def check_origin(self, origin):
32 def check_origin(self, origin):
33 """Check Origin == Host or CORS origins."""
33 """Check Origin == Host or Access-Control-Allow-Origin.
34 if self.cors_origin == '*':
34
35 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
36 We call it explicitly in `open` on Tornado < 4.
37 """
38 if self.allow_origin == '*':
35 return True
39 return True
36
40
37 host = self.request.headers.get("Host")
41 host = self.request.headers.get("Host")
38
42
39 # If no header is provided, assume we can't verify origin
43 # If no header is provided, assume we can't verify origin
40 if(origin is None or host is None):
44 if(origin is None or host is None):
41 return False
45 return False
42
46
43 host_origin = "{0}://{1}".format(self.request.protocol, host)
47 host_origin = "{0}://{1}".format(self.request.protocol, host)
44
48
45 # OK if origin matches host
49 # OK if origin matches host
46 if origin == host_origin:
50 if origin == host_origin:
47 return True
51 return True
48
52
49 # Check CORS headers
53 # Check CORS headers
50 if self.cors_origin:
54 if self.allow_origin:
51 if self.cors_origin == '*':
55 return self.allow_origin == origin
52 return True
56 elif self.allow_origin_pat:
53 else:
57 return bool(self.allow_origin_pat.match(origin))
54 return self.cors_origin == origin
55 elif self.cors_origin_pat:
56 return bool(self.cors_origin_pat.match(origin))
57 else:
58 else:
58 # No CORS headers, deny the request
59 # No CORS headers deny the request
59 return False
60 return False
60
61
61 def clear_cookie(self, *args, **kwargs):
62 def clear_cookie(self, *args, **kwargs):
62 """meaningless for websockets"""
63 """meaningless for websockets"""
63 pass
64 pass
64
65
65 def _reserialize_reply(self, msg_list):
66 def _reserialize_reply(self, msg_list):
66 """Reserialize a reply message using JSON.
67 """Reserialize a reply message using JSON.
67
68
68 This takes the msg list from the ZMQ socket, unserializes it using
69 This takes the msg list from the ZMQ socket, unserializes it using
69 self.session and then serializes the result using JSON. This method
70 self.session and then serializes the result using JSON. This method
70 should be used by self._on_zmq_reply to build messages that can
71 should be used by self._on_zmq_reply to build messages that can
71 be sent back to the browser.
72 be sent back to the browser.
72 """
73 """
73 idents, msg_list = self.session.feed_identities(msg_list)
74 idents, msg_list = self.session.feed_identities(msg_list)
74 msg = self.session.unserialize(msg_list)
75 msg = self.session.unserialize(msg_list)
75 try:
76 try:
76 msg['header'].pop('date')
77 msg['header'].pop('date')
77 except KeyError:
78 except KeyError:
78 pass
79 pass
79 try:
80 try:
80 msg['parent_header'].pop('date')
81 msg['parent_header'].pop('date')
81 except KeyError:
82 except KeyError:
82 pass
83 pass
83 msg.pop('buffers')
84 msg.pop('buffers')
84 return json.dumps(msg, default=date_default)
85 return json.dumps(msg, default=date_default)
85
86
86 def _on_zmq_reply(self, msg_list):
87 def _on_zmq_reply(self, msg_list):
87 # Sometimes this gets triggered when the on_close method is scheduled in the
88 # Sometimes this gets triggered when the on_close method is scheduled in the
88 # eventloop but hasn't been called.
89 # eventloop but hasn't been called.
89 if self.stream.closed(): return
90 if self.stream.closed(): return
90 try:
91 try:
91 msg = self._reserialize_reply(msg_list)
92 msg = self._reserialize_reply(msg_list)
92 except Exception:
93 except Exception:
93 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
94 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
94 else:
95 else:
95 self.write_message(msg)
96 self.write_message(msg)
96
97
97 def allow_draft76(self):
98 def allow_draft76(self):
98 """Allow draft 76, until browsers such as Safari update to RFC 6455.
99 """Allow draft 76, until browsers such as Safari update to RFC 6455.
99
100
100 This has been disabled by default in tornado in release 2.2.0, and
101 This has been disabled by default in tornado in release 2.2.0, and
101 support will be removed in later versions.
102 support will be removed in later versions.
102 """
103 """
103 return True
104 return True
104
105
105
106
106 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
107 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
107 def set_default_headers(self):
108 def set_default_headers(self):
108 """Undo the set_default_headers in IPythonHandler
109 """Undo the set_default_headers in IPythonHandler
109
110
110 which doesn't make sense for websockets
111 which doesn't make sense for websockets
111 """
112 """
112 pass
113 pass
113
114
114 def open(self, kernel_id):
115 def open(self, kernel_id):
115 self.kernel_id = cast_unicode(kernel_id, 'ascii')
116 self.kernel_id = cast_unicode(kernel_id, 'ascii')
116 # Check to see that origin matches host directly, including ports
117 # Check to see that origin matches host directly, including ports
117 # Tornado 4 already does CORS checking
118 # Tornado 4 already does CORS checking
118 if tornado.version_info[0] < 4:
119 if tornado.version_info[0] < 4:
119 if not self.check_origin(self.get_origin()):
120 if not self.check_origin(self.get_origin()):
120 self.log.warn("Cross Origin WebSocket Attempt.")
121 self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin())
121 raise web.HTTPError(404)
122 raise web.HTTPError(403)
122
123
123 self.session = Session(config=self.config)
124 self.session = Session(config=self.config)
124 self.save_on_message = self.on_message
125 self.save_on_message = self.on_message
125 self.on_message = self.on_first_message
126 self.on_message = self.on_first_message
126
127
127 def _inject_cookie_message(self, msg):
128 def _inject_cookie_message(self, msg):
128 """Inject the first message, which is the document cookie,
129 """Inject the first message, which is the document cookie,
129 for authentication."""
130 for authentication."""
130 if not PY3 and isinstance(msg, unicode):
131 if not PY3 and isinstance(msg, unicode):
131 # Cookie constructor doesn't accept unicode strings
132 # Cookie constructor doesn't accept unicode strings
132 # under Python 2.x for some reason
133 # under Python 2.x for some reason
133 msg = msg.encode('utf8', 'replace')
134 msg = msg.encode('utf8', 'replace')
134 try:
135 try:
135 identity, msg = msg.split(':', 1)
136 identity, msg = msg.split(':', 1)
136 self.session.session = cast_unicode(identity, 'ascii')
137 self.session.session = cast_unicode(identity, 'ascii')
137 except Exception:
138 except Exception:
138 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
139 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
139
140
140 try:
141 try:
141 self.request._cookies = SimpleCookie(msg)
142 self.request._cookies = SimpleCookie(msg)
142 except:
143 except:
143 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
144 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
144
145
145 def on_first_message(self, msg):
146 def on_first_message(self, msg):
146 self._inject_cookie_message(msg)
147 self._inject_cookie_message(msg)
147 if self.get_current_user() is None:
148 if self.get_current_user() is None:
148 self.log.warn("Couldn't authenticate WebSocket connection")
149 self.log.warn("Couldn't authenticate WebSocket connection")
149 raise web.HTTPError(403)
150 raise web.HTTPError(403)
150 self.on_message = self.save_on_message
151 self.on_message = self.save_on_message
@@ -1,901 +1,901 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import errno
9 import errno
10 import io
10 import io
11 import json
11 import json
12 import logging
12 import logging
13 import os
13 import os
14 import random
14 import random
15 import re
15 import re
16 import select
16 import select
17 import signal
17 import signal
18 import socket
18 import socket
19 import sys
19 import sys
20 import threading
20 import threading
21 import time
21 import time
22 import webbrowser
22 import webbrowser
23
23
24
24
25 # check for pyzmq 2.1.11
25 # check for pyzmq 2.1.11
26 from IPython.utils.zmqrelated import check_for_zmq
26 from IPython.utils.zmqrelated import check_for_zmq
27 check_for_zmq('2.1.11', 'IPython.html')
27 check_for_zmq('2.1.11', 'IPython.html')
28
28
29 from jinja2 import Environment, FileSystemLoader
29 from jinja2 import Environment, FileSystemLoader
30
30
31 # Install the pyzmq ioloop. This has to be done before anything else from
31 # Install the pyzmq ioloop. This has to be done before anything else from
32 # tornado is imported.
32 # tornado is imported.
33 from zmq.eventloop import ioloop
33 from zmq.eventloop import ioloop
34 ioloop.install()
34 ioloop.install()
35
35
36 # check for tornado 3.1.0
36 # check for tornado 3.1.0
37 msg = "The IPython Notebook requires tornado >= 3.1.0"
37 msg = "The IPython Notebook requires tornado >= 3.1.0"
38 try:
38 try:
39 import tornado
39 import tornado
40 except ImportError:
40 except ImportError:
41 raise ImportError(msg)
41 raise ImportError(msg)
42 try:
42 try:
43 version_info = tornado.version_info
43 version_info = tornado.version_info
44 except AttributeError:
44 except AttributeError:
45 raise ImportError(msg + ", but you have < 1.1.0")
45 raise ImportError(msg + ", but you have < 1.1.0")
46 if version_info < (3,1,0):
46 if version_info < (3,1,0):
47 raise ImportError(msg + ", but you have %s" % tornado.version)
47 raise ImportError(msg + ", but you have %s" % tornado.version)
48
48
49 from tornado import httpserver
49 from tornado import httpserver
50 from tornado import web
50 from tornado import web
51 from tornado.log import LogFormatter
51 from tornado.log import LogFormatter
52
52
53 from IPython.html import DEFAULT_STATIC_FILES_PATH
53 from IPython.html import DEFAULT_STATIC_FILES_PATH
54 from .base.handlers import Template404
54 from .base.handlers import Template404
55 from .log import log_request
55 from .log import log_request
56 from .services.kernels.kernelmanager import MappingKernelManager
56 from .services.kernels.kernelmanager import MappingKernelManager
57 from .services.notebooks.nbmanager import NotebookManager
57 from .services.notebooks.nbmanager import NotebookManager
58 from .services.notebooks.filenbmanager import FileNotebookManager
58 from .services.notebooks.filenbmanager import FileNotebookManager
59 from .services.clusters.clustermanager import ClusterManager
59 from .services.clusters.clustermanager import ClusterManager
60 from .services.sessions.sessionmanager import SessionManager
60 from .services.sessions.sessionmanager import SessionManager
61
61
62 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
62 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
63
63
64 from IPython.config import Config
64 from IPython.config import Config
65 from IPython.config.application import catch_config_error, boolean_flag
65 from IPython.config.application import catch_config_error, boolean_flag
66 from IPython.core.application import (
66 from IPython.core.application import (
67 BaseIPythonApplication, base_flags, base_aliases,
67 BaseIPythonApplication, base_flags, base_aliases,
68 )
68 )
69 from IPython.core.profiledir import ProfileDir
69 from IPython.core.profiledir import ProfileDir
70 from IPython.kernel import KernelManager
70 from IPython.kernel import KernelManager
71 from IPython.kernel.kernelspec import KernelSpecManager
71 from IPython.kernel.kernelspec import KernelSpecManager
72 from IPython.kernel.zmq.session import default_secure, Session
72 from IPython.kernel.zmq.session import default_secure, Session
73 from IPython.nbformat.sign import NotebookNotary
73 from IPython.nbformat.sign import NotebookNotary
74 from IPython.utils.importstring import import_item
74 from IPython.utils.importstring import import_item
75 from IPython.utils import submodule
75 from IPython.utils import submodule
76 from IPython.utils.traitlets import (
76 from IPython.utils.traitlets import (
77 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
77 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
78 DottedObjectName, TraitError,
78 DottedObjectName, TraitError,
79 )
79 )
80 from IPython.utils import py3compat
80 from IPython.utils import py3compat
81 from IPython.utils.path import filefind, get_ipython_dir
81 from IPython.utils.path import filefind, get_ipython_dir
82
82
83 from .utils import url_path_join
83 from .utils import url_path_join
84
84
85 #-----------------------------------------------------------------------------
85 #-----------------------------------------------------------------------------
86 # Module globals
86 # Module globals
87 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
88
88
89 _examples = """
89 _examples = """
90 ipython notebook # start the notebook
90 ipython notebook # start the notebook
91 ipython notebook --profile=sympy # use the sympy profile
91 ipython notebook --profile=sympy # use the sympy profile
92 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
92 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
93 """
93 """
94
94
95 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
96 # Helper functions
96 # Helper functions
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98
98
99 def random_ports(port, n):
99 def random_ports(port, n):
100 """Generate a list of n random ports near the given port.
100 """Generate a list of n random ports near the given port.
101
101
102 The first 5 ports will be sequential, and the remaining n-5 will be
102 The first 5 ports will be sequential, and the remaining n-5 will be
103 randomly selected in the range [port-2*n, port+2*n].
103 randomly selected in the range [port-2*n, port+2*n].
104 """
104 """
105 for i in range(min(5, n)):
105 for i in range(min(5, n)):
106 yield port + i
106 yield port + i
107 for i in range(n-5):
107 for i in range(n-5):
108 yield max(1, port + random.randint(-2*n, 2*n))
108 yield max(1, port + random.randint(-2*n, 2*n))
109
109
110 def load_handlers(name):
110 def load_handlers(name):
111 """Load the (URL pattern, handler) tuples for each component."""
111 """Load the (URL pattern, handler) tuples for each component."""
112 name = 'IPython.html.' + name
112 name = 'IPython.html.' + name
113 mod = __import__(name, fromlist=['default_handlers'])
113 mod = __import__(name, fromlist=['default_handlers'])
114 return mod.default_handlers
114 return mod.default_handlers
115
115
116 #-----------------------------------------------------------------------------
116 #-----------------------------------------------------------------------------
117 # The Tornado web application
117 # The Tornado web application
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119
119
120 class NotebookWebApplication(web.Application):
120 class NotebookWebApplication(web.Application):
121
121
122 def __init__(self, ipython_app, kernel_manager, notebook_manager,
122 def __init__(self, ipython_app, kernel_manager, notebook_manager,
123 cluster_manager, session_manager, kernel_spec_manager, log,
123 cluster_manager, session_manager, kernel_spec_manager, log,
124 base_url, settings_overrides, jinja_env_options):
124 base_url, settings_overrides, jinja_env_options):
125
125
126 settings = self.init_settings(
126 settings = self.init_settings(
127 ipython_app, kernel_manager, notebook_manager, cluster_manager,
127 ipython_app, kernel_manager, notebook_manager, cluster_manager,
128 session_manager, kernel_spec_manager, log, base_url,
128 session_manager, kernel_spec_manager, log, base_url,
129 settings_overrides, jinja_env_options)
129 settings_overrides, jinja_env_options)
130 handlers = self.init_handlers(settings)
130 handlers = self.init_handlers(settings)
131
131
132 super(NotebookWebApplication, self).__init__(handlers, **settings)
132 super(NotebookWebApplication, self).__init__(handlers, **settings)
133
133
134 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
134 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
135 cluster_manager, session_manager, kernel_spec_manager,
135 cluster_manager, session_manager, kernel_spec_manager,
136 log, base_url, settings_overrides,
136 log, base_url, settings_overrides,
137 jinja_env_options=None):
137 jinja_env_options=None):
138 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
138 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
139 # base_url will always be unicode, which will in turn
139 # base_url will always be unicode, which will in turn
140 # make the patterns unicode, and ultimately result in unicode
140 # make the patterns unicode, and ultimately result in unicode
141 # keys in kwargs to handler._execute(**kwargs) in tornado.
141 # keys in kwargs to handler._execute(**kwargs) in tornado.
142 # This enforces that base_url be ascii in that situation.
142 # This enforces that base_url be ascii in that situation.
143 #
143 #
144 # Note that the URLs these patterns check against are escaped,
144 # Note that the URLs these patterns check against are escaped,
145 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
145 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
146 base_url = py3compat.unicode_to_str(base_url, 'ascii')
146 base_url = py3compat.unicode_to_str(base_url, 'ascii')
147 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
147 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
148 jenv_opt = jinja_env_options if jinja_env_options else {}
148 jenv_opt = jinja_env_options if jinja_env_options else {}
149 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
149 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
150 settings = dict(
150 settings = dict(
151 # basics
151 # basics
152 log_function=log_request,
152 log_function=log_request,
153 base_url=base_url,
153 base_url=base_url,
154 template_path=template_path,
154 template_path=template_path,
155 static_path=ipython_app.static_file_path,
155 static_path=ipython_app.static_file_path,
156 static_handler_class = FileFindHandler,
156 static_handler_class = FileFindHandler,
157 static_url_prefix = url_path_join(base_url,'/static/'),
157 static_url_prefix = url_path_join(base_url,'/static/'),
158
158
159 # authentication
159 # authentication
160 cookie_secret=ipython_app.cookie_secret,
160 cookie_secret=ipython_app.cookie_secret,
161 login_url=url_path_join(base_url,'/login'),
161 login_url=url_path_join(base_url,'/login'),
162 password=ipython_app.password,
162 password=ipython_app.password,
163
163
164 # managers
164 # managers
165 kernel_manager=kernel_manager,
165 kernel_manager=kernel_manager,
166 notebook_manager=notebook_manager,
166 notebook_manager=notebook_manager,
167 cluster_manager=cluster_manager,
167 cluster_manager=cluster_manager,
168 session_manager=session_manager,
168 session_manager=session_manager,
169 kernel_spec_manager=kernel_spec_manager,
169 kernel_spec_manager=kernel_spec_manager,
170
170
171 # IPython stuff
171 # IPython stuff
172 nbextensions_path = ipython_app.nbextensions_path,
172 nbextensions_path = ipython_app.nbextensions_path,
173 mathjax_url=ipython_app.mathjax_url,
173 mathjax_url=ipython_app.mathjax_url,
174 config=ipython_app.config,
174 config=ipython_app.config,
175 jinja2_env=env,
175 jinja2_env=env,
176 )
176 )
177
177
178 # allow custom overrides for the tornado web app.
178 # allow custom overrides for the tornado web app.
179 settings.update(settings_overrides)
179 settings.update(settings_overrides)
180 return settings
180 return settings
181
181
182 def init_handlers(self, settings):
182 def init_handlers(self, settings):
183 # Load the (URL pattern, handler) tuples for each component.
183 # Load the (URL pattern, handler) tuples for each component.
184 handlers = []
184 handlers = []
185 handlers.extend(load_handlers('base.handlers'))
185 handlers.extend(load_handlers('base.handlers'))
186 handlers.extend(load_handlers('tree.handlers'))
186 handlers.extend(load_handlers('tree.handlers'))
187 handlers.extend(load_handlers('auth.login'))
187 handlers.extend(load_handlers('auth.login'))
188 handlers.extend(load_handlers('auth.logout'))
188 handlers.extend(load_handlers('auth.logout'))
189 handlers.extend(load_handlers('notebook.handlers'))
189 handlers.extend(load_handlers('notebook.handlers'))
190 handlers.extend(load_handlers('nbconvert.handlers'))
190 handlers.extend(load_handlers('nbconvert.handlers'))
191 handlers.extend(load_handlers('kernelspecs.handlers'))
191 handlers.extend(load_handlers('kernelspecs.handlers'))
192 handlers.extend(load_handlers('services.kernels.handlers'))
192 handlers.extend(load_handlers('services.kernels.handlers'))
193 handlers.extend(load_handlers('services.notebooks.handlers'))
193 handlers.extend(load_handlers('services.notebooks.handlers'))
194 handlers.extend(load_handlers('services.clusters.handlers'))
194 handlers.extend(load_handlers('services.clusters.handlers'))
195 handlers.extend(load_handlers('services.sessions.handlers'))
195 handlers.extend(load_handlers('services.sessions.handlers'))
196 handlers.extend(load_handlers('services.nbconvert.handlers'))
196 handlers.extend(load_handlers('services.nbconvert.handlers'))
197 handlers.extend(load_handlers('services.kernelspecs.handlers'))
197 handlers.extend(load_handlers('services.kernelspecs.handlers'))
198 # FIXME: /files/ should be handled by the Contents service when it exists
198 # FIXME: /files/ should be handled by the Contents service when it exists
199 nbm = settings['notebook_manager']
199 nbm = settings['notebook_manager']
200 if hasattr(nbm, 'notebook_dir'):
200 if hasattr(nbm, 'notebook_dir'):
201 handlers.extend([
201 handlers.extend([
202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
203 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
203 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
204 ])
204 ])
205 # prepend base_url onto the patterns that we match
205 # prepend base_url onto the patterns that we match
206 new_handlers = []
206 new_handlers = []
207 for handler in handlers:
207 for handler in handlers:
208 pattern = url_path_join(settings['base_url'], handler[0])
208 pattern = url_path_join(settings['base_url'], handler[0])
209 new_handler = tuple([pattern] + list(handler[1:]))
209 new_handler = tuple([pattern] + list(handler[1:]))
210 new_handlers.append(new_handler)
210 new_handlers.append(new_handler)
211 # add 404 on the end, which will catch everything that falls through
211 # add 404 on the end, which will catch everything that falls through
212 new_handlers.append((r'(.*)', Template404))
212 new_handlers.append((r'(.*)', Template404))
213 return new_handlers
213 return new_handlers
214
214
215
215
216 class NbserverListApp(BaseIPythonApplication):
216 class NbserverListApp(BaseIPythonApplication):
217
217
218 description="List currently running notebook servers in this profile."
218 description="List currently running notebook servers in this profile."
219
219
220 flags = dict(
220 flags = dict(
221 json=({'NbserverListApp': {'json': True}},
221 json=({'NbserverListApp': {'json': True}},
222 "Produce machine-readable JSON output."),
222 "Produce machine-readable JSON output."),
223 )
223 )
224
224
225 json = Bool(False, config=True,
225 json = Bool(False, config=True,
226 help="If True, each line of output will be a JSON object with the "
226 help="If True, each line of output will be a JSON object with the "
227 "details from the server info file.")
227 "details from the server info file.")
228
228
229 def start(self):
229 def start(self):
230 if not self.json:
230 if not self.json:
231 print("Currently running servers:")
231 print("Currently running servers:")
232 for serverinfo in list_running_servers(self.profile):
232 for serverinfo in list_running_servers(self.profile):
233 if self.json:
233 if self.json:
234 print(json.dumps(serverinfo))
234 print(json.dumps(serverinfo))
235 else:
235 else:
236 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
236 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
237
237
238 #-----------------------------------------------------------------------------
238 #-----------------------------------------------------------------------------
239 # Aliases and Flags
239 # Aliases and Flags
240 #-----------------------------------------------------------------------------
240 #-----------------------------------------------------------------------------
241
241
242 flags = dict(base_flags)
242 flags = dict(base_flags)
243 flags['no-browser']=(
243 flags['no-browser']=(
244 {'NotebookApp' : {'open_browser' : False}},
244 {'NotebookApp' : {'open_browser' : False}},
245 "Don't open the notebook in a browser after startup."
245 "Don't open the notebook in a browser after startup."
246 )
246 )
247 flags['pylab']=(
247 flags['pylab']=(
248 {'NotebookApp' : {'pylab' : 'warn'}},
248 {'NotebookApp' : {'pylab' : 'warn'}},
249 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
249 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
250 )
250 )
251 flags['no-mathjax']=(
251 flags['no-mathjax']=(
252 {'NotebookApp' : {'enable_mathjax' : False}},
252 {'NotebookApp' : {'enable_mathjax' : False}},
253 """Disable MathJax
253 """Disable MathJax
254
254
255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 very large, so you may want to disable it if you have a slow internet
256 very large, so you may want to disable it if you have a slow internet
257 connection, or for offline use of the notebook.
257 connection, or for offline use of the notebook.
258
258
259 When disabled, equations etc. will appear as their untransformed TeX source.
259 When disabled, equations etc. will appear as their untransformed TeX source.
260 """
260 """
261 )
261 )
262
262
263 # Add notebook manager flags
263 # Add notebook manager flags
264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 'Do not auto-save .py scripts for every notebook'))
266 'Do not auto-save .py scripts for every notebook'))
267
267
268 aliases = dict(base_aliases)
268 aliases = dict(base_aliases)
269
269
270 aliases.update({
270 aliases.update({
271 'ip': 'NotebookApp.ip',
271 'ip': 'NotebookApp.ip',
272 'port': 'NotebookApp.port',
272 'port': 'NotebookApp.port',
273 'port-retries': 'NotebookApp.port_retries',
273 'port-retries': 'NotebookApp.port_retries',
274 'transport': 'KernelManager.transport',
274 'transport': 'KernelManager.transport',
275 'keyfile': 'NotebookApp.keyfile',
275 'keyfile': 'NotebookApp.keyfile',
276 'certfile': 'NotebookApp.certfile',
276 'certfile': 'NotebookApp.certfile',
277 'notebook-dir': 'NotebookApp.notebook_dir',
277 'notebook-dir': 'NotebookApp.notebook_dir',
278 'browser': 'NotebookApp.browser',
278 'browser': 'NotebookApp.browser',
279 'pylab': 'NotebookApp.pylab',
279 'pylab': 'NotebookApp.pylab',
280 })
280 })
281
281
282 #-----------------------------------------------------------------------------
282 #-----------------------------------------------------------------------------
283 # NotebookApp
283 # NotebookApp
284 #-----------------------------------------------------------------------------
284 #-----------------------------------------------------------------------------
285
285
286 class NotebookApp(BaseIPythonApplication):
286 class NotebookApp(BaseIPythonApplication):
287
287
288 name = 'ipython-notebook'
288 name = 'ipython-notebook'
289
289
290 description = """
290 description = """
291 The IPython HTML Notebook.
291 The IPython HTML Notebook.
292
292
293 This launches a Tornado based HTML Notebook Server that serves up an
293 This launches a Tornado based HTML Notebook Server that serves up an
294 HTML5/Javascript Notebook client.
294 HTML5/Javascript Notebook client.
295 """
295 """
296 examples = _examples
296 examples = _examples
297 aliases = aliases
297 aliases = aliases
298 flags = flags
298 flags = flags
299
299
300 classes = [
300 classes = [
301 KernelManager, ProfileDir, Session, MappingKernelManager,
301 KernelManager, ProfileDir, Session, MappingKernelManager,
302 NotebookManager, FileNotebookManager, NotebookNotary,
302 NotebookManager, FileNotebookManager, NotebookNotary,
303 ]
303 ]
304 flags = Dict(flags)
304 flags = Dict(flags)
305 aliases = Dict(aliases)
305 aliases = Dict(aliases)
306
306
307 subcommands = dict(
307 subcommands = dict(
308 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
308 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
309 )
309 )
310
310
311 kernel_argv = List(Unicode)
311 kernel_argv = List(Unicode)
312
312
313 _log_formatter_cls = LogFormatter
313 _log_formatter_cls = LogFormatter
314
314
315 def _log_level_default(self):
315 def _log_level_default(self):
316 return logging.INFO
316 return logging.INFO
317
317
318 def _log_datefmt_default(self):
318 def _log_datefmt_default(self):
319 """Exclude date from default date format"""
319 """Exclude date from default date format"""
320 return "%H:%M:%S"
320 return "%H:%M:%S"
321
321
322 def _log_format_default(self):
322 def _log_format_default(self):
323 """override default log format to include time"""
323 """override default log format to include time"""
324 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
324 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
325
325
326 # create requested profiles by default, if they don't exist:
326 # create requested profiles by default, if they don't exist:
327 auto_create = Bool(True)
327 auto_create = Bool(True)
328
328
329 # file to be opened in the notebook server
329 # file to be opened in the notebook server
330 file_to_run = Unicode('', config=True)
330 file_to_run = Unicode('', config=True)
331 def _file_to_run_changed(self, name, old, new):
331 def _file_to_run_changed(self, name, old, new):
332 path, base = os.path.split(new)
332 path, base = os.path.split(new)
333 if path:
333 if path:
334 self.file_to_run = base
334 self.file_to_run = base
335 self.notebook_dir = path
335 self.notebook_dir = path
336
336
337 # Network related information
337 # Network related information
338
338
339 cors_origin = Unicode('', config=True,
339 allow_origin = Unicode('', config=True,
340 help="""Set the Access-Control-Allow-Origin header
340 help="""Set the Access-Control-Allow-Origin header
341
341
342 Use '*' to allow any origin to access your server.
342 Use '*' to allow any origin to access your server.
343
343
344 Mutually exclusive with cors_origin_pat.
344 Takes precedence over allow_origin_pat.
345 """
345 """
346 )
346 )
347
347
348 cors_origin_pat = Unicode('', config=True,
348 allow_origin_pat = Unicode('', config=True,
349 help="""Use a regular expression for the Access-Control-Allow-Origin header
349 help="""Use a regular expression for the Access-Control-Allow-Origin header
350
350
351 Requests from an origin matching the expression will get replies with:
351 Requests from an origin matching the expression will get replies with:
352
352
353 Access-Control-Allow-Origin: origin
353 Access-Control-Allow-Origin: origin
354
354
355 where `origin` is the origin of the request.
355 where `origin` is the origin of the request.
356
356
357 Mutually exclusive with cors_origin.
357 Ignored if allow_origin is set.
358 """
358 """
359 )
359 )
360
360
361 cors_credentials = Bool(False, config=True,
361 allow_credentials = Bool(False, config=True,
362 help="Set the Access-Control-Allow-Credentials: true header"
362 help="Set the Access-Control-Allow-Credentials: true header"
363 )
363 )
364
364
365 ip = Unicode('localhost', config=True,
365 ip = Unicode('localhost', config=True,
366 help="The IP address the notebook server will listen on."
366 help="The IP address the notebook server will listen on."
367 )
367 )
368
368
369 def _ip_changed(self, name, old, new):
369 def _ip_changed(self, name, old, new):
370 if new == u'*': self.ip = u''
370 if new == u'*': self.ip = u''
371
371
372 port = Integer(8888, config=True,
372 port = Integer(8888, config=True,
373 help="The port the notebook server will listen on."
373 help="The port the notebook server will listen on."
374 )
374 )
375 port_retries = Integer(50, config=True,
375 port_retries = Integer(50, config=True,
376 help="The number of additional ports to try if the specified port is not available."
376 help="The number of additional ports to try if the specified port is not available."
377 )
377 )
378
378
379 certfile = Unicode(u'', config=True,
379 certfile = Unicode(u'', config=True,
380 help="""The full path to an SSL/TLS certificate file."""
380 help="""The full path to an SSL/TLS certificate file."""
381 )
381 )
382
382
383 keyfile = Unicode(u'', config=True,
383 keyfile = Unicode(u'', config=True,
384 help="""The full path to a private key file for usage with SSL/TLS."""
384 help="""The full path to a private key file for usage with SSL/TLS."""
385 )
385 )
386
386
387 cookie_secret = Bytes(b'', config=True,
387 cookie_secret = Bytes(b'', config=True,
388 help="""The random bytes used to secure cookies.
388 help="""The random bytes used to secure cookies.
389 By default this is a new random number every time you start the Notebook.
389 By default this is a new random number every time you start the Notebook.
390 Set it to a value in a config file to enable logins to persist across server sessions.
390 Set it to a value in a config file to enable logins to persist across server sessions.
391
391
392 Note: Cookie secrets should be kept private, do not share config files with
392 Note: Cookie secrets should be kept private, do not share config files with
393 cookie_secret stored in plaintext (you can read the value from a file).
393 cookie_secret stored in plaintext (you can read the value from a file).
394 """
394 """
395 )
395 )
396 def _cookie_secret_default(self):
396 def _cookie_secret_default(self):
397 return os.urandom(1024)
397 return os.urandom(1024)
398
398
399 password = Unicode(u'', config=True,
399 password = Unicode(u'', config=True,
400 help="""Hashed password to use for web authentication.
400 help="""Hashed password to use for web authentication.
401
401
402 To generate, type in a python/IPython shell:
402 To generate, type in a python/IPython shell:
403
403
404 from IPython.lib import passwd; passwd()
404 from IPython.lib import passwd; passwd()
405
405
406 The string should be of the form type:salt:hashed-password.
406 The string should be of the form type:salt:hashed-password.
407 """
407 """
408 )
408 )
409
409
410 open_browser = Bool(True, config=True,
410 open_browser = Bool(True, config=True,
411 help="""Whether to open in a browser after starting.
411 help="""Whether to open in a browser after starting.
412 The specific browser used is platform dependent and
412 The specific browser used is platform dependent and
413 determined by the python standard library `webbrowser`
413 determined by the python standard library `webbrowser`
414 module, unless it is overridden using the --browser
414 module, unless it is overridden using the --browser
415 (NotebookApp.browser) configuration option.
415 (NotebookApp.browser) configuration option.
416 """)
416 """)
417
417
418 browser = Unicode(u'', config=True,
418 browser = Unicode(u'', config=True,
419 help="""Specify what command to use to invoke a web
419 help="""Specify what command to use to invoke a web
420 browser when opening the notebook. If not specified, the
420 browser when opening the notebook. If not specified, the
421 default browser will be determined by the `webbrowser`
421 default browser will be determined by the `webbrowser`
422 standard library module, which allows setting of the
422 standard library module, which allows setting of the
423 BROWSER environment variable to override it.
423 BROWSER environment variable to override it.
424 """)
424 """)
425
425
426 webapp_settings = Dict(config=True,
426 webapp_settings = Dict(config=True,
427 help="Supply overrides for the tornado.web.Application that the "
427 help="Supply overrides for the tornado.web.Application that the "
428 "IPython notebook uses.")
428 "IPython notebook uses.")
429
429
430 jinja_environment_options = Dict(config=True,
430 jinja_environment_options = Dict(config=True,
431 help="Supply extra arguments that will be passed to Jinja environment.")
431 help="Supply extra arguments that will be passed to Jinja environment.")
432
432
433
433
434 enable_mathjax = Bool(True, config=True,
434 enable_mathjax = Bool(True, config=True,
435 help="""Whether to enable MathJax for typesetting math/TeX
435 help="""Whether to enable MathJax for typesetting math/TeX
436
436
437 MathJax is the javascript library IPython uses to render math/LaTeX. It is
437 MathJax is the javascript library IPython uses to render math/LaTeX. It is
438 very large, so you may want to disable it if you have a slow internet
438 very large, so you may want to disable it if you have a slow internet
439 connection, or for offline use of the notebook.
439 connection, or for offline use of the notebook.
440
440
441 When disabled, equations etc. will appear as their untransformed TeX source.
441 When disabled, equations etc. will appear as their untransformed TeX source.
442 """
442 """
443 )
443 )
444 def _enable_mathjax_changed(self, name, old, new):
444 def _enable_mathjax_changed(self, name, old, new):
445 """set mathjax url to empty if mathjax is disabled"""
445 """set mathjax url to empty if mathjax is disabled"""
446 if not new:
446 if not new:
447 self.mathjax_url = u''
447 self.mathjax_url = u''
448
448
449 base_url = Unicode('/', config=True,
449 base_url = Unicode('/', config=True,
450 help='''The base URL for the notebook server.
450 help='''The base URL for the notebook server.
451
451
452 Leading and trailing slashes can be omitted,
452 Leading and trailing slashes can be omitted,
453 and will automatically be added.
453 and will automatically be added.
454 ''')
454 ''')
455 def _base_url_changed(self, name, old, new):
455 def _base_url_changed(self, name, old, new):
456 if not new.startswith('/'):
456 if not new.startswith('/'):
457 self.base_url = '/'+new
457 self.base_url = '/'+new
458 elif not new.endswith('/'):
458 elif not new.endswith('/'):
459 self.base_url = new+'/'
459 self.base_url = new+'/'
460
460
461 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
461 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
462 def _base_project_url_changed(self, name, old, new):
462 def _base_project_url_changed(self, name, old, new):
463 self.log.warn("base_project_url is deprecated, use base_url")
463 self.log.warn("base_project_url is deprecated, use base_url")
464 self.base_url = new
464 self.base_url = new
465
465
466 extra_static_paths = List(Unicode, config=True,
466 extra_static_paths = List(Unicode, config=True,
467 help="""Extra paths to search for serving static files.
467 help="""Extra paths to search for serving static files.
468
468
469 This allows adding javascript/css to be available from the notebook server machine,
469 This allows adding javascript/css to be available from the notebook server machine,
470 or overriding individual files in the IPython"""
470 or overriding individual files in the IPython"""
471 )
471 )
472 def _extra_static_paths_default(self):
472 def _extra_static_paths_default(self):
473 return [os.path.join(self.profile_dir.location, 'static')]
473 return [os.path.join(self.profile_dir.location, 'static')]
474
474
475 @property
475 @property
476 def static_file_path(self):
476 def static_file_path(self):
477 """return extra paths + the default location"""
477 """return extra paths + the default location"""
478 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
478 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
479
479
480 nbextensions_path = List(Unicode, config=True,
480 nbextensions_path = List(Unicode, config=True,
481 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
481 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
482 )
482 )
483 def _nbextensions_path_default(self):
483 def _nbextensions_path_default(self):
484 return [os.path.join(get_ipython_dir(), 'nbextensions')]
484 return [os.path.join(get_ipython_dir(), 'nbextensions')]
485
485
486 mathjax_url = Unicode("", config=True,
486 mathjax_url = Unicode("", config=True,
487 help="""The url for MathJax.js."""
487 help="""The url for MathJax.js."""
488 )
488 )
489 def _mathjax_url_default(self):
489 def _mathjax_url_default(self):
490 if not self.enable_mathjax:
490 if not self.enable_mathjax:
491 return u''
491 return u''
492 static_url_prefix = self.webapp_settings.get("static_url_prefix",
492 static_url_prefix = self.webapp_settings.get("static_url_prefix",
493 url_path_join(self.base_url, "static")
493 url_path_join(self.base_url, "static")
494 )
494 )
495
495
496 # try local mathjax, either in nbextensions/mathjax or static/mathjax
496 # try local mathjax, either in nbextensions/mathjax or static/mathjax
497 for (url_prefix, search_path) in [
497 for (url_prefix, search_path) in [
498 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
498 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
499 (static_url_prefix, self.static_file_path),
499 (static_url_prefix, self.static_file_path),
500 ]:
500 ]:
501 self.log.debug("searching for local mathjax in %s", search_path)
501 self.log.debug("searching for local mathjax in %s", search_path)
502 try:
502 try:
503 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
503 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
504 except IOError:
504 except IOError:
505 continue
505 continue
506 else:
506 else:
507 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
507 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
508 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
508 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
509 return url
509 return url
510
510
511 # no local mathjax, serve from CDN
511 # no local mathjax, serve from CDN
512 if self.certfile:
512 if self.certfile:
513 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
513 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
514 host = u"https://c328740.ssl.cf1.rackcdn.com"
514 host = u"https://c328740.ssl.cf1.rackcdn.com"
515 else:
515 else:
516 host = u"http://cdn.mathjax.org"
516 host = u"http://cdn.mathjax.org"
517
517
518 url = host + u"/mathjax/latest/MathJax.js"
518 url = host + u"/mathjax/latest/MathJax.js"
519 self.log.info("Using MathJax from CDN: %s", url)
519 self.log.info("Using MathJax from CDN: %s", url)
520 return url
520 return url
521
521
522 def _mathjax_url_changed(self, name, old, new):
522 def _mathjax_url_changed(self, name, old, new):
523 if new and not self.enable_mathjax:
523 if new and not self.enable_mathjax:
524 # enable_mathjax=False overrides mathjax_url
524 # enable_mathjax=False overrides mathjax_url
525 self.mathjax_url = u''
525 self.mathjax_url = u''
526 else:
526 else:
527 self.log.info("Using MathJax: %s", new)
527 self.log.info("Using MathJax: %s", new)
528
528
529 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
529 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
530 config=True,
530 config=True,
531 help='The notebook manager class to use.'
531 help='The notebook manager class to use.'
532 )
532 )
533 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
533 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
534 config=True,
534 config=True,
535 help='The kernel manager class to use.'
535 help='The kernel manager class to use.'
536 )
536 )
537 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
537 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
538 config=True,
538 config=True,
539 help='The session manager class to use.'
539 help='The session manager class to use.'
540 )
540 )
541 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
541 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
542 config=True,
542 config=True,
543 help='The cluster manager class to use.'
543 help='The cluster manager class to use.'
544 )
544 )
545
545
546 kernel_spec_manager = Instance(KernelSpecManager)
546 kernel_spec_manager = Instance(KernelSpecManager)
547
547
548 def _kernel_spec_manager_default(self):
548 def _kernel_spec_manager_default(self):
549 return KernelSpecManager(ipython_dir=self.ipython_dir)
549 return KernelSpecManager(ipython_dir=self.ipython_dir)
550
550
551 trust_xheaders = Bool(False, config=True,
551 trust_xheaders = Bool(False, config=True,
552 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
552 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
553 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
553 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
554 )
554 )
555
555
556 info_file = Unicode()
556 info_file = Unicode()
557
557
558 def _info_file_default(self):
558 def _info_file_default(self):
559 info_file = "nbserver-%s.json"%os.getpid()
559 info_file = "nbserver-%s.json"%os.getpid()
560 return os.path.join(self.profile_dir.security_dir, info_file)
560 return os.path.join(self.profile_dir.security_dir, info_file)
561
561
562 notebook_dir = Unicode(py3compat.getcwd(), config=True,
562 notebook_dir = Unicode(py3compat.getcwd(), config=True,
563 help="The directory to use for notebooks and kernels."
563 help="The directory to use for notebooks and kernels."
564 )
564 )
565
565
566 pylab = Unicode('disabled', config=True,
566 pylab = Unicode('disabled', config=True,
567 help="""
567 help="""
568 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
568 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
569 """
569 """
570 )
570 )
571 def _pylab_changed(self, name, old, new):
571 def _pylab_changed(self, name, old, new):
572 """when --pylab is specified, display a warning and exit"""
572 """when --pylab is specified, display a warning and exit"""
573 if new != 'warn':
573 if new != 'warn':
574 backend = ' %s' % new
574 backend = ' %s' % new
575 else:
575 else:
576 backend = ''
576 backend = ''
577 self.log.error("Support for specifying --pylab on the command line has been removed.")
577 self.log.error("Support for specifying --pylab on the command line has been removed.")
578 self.log.error(
578 self.log.error(
579 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
579 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
580 )
580 )
581 self.exit(1)
581 self.exit(1)
582
582
583 def _notebook_dir_changed(self, name, old, new):
583 def _notebook_dir_changed(self, name, old, new):
584 """Do a bit of validation of the notebook dir."""
584 """Do a bit of validation of the notebook dir."""
585 if not os.path.isabs(new):
585 if not os.path.isabs(new):
586 # If we receive a non-absolute path, make it absolute.
586 # If we receive a non-absolute path, make it absolute.
587 self.notebook_dir = os.path.abspath(new)
587 self.notebook_dir = os.path.abspath(new)
588 return
588 return
589 if not os.path.isdir(new):
589 if not os.path.isdir(new):
590 raise TraitError("No such notebook dir: %r" % new)
590 raise TraitError("No such notebook dir: %r" % new)
591
591
592 # setting App.notebook_dir implies setting notebook and kernel dirs as well
592 # setting App.notebook_dir implies setting notebook and kernel dirs as well
593 self.config.FileNotebookManager.notebook_dir = new
593 self.config.FileNotebookManager.notebook_dir = new
594 self.config.MappingKernelManager.root_dir = new
594 self.config.MappingKernelManager.root_dir = new
595
595
596
596
597 def parse_command_line(self, argv=None):
597 def parse_command_line(self, argv=None):
598 super(NotebookApp, self).parse_command_line(argv)
598 super(NotebookApp, self).parse_command_line(argv)
599
599
600 if self.extra_args:
600 if self.extra_args:
601 arg0 = self.extra_args[0]
601 arg0 = self.extra_args[0]
602 f = os.path.abspath(arg0)
602 f = os.path.abspath(arg0)
603 self.argv.remove(arg0)
603 self.argv.remove(arg0)
604 if not os.path.exists(f):
604 if not os.path.exists(f):
605 self.log.critical("No such file or directory: %s", f)
605 self.log.critical("No such file or directory: %s", f)
606 self.exit(1)
606 self.exit(1)
607
607
608 # Use config here, to ensure that it takes higher priority than
608 # Use config here, to ensure that it takes higher priority than
609 # anything that comes from the profile.
609 # anything that comes from the profile.
610 c = Config()
610 c = Config()
611 if os.path.isdir(f):
611 if os.path.isdir(f):
612 c.NotebookApp.notebook_dir = f
612 c.NotebookApp.notebook_dir = f
613 elif os.path.isfile(f):
613 elif os.path.isfile(f):
614 c.NotebookApp.file_to_run = f
614 c.NotebookApp.file_to_run = f
615 self.update_config(c)
615 self.update_config(c)
616
616
617 def init_kernel_argv(self):
617 def init_kernel_argv(self):
618 """construct the kernel arguments"""
618 """construct the kernel arguments"""
619 # Kernel should get *absolute* path to profile directory
619 # Kernel should get *absolute* path to profile directory
620 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
620 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
621
621
622 def init_configurables(self):
622 def init_configurables(self):
623 # force Session default to be secure
623 # force Session default to be secure
624 default_secure(self.config)
624 default_secure(self.config)
625 kls = import_item(self.kernel_manager_class)
625 kls = import_item(self.kernel_manager_class)
626 self.kernel_manager = kls(
626 self.kernel_manager = kls(
627 parent=self, log=self.log, kernel_argv=self.kernel_argv,
627 parent=self, log=self.log, kernel_argv=self.kernel_argv,
628 connection_dir = self.profile_dir.security_dir,
628 connection_dir = self.profile_dir.security_dir,
629 )
629 )
630 kls = import_item(self.notebook_manager_class)
630 kls = import_item(self.notebook_manager_class)
631 self.notebook_manager = kls(parent=self, log=self.log)
631 self.notebook_manager = kls(parent=self, log=self.log)
632 kls = import_item(self.session_manager_class)
632 kls = import_item(self.session_manager_class)
633 self.session_manager = kls(parent=self, log=self.log)
633 self.session_manager = kls(parent=self, log=self.log)
634 kls = import_item(self.cluster_manager_class)
634 kls = import_item(self.cluster_manager_class)
635 self.cluster_manager = kls(parent=self, log=self.log)
635 self.cluster_manager = kls(parent=self, log=self.log)
636 self.cluster_manager.update_profiles()
636 self.cluster_manager.update_profiles()
637
637
638 def init_logging(self):
638 def init_logging(self):
639 # This prevents double log messages because tornado use a root logger that
639 # This prevents double log messages because tornado use a root logger that
640 # self.log is a child of. The logging module dipatches log messages to a log
640 # self.log is a child of. The logging module dipatches log messages to a log
641 # and all of its ancenstors until propagate is set to False.
641 # and all of its ancenstors until propagate is set to False.
642 self.log.propagate = False
642 self.log.propagate = False
643
643
644 # hook up tornado 3's loggers to our app handlers
644 # hook up tornado 3's loggers to our app handlers
645 logger = logging.getLogger('tornado')
645 logger = logging.getLogger('tornado')
646 logger.propagate = True
646 logger.propagate = True
647 logger.parent = self.log
647 logger.parent = self.log
648 logger.setLevel(self.log.level)
648 logger.setLevel(self.log.level)
649
649
650 def init_webapp(self):
650 def init_webapp(self):
651 """initialize tornado webapp and httpserver"""
651 """initialize tornado webapp and httpserver"""
652 self.webapp_settings['cors_origin'] = self.cors_origin
652 self.webapp_settings['allow_origin'] = self.allow_origin
653 self.webapp_settings['cors_origin_pat'] = re.compile(self.cors_origin_pat)
653 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
654 self.webapp_settings['cors_credentials'] = self.cors_credentials
654 self.webapp_settings['allow_credentials'] = self.allow_credentials
655
655
656 self.web_app = NotebookWebApplication(
656 self.web_app = NotebookWebApplication(
657 self, self.kernel_manager, self.notebook_manager,
657 self, self.kernel_manager, self.notebook_manager,
658 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
658 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
659 self.log, self.base_url, self.webapp_settings,
659 self.log, self.base_url, self.webapp_settings,
660 self.jinja_environment_options
660 self.jinja_environment_options
661 )
661 )
662 if self.certfile:
662 if self.certfile:
663 ssl_options = dict(certfile=self.certfile)
663 ssl_options = dict(certfile=self.certfile)
664 if self.keyfile:
664 if self.keyfile:
665 ssl_options['keyfile'] = self.keyfile
665 ssl_options['keyfile'] = self.keyfile
666 else:
666 else:
667 ssl_options = None
667 ssl_options = None
668 self.web_app.password = self.password
668 self.web_app.password = self.password
669 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
669 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
670 xheaders=self.trust_xheaders)
670 xheaders=self.trust_xheaders)
671 if not self.ip:
671 if not self.ip:
672 warning = "WARNING: The notebook server is listening on all IP addresses"
672 warning = "WARNING: The notebook server is listening on all IP addresses"
673 if ssl_options is None:
673 if ssl_options is None:
674 self.log.critical(warning + " and not using encryption. This "
674 self.log.critical(warning + " and not using encryption. This "
675 "is not recommended.")
675 "is not recommended.")
676 if not self.password:
676 if not self.password:
677 self.log.critical(warning + " and not using authentication. "
677 self.log.critical(warning + " and not using authentication. "
678 "This is highly insecure and not recommended.")
678 "This is highly insecure and not recommended.")
679 success = None
679 success = None
680 for port in random_ports(self.port, self.port_retries+1):
680 for port in random_ports(self.port, self.port_retries+1):
681 try:
681 try:
682 self.http_server.listen(port, self.ip)
682 self.http_server.listen(port, self.ip)
683 except socket.error as e:
683 except socket.error as e:
684 if e.errno == errno.EADDRINUSE:
684 if e.errno == errno.EADDRINUSE:
685 self.log.info('The port %i is already in use, trying another random port.' % port)
685 self.log.info('The port %i is already in use, trying another random port.' % port)
686 continue
686 continue
687 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
687 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
688 self.log.warn("Permission to listen on port %i denied" % port)
688 self.log.warn("Permission to listen on port %i denied" % port)
689 continue
689 continue
690 else:
690 else:
691 raise
691 raise
692 else:
692 else:
693 self.port = port
693 self.port = port
694 success = True
694 success = True
695 break
695 break
696 if not success:
696 if not success:
697 self.log.critical('ERROR: the notebook server could not be started because '
697 self.log.critical('ERROR: the notebook server could not be started because '
698 'no available port could be found.')
698 'no available port could be found.')
699 self.exit(1)
699 self.exit(1)
700
700
701 @property
701 @property
702 def display_url(self):
702 def display_url(self):
703 ip = self.ip if self.ip else '[all ip addresses on your system]'
703 ip = self.ip if self.ip else '[all ip addresses on your system]'
704 return self._url(ip)
704 return self._url(ip)
705
705
706 @property
706 @property
707 def connection_url(self):
707 def connection_url(self):
708 ip = self.ip if self.ip else 'localhost'
708 ip = self.ip if self.ip else 'localhost'
709 return self._url(ip)
709 return self._url(ip)
710
710
711 def _url(self, ip):
711 def _url(self, ip):
712 proto = 'https' if self.certfile else 'http'
712 proto = 'https' if self.certfile else 'http'
713 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
713 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
714
714
715 def init_signal(self):
715 def init_signal(self):
716 if not sys.platform.startswith('win'):
716 if not sys.platform.startswith('win'):
717 signal.signal(signal.SIGINT, self._handle_sigint)
717 signal.signal(signal.SIGINT, self._handle_sigint)
718 signal.signal(signal.SIGTERM, self._signal_stop)
718 signal.signal(signal.SIGTERM, self._signal_stop)
719 if hasattr(signal, 'SIGUSR1'):
719 if hasattr(signal, 'SIGUSR1'):
720 # Windows doesn't support SIGUSR1
720 # Windows doesn't support SIGUSR1
721 signal.signal(signal.SIGUSR1, self._signal_info)
721 signal.signal(signal.SIGUSR1, self._signal_info)
722 if hasattr(signal, 'SIGINFO'):
722 if hasattr(signal, 'SIGINFO'):
723 # only on BSD-based systems
723 # only on BSD-based systems
724 signal.signal(signal.SIGINFO, self._signal_info)
724 signal.signal(signal.SIGINFO, self._signal_info)
725
725
726 def _handle_sigint(self, sig, frame):
726 def _handle_sigint(self, sig, frame):
727 """SIGINT handler spawns confirmation dialog"""
727 """SIGINT handler spawns confirmation dialog"""
728 # register more forceful signal handler for ^C^C case
728 # register more forceful signal handler for ^C^C case
729 signal.signal(signal.SIGINT, self._signal_stop)
729 signal.signal(signal.SIGINT, self._signal_stop)
730 # request confirmation dialog in bg thread, to avoid
730 # request confirmation dialog in bg thread, to avoid
731 # blocking the App
731 # blocking the App
732 thread = threading.Thread(target=self._confirm_exit)
732 thread = threading.Thread(target=self._confirm_exit)
733 thread.daemon = True
733 thread.daemon = True
734 thread.start()
734 thread.start()
735
735
736 def _restore_sigint_handler(self):
736 def _restore_sigint_handler(self):
737 """callback for restoring original SIGINT handler"""
737 """callback for restoring original SIGINT handler"""
738 signal.signal(signal.SIGINT, self._handle_sigint)
738 signal.signal(signal.SIGINT, self._handle_sigint)
739
739
740 def _confirm_exit(self):
740 def _confirm_exit(self):
741 """confirm shutdown on ^C
741 """confirm shutdown on ^C
742
742
743 A second ^C, or answering 'y' within 5s will cause shutdown,
743 A second ^C, or answering 'y' within 5s will cause shutdown,
744 otherwise original SIGINT handler will be restored.
744 otherwise original SIGINT handler will be restored.
745
745
746 This doesn't work on Windows.
746 This doesn't work on Windows.
747 """
747 """
748 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
748 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
749 time.sleep(0.1)
749 time.sleep(0.1)
750 info = self.log.info
750 info = self.log.info
751 info('interrupted')
751 info('interrupted')
752 print(self.notebook_info())
752 print(self.notebook_info())
753 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
753 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
754 sys.stdout.flush()
754 sys.stdout.flush()
755 r,w,x = select.select([sys.stdin], [], [], 5)
755 r,w,x = select.select([sys.stdin], [], [], 5)
756 if r:
756 if r:
757 line = sys.stdin.readline()
757 line = sys.stdin.readline()
758 if line.lower().startswith('y') and 'n' not in line.lower():
758 if line.lower().startswith('y') and 'n' not in line.lower():
759 self.log.critical("Shutdown confirmed")
759 self.log.critical("Shutdown confirmed")
760 ioloop.IOLoop.instance().stop()
760 ioloop.IOLoop.instance().stop()
761 return
761 return
762 else:
762 else:
763 print("No answer for 5s:", end=' ')
763 print("No answer for 5s:", end=' ')
764 print("resuming operation...")
764 print("resuming operation...")
765 # no answer, or answer is no:
765 # no answer, or answer is no:
766 # set it back to original SIGINT handler
766 # set it back to original SIGINT handler
767 # use IOLoop.add_callback because signal.signal must be called
767 # use IOLoop.add_callback because signal.signal must be called
768 # from main thread
768 # from main thread
769 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
769 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
770
770
771 def _signal_stop(self, sig, frame):
771 def _signal_stop(self, sig, frame):
772 self.log.critical("received signal %s, stopping", sig)
772 self.log.critical("received signal %s, stopping", sig)
773 ioloop.IOLoop.instance().stop()
773 ioloop.IOLoop.instance().stop()
774
774
775 def _signal_info(self, sig, frame):
775 def _signal_info(self, sig, frame):
776 print(self.notebook_info())
776 print(self.notebook_info())
777
777
778 def init_components(self):
778 def init_components(self):
779 """Check the components submodule, and warn if it's unclean"""
779 """Check the components submodule, and warn if it's unclean"""
780 status = submodule.check_submodule_status()
780 status = submodule.check_submodule_status()
781 if status == 'missing':
781 if status == 'missing':
782 self.log.warn("components submodule missing, running `git submodule update`")
782 self.log.warn("components submodule missing, running `git submodule update`")
783 submodule.update_submodules(submodule.ipython_parent())
783 submodule.update_submodules(submodule.ipython_parent())
784 elif status == 'unclean':
784 elif status == 'unclean':
785 self.log.warn("components submodule unclean, you may see 404s on static/components")
785 self.log.warn("components submodule unclean, you may see 404s on static/components")
786 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
786 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
787
787
788 @catch_config_error
788 @catch_config_error
789 def initialize(self, argv=None):
789 def initialize(self, argv=None):
790 super(NotebookApp, self).initialize(argv)
790 super(NotebookApp, self).initialize(argv)
791 self.init_logging()
791 self.init_logging()
792 self.init_kernel_argv()
792 self.init_kernel_argv()
793 self.init_configurables()
793 self.init_configurables()
794 self.init_components()
794 self.init_components()
795 self.init_webapp()
795 self.init_webapp()
796 self.init_signal()
796 self.init_signal()
797
797
798 def cleanup_kernels(self):
798 def cleanup_kernels(self):
799 """Shutdown all kernels.
799 """Shutdown all kernels.
800
800
801 The kernels will shutdown themselves when this process no longer exists,
801 The kernels will shutdown themselves when this process no longer exists,
802 but explicit shutdown allows the KernelManagers to cleanup the connection files.
802 but explicit shutdown allows the KernelManagers to cleanup the connection files.
803 """
803 """
804 self.log.info('Shutting down kernels')
804 self.log.info('Shutting down kernels')
805 self.kernel_manager.shutdown_all()
805 self.kernel_manager.shutdown_all()
806
806
807 def notebook_info(self):
807 def notebook_info(self):
808 "Return the current working directory and the server url information"
808 "Return the current working directory and the server url information"
809 info = self.notebook_manager.info_string() + "\n"
809 info = self.notebook_manager.info_string() + "\n"
810 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
810 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
811 return info + "The IPython Notebook is running at: %s" % self.display_url
811 return info + "The IPython Notebook is running at: %s" % self.display_url
812
812
813 def server_info(self):
813 def server_info(self):
814 """Return a JSONable dict of information about this server."""
814 """Return a JSONable dict of information about this server."""
815 return {'url': self.connection_url,
815 return {'url': self.connection_url,
816 'hostname': self.ip if self.ip else 'localhost',
816 'hostname': self.ip if self.ip else 'localhost',
817 'port': self.port,
817 'port': self.port,
818 'secure': bool(self.certfile),
818 'secure': bool(self.certfile),
819 'base_url': self.base_url,
819 'base_url': self.base_url,
820 'notebook_dir': os.path.abspath(self.notebook_dir),
820 'notebook_dir': os.path.abspath(self.notebook_dir),
821 }
821 }
822
822
823 def write_server_info_file(self):
823 def write_server_info_file(self):
824 """Write the result of server_info() to the JSON file info_file."""
824 """Write the result of server_info() to the JSON file info_file."""
825 with open(self.info_file, 'w') as f:
825 with open(self.info_file, 'w') as f:
826 json.dump(self.server_info(), f, indent=2)
826 json.dump(self.server_info(), f, indent=2)
827
827
828 def remove_server_info_file(self):
828 def remove_server_info_file(self):
829 """Remove the nbserver-<pid>.json file created for this server.
829 """Remove the nbserver-<pid>.json file created for this server.
830
830
831 Ignores the error raised when the file has already been removed.
831 Ignores the error raised when the file has already been removed.
832 """
832 """
833 try:
833 try:
834 os.unlink(self.info_file)
834 os.unlink(self.info_file)
835 except OSError as e:
835 except OSError as e:
836 if e.errno != errno.ENOENT:
836 if e.errno != errno.ENOENT:
837 raise
837 raise
838
838
839 def start(self):
839 def start(self):
840 """ Start the IPython Notebook server app, after initialization
840 """ Start the IPython Notebook server app, after initialization
841
841
842 This method takes no arguments so all configuration and initialization
842 This method takes no arguments so all configuration and initialization
843 must be done prior to calling this method."""
843 must be done prior to calling this method."""
844 if self.subapp is not None:
844 if self.subapp is not None:
845 return self.subapp.start()
845 return self.subapp.start()
846
846
847 info = self.log.info
847 info = self.log.info
848 for line in self.notebook_info().split("\n"):
848 for line in self.notebook_info().split("\n"):
849 info(line)
849 info(line)
850 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
850 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
851
851
852 self.write_server_info_file()
852 self.write_server_info_file()
853
853
854 if self.open_browser or self.file_to_run:
854 if self.open_browser or self.file_to_run:
855 try:
855 try:
856 browser = webbrowser.get(self.browser or None)
856 browser = webbrowser.get(self.browser or None)
857 except webbrowser.Error as e:
857 except webbrowser.Error as e:
858 self.log.warn('No web browser found: %s.' % e)
858 self.log.warn('No web browser found: %s.' % e)
859 browser = None
859 browser = None
860
860
861 if self.file_to_run:
861 if self.file_to_run:
862 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
862 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
863 if not os.path.exists(fullpath):
863 if not os.path.exists(fullpath):
864 self.log.critical("%s does not exist" % fullpath)
864 self.log.critical("%s does not exist" % fullpath)
865 self.exit(1)
865 self.exit(1)
866
866
867 uri = url_path_join('notebooks', self.file_to_run)
867 uri = url_path_join('notebooks', self.file_to_run)
868 else:
868 else:
869 uri = 'tree'
869 uri = 'tree'
870 if browser:
870 if browser:
871 b = lambda : browser.open(url_path_join(self.connection_url, uri),
871 b = lambda : browser.open(url_path_join(self.connection_url, uri),
872 new=2)
872 new=2)
873 threading.Thread(target=b).start()
873 threading.Thread(target=b).start()
874 try:
874 try:
875 ioloop.IOLoop.instance().start()
875 ioloop.IOLoop.instance().start()
876 except KeyboardInterrupt:
876 except KeyboardInterrupt:
877 info("Interrupted...")
877 info("Interrupted...")
878 finally:
878 finally:
879 self.cleanup_kernels()
879 self.cleanup_kernels()
880 self.remove_server_info_file()
880 self.remove_server_info_file()
881
881
882
882
883 def list_running_servers(profile='default'):
883 def list_running_servers(profile='default'):
884 """Iterate over the server info files of running notebook servers.
884 """Iterate over the server info files of running notebook servers.
885
885
886 Given a profile name, find nbserver-* files in the security directory of
886 Given a profile name, find nbserver-* files in the security directory of
887 that profile, and yield dicts of their information, each one pertaining to
887 that profile, and yield dicts of their information, each one pertaining to
888 a currently running notebook server instance.
888 a currently running notebook server instance.
889 """
889 """
890 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
890 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
891 for file in os.listdir(pd.security_dir):
891 for file in os.listdir(pd.security_dir):
892 if file.startswith('nbserver-'):
892 if file.startswith('nbserver-'):
893 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
893 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
894 yield json.load(f)
894 yield json.load(f)
895
895
896 #-----------------------------------------------------------------------------
896 #-----------------------------------------------------------------------------
897 # Main entry point
897 # Main entry point
898 #-----------------------------------------------------------------------------
898 #-----------------------------------------------------------------------------
899
899
900 launch_new_instance = NotebookApp.launch_instance
900 launch_new_instance = NotebookApp.launch_instance
901
901
General Comments 0
You need to be logged in to leave comments. Login now