Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 99 additions & 48 deletions jupyter_server/serverapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,9 @@ def _default_allow_remote(self):
help=_("Extra keyword arguments to pass to `get_secure_cookie`."
" See tornado's get_secure_cookie docs for details.")
)
ssl_options = Dict(config=True,
ssl_options = Dict(
allow_none=True,
config=True,
help=_("""Supply SSL options for the tornado HTTPServer.
See the tornado docs for details."""))

Expand Down Expand Up @@ -1277,7 +1279,7 @@ def init_logging(self):
logger.setLevel(self.log.level)

def init_webapp(self):
"""initialize tornado webapp and httpserver"""
"""initialize tornado webapp"""
self.tornado_settings['allow_origin'] = self.allow_origin
self.tornado_settings['websocket_compression_options'] = self.websocket_compression_options
if self.allow_origin_pat:
Expand All @@ -1304,56 +1306,29 @@ def init_webapp(self):
self.log, self.base_url, self.default_url, self.tornado_settings,
self.jinja_environment_options,
)
ssl_options = self.ssl_options
if self.certfile:
ssl_options['certfile'] = self.certfile
self.ssl_options['certfile'] = self.certfile
if self.keyfile:
ssl_options['keyfile'] = self.keyfile
self.ssl_options['keyfile'] = self.keyfile
if self.client_ca:
ssl_options['ca_certs'] = self.client_ca
if not ssl_options:
self.ssl_options['ca_certs'] = self.client_ca
if len(self.ssl_options) == 0:
# None indicates no SSL config
ssl_options = None
self.ssl_options = None
else:
# SSL may be missing, so only import it if it's to be used
import ssl
# PROTOCOL_TLS selects the highest ssl/tls protocol version that both the client and
# server support. When PROTOCOL_TLS is not available use PROTOCOL_SSLv23.
# PROTOCOL_TLS is new in version 2.7.13, 3.5.3 and 3.6
ssl_options.setdefault(
self.ssl_options.setdefault(
'ssl_version',
getattr(ssl, 'PROTOCOL_TLS', ssl.PROTOCOL_SSLv23)
)
if ssl_options.get('ca_certs', False):
ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED)
if self.ssl_options.get('ca_certs', False):
self.ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED)

self.login_handler_class.validate_security(self, ssl_options=ssl_options)
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
xheaders=self.trust_xheaders,
max_body_size=self.max_body_size,
max_buffer_size=self.max_buffer_size)

success = None
for port in random_ports(self.port, self.port_retries+1):
try:
self.http_server.listen(port, self.ip)
except socket.error as e:
if e.errno == errno.EADDRINUSE:
self.log.info(_('The port %i is already in use, trying another port.') % port)
continue
elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
self.log.warning(_("Permission to listen on port %i denied") % port)
continue
else:
raise
else:
self.port = port
success = True
break
if not success:
self.log.critical(_('ERROR: the Jupyter server could not be started because '
'no available port could be found.'))
self.exit(1)
self.login_handler_class.validate_security(self, ssl_options=self.ssl_options)

@property
def display_url(self):
Expand Down Expand Up @@ -1489,7 +1464,7 @@ def init_components(self):
def init_server_extension_config(self):
"""Consolidate server extensions specified by all configs.

The resulting list is stored on self.nbserver_extensions and updates config object.
The resulting list is stored on self.jpserver_extensions and updates config object.

The extension API is experimental, and may change in future releases.
"""
Expand Down Expand Up @@ -1518,6 +1493,7 @@ def init_server_extensions(self):

The extension API is experimental, and may change in future releases.
"""
# Initialize extensions
for modulename, enabled in sorted(self.jpserver_extensions.items()):
if enabled:
try:
Expand Down Expand Up @@ -1579,8 +1555,74 @@ def init_shutdown_no_activity(self):
pc = ioloop.PeriodicCallback(self.shutdown_no_activity, 60000)
pc.start()

@property
def http_server(self):
"""An instance of Tornado's HTTPServer class for the Server Web Application."""
try:
return self._http_server
except AttributeError:
raise AttributeError(
'An HTTPServer instance has not been created for the '
'Server Web Application. To create an HTTPServer for this '
'application, call `.init_httpserver()`.'
)

def init_httpserver(self):
"""Creates an instance of a Tornado HTTPServer for the Server Web Application
and sets the http_server attribute.
"""
# Check that a web_app has been initialized before starting a server.
if not hasattr(self, 'web_app'):
raise AttributeError('A tornado web application has not be initialized. '
'Try calling `.init_webapp()` first.')

# Create an instance of the server.
self._http_server = httpserver.HTTPServer(
self.web_app,
ssl_options=self.ssl_options,
xheaders=self.trust_xheaders,
max_body_size=self.max_body_size,
max_buffer_size=self.max_buffer_size
)
success = None
for port in random_ports(self.port, self.port_retries+1):
try:
self.http_server.listen(port, self.ip)
except socket.error as e:
if e.errno == errno.EADDRINUSE:
self.log.info(_('The port %i is already in use, trying another port.') % port)
continue
elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
self.log.warning(_("Permission to listen on port %i denied") % port)
continue
else:
raise
else:
self.port = port
success = True
break
if not success:
self.log.critical(_('ERROR: the Jupyter server could not be started because '
'no available port could be found.'))
self.exit(1)

@catch_config_error
def initialize(self, argv=None, load_extensions=True):
def initialize(self, argv=None, load_extensions=True, new_httpserver=True):
"""Initialize the Server application class, configurables, web application, and http server.

Parameters
----------
argv: list or None
CLI arguments to parse.

load_extensions: bool
If True, the server will load server extensions listed in the jpserver_extension trait.
Otherwise, no server extensions will be loaded.

new_httpserver: bool
If True, a tornado HTTPServer instance will be created and configured for the Server Web
Application. This will set the http_server attribute of this class.
"""
super(ServerApp, self).initialize(argv)
self.init_logging()
if self._dispatching:
Expand All @@ -1590,6 +1632,8 @@ def initialize(self, argv=None, load_extensions=True):
self.init_server_extension_config()
self.init_components()
self.init_webapp()
if new_httpserver:
self.init_httpserver()
self.init_terminals()
self.init_signal()
if load_extensions:
Expand Down Expand Up @@ -1718,12 +1762,7 @@ def launch_browser(self):
new=self.webbrowser_open_new)
threading.Thread(target=b).start()

def start(self):
""" Start the Jupyter server app, after initialization

This method takes no arguments so all configuration and initialization
must be done prior to calling this method."""

def start_app(self):
super(ServerApp, self).start()

if not self.allow_root:
Expand Down Expand Up @@ -1763,6 +1802,8 @@ def start(self):
' %s' % self.display_url,
]))

def start_ioloop(self):
"""Start the IO Loop."""
self.io_loop = ioloop.IOLoop.current()
if sys.platform.startswith('win'):
# add no-op to wake every 5s
Expand All @@ -1772,15 +1813,25 @@ def start(self):
try:
self.io_loop.start()
except KeyboardInterrupt:
info(_("Interrupted..."))
self.log.info(_("Interrupted..."))
finally:
self.remove_server_info_file()
self.remove_browser_open_file()
self.cleanup_kernels()

def start(self):
""" Start the Jupyter server app, after initialization

This method takes no arguments so all configuration and initialization
must be done prior to calling this method."""
self.start_app()
self.start_ioloop()

def stop(self):
def _stop():
self.http_server.stop()
# Stop a server if its set.
if hasattr(self, '_http_server'):
self.http_server.stop()
self.io_loop.stop()
self.io_loop.add_callback(_stop)

Expand Down