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