mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
221 lines
6.6 KiB
Python
221 lines
6.6 KiB
Python
"""Some general objects for use in JupyterHub"""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
import socket
|
|
import warnings
|
|
from urllib.parse import urlparse, urlunparse
|
|
|
|
from traitlets import HasTraits, Instance, Integer, Unicode, default, observe, validate
|
|
|
|
from . import orm
|
|
from .traitlets import URLPrefix
|
|
from .utils import (
|
|
can_connect,
|
|
fmt_ip_url,
|
|
make_ssl_context,
|
|
random_port,
|
|
url_path_join,
|
|
wait_for_http_server,
|
|
wait_for_server,
|
|
)
|
|
|
|
|
|
class Server(HasTraits):
|
|
"""An object representing an HTTP endpoint.
|
|
|
|
*Some* of these reside in the database (user servers),
|
|
but others (Hub, proxy) are in-memory only.
|
|
"""
|
|
|
|
orm_server = Instance(orm.Server, allow_none=True)
|
|
|
|
ip = Unicode()
|
|
connect_ip = Unicode()
|
|
connect_port = Integer()
|
|
proto = Unicode('http')
|
|
port = Integer()
|
|
base_url = URLPrefix('/')
|
|
cookie_name = Unicode('')
|
|
connect_url = Unicode('')
|
|
bind_url = Unicode('')
|
|
certfile = Unicode()
|
|
keyfile = Unicode()
|
|
cafile = Unicode()
|
|
|
|
@default('bind_url')
|
|
def bind_url_default(self):
|
|
"""representation of URL used for binding
|
|
|
|
Never used in APIs, only logging,
|
|
since it can be non-connectable value, such as '', meaning all interfaces.
|
|
"""
|
|
if self.ip in {'', '0.0.0.0', '::'}:
|
|
return self.url.replace(self._connect_ip, fmt_ip_url(self.ip) or '*', 1)
|
|
return self.url
|
|
|
|
@observe('bind_url')
|
|
def _bind_url_changed(self, change):
|
|
urlinfo = urlparse(change.new)
|
|
self.proto = urlinfo.scheme
|
|
self.ip = urlinfo.hostname or ''
|
|
port = urlinfo.port
|
|
if port is None:
|
|
if self.proto == 'https':
|
|
port = 443
|
|
else:
|
|
port = 80
|
|
self.port = port
|
|
|
|
@validate('connect_url')
|
|
def _connect_url_add_prefix(self, proposal):
|
|
"""Ensure connect_url includes base_url"""
|
|
if not proposal.value:
|
|
# Don't add the prefix if the setting is being cleared
|
|
return proposal.value
|
|
urlinfo = urlparse(proposal.value)
|
|
if not urlinfo.path.startswith(self.base_url):
|
|
urlinfo = urlinfo._replace(path=self.base_url)
|
|
return urlunparse(urlinfo)
|
|
return proposal.value
|
|
|
|
@property
|
|
def _connect_ip(self):
|
|
"""The address to use when connecting to this server
|
|
|
|
When `ip` is set to a real ip address, the same value is used.
|
|
When `ip` refers to 'all interfaces' (e.g. '0.0.0.0' or '::'),
|
|
clients connect via hostname by default.
|
|
Setting `connect_ip` explicitly overrides any default behavior.
|
|
"""
|
|
if self.connect_ip:
|
|
return self.connect_ip
|
|
elif self.ip in {'', '0.0.0.0', '::'}:
|
|
# if listening on all interfaces, default to hostname for connect
|
|
return socket.gethostname()
|
|
else:
|
|
return self.ip
|
|
|
|
@property
|
|
def _connect_port(self):
|
|
"""
|
|
The port to use when connecting to this server.
|
|
|
|
Defaults to self.port, but can be overridden by setting self.connect_port
|
|
"""
|
|
if self.connect_port:
|
|
return self.connect_port
|
|
return self.port
|
|
|
|
@classmethod
|
|
def from_orm(cls, orm_server):
|
|
"""Create a server from an orm.Server"""
|
|
return cls(orm_server=orm_server)
|
|
|
|
@classmethod
|
|
def from_url(cls, url):
|
|
"""Create a Server from a given URL"""
|
|
return cls(bind_url=url, base_url=urlparse(url).path)
|
|
|
|
@default('port')
|
|
def _default_port(self):
|
|
return random_port()
|
|
|
|
@observe('orm_server')
|
|
def _orm_server_changed(self, change):
|
|
"""When we get an orm_server, get attributes from there."""
|
|
obj = change.new
|
|
self.proto = obj.proto
|
|
self.ip = obj.ip
|
|
self.port = obj.port
|
|
self.base_url = obj.base_url
|
|
self.cookie_name = obj.cookie_name
|
|
|
|
# setter to pass through to the database
|
|
@observe('ip', 'proto', 'port', 'base_url', 'cookie_name')
|
|
def _change(self, change):
|
|
if self.orm_server and getattr(self.orm_server, change.name) != change.new:
|
|
# setattr on an sqlalchemy object sets the dirty flag,
|
|
# even if the value doesn't change.
|
|
# Avoid calling setattr when there's been no change,
|
|
# to avoid setting the dirty flag and triggering rollback.
|
|
setattr(self.orm_server, change.name, change.new)
|
|
|
|
@property
|
|
def host(self):
|
|
if self.connect_url:
|
|
parsed = urlparse(self.connect_url)
|
|
proto = parsed.scheme
|
|
host = parsed.netloc
|
|
return f"{proto}://{host}"
|
|
|
|
if ':' in self._connect_ip:
|
|
fmt = "{proto}://[{ip}]:{port}"
|
|
else:
|
|
fmt = "{proto}://{ip}:{port}"
|
|
return fmt.format(
|
|
proto=self.proto, ip=self._connect_ip, port=self._connect_port
|
|
)
|
|
|
|
@property
|
|
def url(self):
|
|
if self.connect_url:
|
|
return self.connect_url
|
|
return f"{self.host}{self.base_url}"
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}(url={self.url}, bind_url={self.bind_url})"
|
|
|
|
def wait_up(self, timeout=10, http=False, ssl_context=None, extra_path=""):
|
|
"""Wait for this server to come up"""
|
|
if http:
|
|
ssl_context = ssl_context or make_ssl_context(
|
|
self.keyfile, self.certfile, cafile=self.cafile
|
|
)
|
|
|
|
return wait_for_http_server(
|
|
url_path_join(self.url, extra_path),
|
|
timeout=timeout,
|
|
ssl_context=ssl_context,
|
|
)
|
|
else:
|
|
return wait_for_server(
|
|
self._connect_ip, self._connect_port, timeout=timeout
|
|
)
|
|
|
|
def is_up(self):
|
|
"""Is the server accepting connections?"""
|
|
return can_connect(self._connect_ip, self._connect_port)
|
|
|
|
|
|
class Hub(Server):
|
|
"""Bring it all together at the hub.
|
|
|
|
The Hub is a server, plus its API path suffix
|
|
|
|
the api_url is the full URL plus the api_path suffix on the end
|
|
of the server base_url.
|
|
"""
|
|
|
|
cookie_name = 'jupyterhub-hub-login'
|
|
|
|
@property
|
|
def server(self):
|
|
warnings.warn(
|
|
"Hub.server is deprecated in JupyterHub 0.8. Access attributes on the Hub directly.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self
|
|
|
|
public_host = Unicode()
|
|
routespec = Unicode()
|
|
|
|
@property
|
|
def api_url(self):
|
|
"""return the full API url (with proto://host...)"""
|
|
return url_path_join(self.url, 'api')
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__} {fmt_ip_url(self.ip)}:{self.port}>"
|