mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 04:53:01 +00:00
remove Hub, Proxy from database
These are in-memory-only objects, no need for a table with one row
This commit is contained in:
@@ -32,7 +32,7 @@ class APIHandler(BaseHandler):
|
|||||||
self.log.warning("Blocking API request with no referer")
|
self.log.warning("Blocking API request with no referer")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
host_path = url_path_join(host, self.hub.server.base_url)
|
host_path = url_path_join(host, self.hub.base_url)
|
||||||
referer_path = referer.split('://', 1)[-1]
|
referer_path = referer.split('://', 1)[-1]
|
||||||
if not (referer_path + '/').startswith(host_path):
|
if not (referer_path + '/').startswith(host_path):
|
||||||
self.log.warning("Blocking Cross Origin API request. Referer: %s, Host: %s",
|
self.log.warning("Blocking Cross Origin API request. Referer: %s, Host: %s",
|
||||||
|
@@ -62,6 +62,7 @@ from .utils import (
|
|||||||
# classes for config
|
# classes for config
|
||||||
from .auth import Authenticator, PAMAuthenticator
|
from .auth import Authenticator, PAMAuthenticator
|
||||||
from .spawner import Spawner, LocalProcessSpawner
|
from .spawner import Spawner, LocalProcessSpawner
|
||||||
|
from .objects import Server, Hub
|
||||||
|
|
||||||
# For faking stats
|
# For faking stats
|
||||||
from .emptyclass import EmptyClass
|
from .emptyclass import EmptyClass
|
||||||
@@ -805,36 +806,6 @@ class JupyterHub(Application):
|
|||||||
self._local.db = scoped_session(self.session_factory)()
|
self._local.db = scoped_session(self.session_factory)()
|
||||||
return self._local.db
|
return self._local.db
|
||||||
|
|
||||||
@property
|
|
||||||
def hub(self):
|
|
||||||
if not getattr(self._local, 'hub', None):
|
|
||||||
q = self.db.query(orm.Hub)
|
|
||||||
assert q.count() <= 1
|
|
||||||
self._local.hub = q.first()
|
|
||||||
if self.subdomain_host and self._local.hub:
|
|
||||||
self._local.hub.host = self.subdomain_host
|
|
||||||
return self._local.hub
|
|
||||||
|
|
||||||
@hub.setter
|
|
||||||
def hub(self, hub):
|
|
||||||
self._local.hub = hub
|
|
||||||
if hub and self.subdomain_host:
|
|
||||||
hub.host = self.subdomain_host
|
|
||||||
|
|
||||||
@property
|
|
||||||
def proxy(self):
|
|
||||||
if not getattr(self._local, 'proxy', None):
|
|
||||||
q = self.db.query(orm.Proxy)
|
|
||||||
assert q.count() <= 1
|
|
||||||
p = self._local.proxy = q.first()
|
|
||||||
if p:
|
|
||||||
p.auth_token = self.proxy_auth_token
|
|
||||||
return self._local.proxy
|
|
||||||
|
|
||||||
@proxy.setter
|
|
||||||
def proxy(self, proxy):
|
|
||||||
self._local.proxy = proxy
|
|
||||||
|
|
||||||
def init_db(self):
|
def init_db(self):
|
||||||
"""Create the database connection"""
|
"""Create the database connection"""
|
||||||
self.log.debug("Connecting to db: %s", self.db_url)
|
self.log.debug("Connecting to db: %s", self.db_url)
|
||||||
@@ -861,28 +832,13 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
def init_hub(self):
|
def init_hub(self):
|
||||||
"""Load the Hub config into the database"""
|
"""Load the Hub config into the database"""
|
||||||
self.hub = self.db.query(orm.Hub).first()
|
self.hub = Hub(
|
||||||
if self.hub is None:
|
ip=self.hub_ip,
|
||||||
self.hub = orm.Hub(
|
port=self.hub_port,
|
||||||
server=orm.Server(
|
base_url=self.hub_prefix,
|
||||||
ip=self.hub_ip,
|
cookie_name='jupyter-hub-token',
|
||||||
port=self.hub_port,
|
public_host=self.subdomain_host,
|
||||||
base_url=self.hub_prefix,
|
)
|
||||||
cookie_name='jupyter-hub-token',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.db.add(self.hub)
|
|
||||||
else:
|
|
||||||
server = self.hub.server
|
|
||||||
server.ip = self.hub_ip
|
|
||||||
server.port = self.hub_port
|
|
||||||
server.base_url = self.hub_prefix
|
|
||||||
if self.subdomain_host:
|
|
||||||
if not self.subdomain_host:
|
|
||||||
raise ValueError("Must specify subdomain_host when using subdomains."
|
|
||||||
" This should be the public domain[:port] of the Hub.")
|
|
||||||
|
|
||||||
self.db.commit()
|
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_users(self):
|
def init_users(self):
|
||||||
@@ -1231,8 +1187,8 @@ class JupyterHub(Application):
|
|||||||
'--port', str(self.proxy.public_server.port),
|
'--port', str(self.proxy.public_server.port),
|
||||||
'--api-ip', self.proxy.api_server.ip,
|
'--api-ip', self.proxy.api_server.ip,
|
||||||
'--api-port', str(self.proxy.api_server.port),
|
'--api-port', str(self.proxy.api_server.port),
|
||||||
'--default-target', self.hub.server.host,
|
'--default-target', self.hub.host,
|
||||||
'--error-target', url_path_join(self.hub.server.url, 'error'),
|
'--error-target', url_path_join(self.hub.url, 'error'),
|
||||||
]
|
]
|
||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
cmd.append('--host-routing')
|
cmd.append('--host-routing')
|
||||||
@@ -1299,7 +1255,7 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
def init_tornado_settings(self):
|
def init_tornado_settings(self):
|
||||||
"""Set up the tornado settings dict."""
|
"""Set up the tornado settings dict."""
|
||||||
base_url = self.hub.server.base_url
|
base_url = self.hub.base_url
|
||||||
jinja_options = dict(
|
jinja_options = dict(
|
||||||
autoescape=True,
|
autoescape=True,
|
||||||
)
|
)
|
||||||
@@ -1337,7 +1293,7 @@ class JupyterHub(Application):
|
|||||||
login_url=login_url,
|
login_url=login_url,
|
||||||
logout_url=logout_url,
|
logout_url=logout_url,
|
||||||
static_path=os.path.join(self.data_files_path, 'static'),
|
static_path=os.path.join(self.data_files_path, 'static'),
|
||||||
static_url_prefix=url_path_join(self.hub.server.base_url, 'static/'),
|
static_url_prefix=url_path_join(self.hub.base_url, 'static/'),
|
||||||
static_handler_class=CacheControlStaticFilesHandler,
|
static_handler_class=CacheControlStaticFilesHandler,
|
||||||
template_path=self.template_paths,
|
template_path=self.template_paths,
|
||||||
jinja2_env=jinja_env,
|
jinja2_env=jinja_env,
|
||||||
@@ -1530,10 +1486,10 @@ class JupyterHub(Application):
|
|||||||
try:
|
try:
|
||||||
self.http_server.listen(self.hub_port, address=self.hub_ip)
|
self.http_server.listen(self.hub_port, address=self.hub_ip)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.error("Failed to bind hub to %s", self.hub.server.bind_url)
|
self.log.error("Failed to bind hub to %s", self.hub.bind_url)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
self.log.info("Hub API listening on %s", self.hub.server.bind_url)
|
self.log.info("Hub API listening on %s", self.hub.bind_url)
|
||||||
|
|
||||||
# start the proxy
|
# start the proxy
|
||||||
try:
|
try:
|
||||||
|
@@ -103,7 +103,7 @@ class BaseHandler(RequestHandler):
|
|||||||
@property
|
@property
|
||||||
def csp_report_uri(self):
|
def csp_report_uri(self):
|
||||||
return self.settings.get('csp_report_uri',
|
return self.settings.get('csp_report_uri',
|
||||||
url_path_join(self.hub.server.base_url, 'security/csp-report')
|
url_path_join(self.hub.base_url, 'security/csp-report')
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -184,7 +184,7 @@ class BaseHandler(RequestHandler):
|
|||||||
max_age_days=self.cookie_max_age_days,
|
max_age_days=self.cookie_max_age_days,
|
||||||
)
|
)
|
||||||
def clear():
|
def clear():
|
||||||
self.clear_cookie(cookie_name, path=self.hub.server.base_url)
|
self.clear_cookie(cookie_name, path=self.hub.base_url)
|
||||||
|
|
||||||
if cookie_id is None:
|
if cookie_id is None:
|
||||||
if self.get_cookie(cookie_name):
|
if self.get_cookie(cookie_name):
|
||||||
@@ -208,7 +208,7 @@ class BaseHandler(RequestHandler):
|
|||||||
|
|
||||||
def get_current_user_cookie(self):
|
def get_current_user_cookie(self):
|
||||||
"""get_current_user from a cookie token"""
|
"""get_current_user from a cookie token"""
|
||||||
return self._user_for_cookie(self.hub.server.cookie_name)
|
return self._user_for_cookie(self.hub.cookie_name)
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
"""get current username"""
|
"""get current username"""
|
||||||
@@ -247,7 +247,7 @@ class BaseHandler(RequestHandler):
|
|||||||
kwargs['domain'] = self.domain
|
kwargs['domain'] = self.domain
|
||||||
if user and user.server:
|
if user and user.server:
|
||||||
self.clear_cookie(user.server.cookie_name, path=user.server.base_url, **kwargs)
|
self.clear_cookie(user.server.cookie_name, path=user.server.base_url, **kwargs)
|
||||||
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url, **kwargs)
|
self.clear_cookie(self.hub.cookie_name, path=self.hub.base_url, **kwargs)
|
||||||
self.clear_cookie('jupyterhub-services', path=url_path_join(self.base_url, 'services'))
|
self.clear_cookie('jupyterhub-services', path=url_path_join(self.base_url, 'services'))
|
||||||
|
|
||||||
def _set_user_cookie(self, user, server):
|
def _set_user_cookie(self, user, server):
|
||||||
@@ -436,7 +436,7 @@ class BaseHandler(RequestHandler):
|
|||||||
def template_namespace(self):
|
def template_namespace(self):
|
||||||
user = self.get_current_user()
|
user = self.get_current_user()
|
||||||
return dict(
|
return dict(
|
||||||
base_url=self.hub.server.base_url,
|
base_url=self.hub.base_url,
|
||||||
prefix=self.base_url,
|
prefix=self.base_url,
|
||||||
user=user,
|
user=user,
|
||||||
login_url=self.settings['login_url'],
|
login_url=self.settings['login_url'],
|
||||||
@@ -502,7 +502,7 @@ class PrefixRedirectHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
path = self.request.path
|
path = self.request.path
|
||||||
self.redirect(url_path_join(
|
self.redirect(url_path_join(
|
||||||
self.hub.server.base_url, path,
|
self.hub.base_url, path,
|
||||||
), permanent=False)
|
), permanent=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
port = host_info.port
|
port = host_info.port
|
||||||
if not port:
|
if not port:
|
||||||
port = 443 if host_info.scheme == 'https' else 80
|
port = 443 if host_info.scheme == 'https' else 80
|
||||||
if port != self.proxy.public_server.port and port == self.hub.server.port:
|
if port != self.proxy.public_server.port and port == self.hub.port:
|
||||||
self.log.warning("""
|
self.log.warning("""
|
||||||
Detected possible direct connection to Hub's private ip: %s, bypassing proxy.
|
Detected possible direct connection to Hub's private ip: %s, bypassing proxy.
|
||||||
This will result in a redirect loop.
|
This will result in a redirect loop.
|
||||||
@@ -548,14 +548,14 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
status = yield current_user.spawner.poll()
|
status = yield current_user.spawner.poll()
|
||||||
if status is not None:
|
if status is not None:
|
||||||
if current_user.spawner.options_form:
|
if current_user.spawner.options_form:
|
||||||
self.redirect(url_concat(url_path_join(self.hub.server.base_url, 'spawn'),
|
self.redirect(url_concat(url_path_join(self.hub.base_url, 'spawn'),
|
||||||
{'next': self.request.uri}))
|
{'next': self.request.uri}))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
yield self.spawn_single_user(current_user)
|
yield self.spawn_single_user(current_user)
|
||||||
# set login cookie anew
|
# set login cookie anew
|
||||||
self.set_login_cookie(current_user)
|
self.set_login_cookie(current_user)
|
||||||
without_prefix = self.request.uri[len(self.hub.server.base_url):]
|
without_prefix = self.request.uri[len(self.hub.base_url):]
|
||||||
target = url_path_join(self.base_url, without_prefix)
|
target = url_path_join(self.base_url, without_prefix)
|
||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
target = current_user.host + target
|
target = current_user.host + target
|
||||||
|
@@ -54,7 +54,7 @@ class LoginHandler(BaseHandler):
|
|||||||
if user.running:
|
if user.running:
|
||||||
next_url = user.url
|
next_url = user.url
|
||||||
else:
|
else:
|
||||||
next_url = self.hub.server.base_url
|
next_url = self.hub.base_url
|
||||||
# set new login cookie
|
# set new login cookie
|
||||||
# because single-user cookie may have been cleared or incorrect
|
# because single-user cookie may have been cleared or incorrect
|
||||||
self.set_login_cookie(self.get_current_user())
|
self.set_login_cookie(self.get_current_user())
|
||||||
@@ -98,7 +98,7 @@ class LoginHandler(BaseHandler):
|
|||||||
next_url = self.get_argument('next', default='')
|
next_url = self.get_argument('next', default='')
|
||||||
if not next_url.startswith('/'):
|
if not next_url.startswith('/'):
|
||||||
next_url = ''
|
next_url = ''
|
||||||
next_url = next_url or self.hub.server.base_url
|
next_url = next_url or self.hub.base_url
|
||||||
self.redirect(next_url)
|
self.redirect(next_url)
|
||||||
self.log.info("User logged in: %s", username)
|
self.log.info("User logged in: %s", username)
|
||||||
else:
|
else:
|
||||||
|
@@ -37,7 +37,7 @@ class RootHandler(BaseHandler):
|
|||||||
# The next request will be handled by UserSpawnHandler,
|
# The next request will be handled by UserSpawnHandler,
|
||||||
# ultimately redirecting to the logged-in user's server.
|
# ultimately redirecting to the logged-in user's server.
|
||||||
without_prefix = next_url[len(self.base_url):]
|
without_prefix = next_url[len(self.base_url):]
|
||||||
next_url = url_path_join(self.hub.server.base_url, without_prefix)
|
next_url = url_path_join(self.hub.base_url, without_prefix)
|
||||||
self.log.warning("Redirecting %s to %s. For sharing public links, use /user-redirect/",
|
self.log.warning("Redirecting %s to %s. For sharing public links, use /user-redirect/",
|
||||||
self.request.uri, next_url,
|
self.request.uri, next_url,
|
||||||
)
|
)
|
||||||
@@ -50,7 +50,7 @@ class RootHandler(BaseHandler):
|
|||||||
self.log.debug("User is running: %s", url)
|
self.log.debug("User is running: %s", url)
|
||||||
self.set_login_cookie(user) # set cookie
|
self.set_login_cookie(user) # set cookie
|
||||||
else:
|
else:
|
||||||
url = url_path_join(self.hub.server.base_url, 'home')
|
url = url_path_join(self.hub.base_url, 'home')
|
||||||
self.log.debug("User is not running: %s", url)
|
self.log.debug("User is not running: %s", url)
|
||||||
else:
|
else:
|
||||||
url = self.settings['login_url']
|
url = self.settings['login_url']
|
||||||
@@ -215,7 +215,7 @@ class ProxyErrorHandler(BaseHandler):
|
|||||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||||
# build template namespace
|
# build template namespace
|
||||||
|
|
||||||
hub_home = url_path_join(self.hub.server.base_url, 'home')
|
hub_home = url_path_join(self.hub.base_url, 'home')
|
||||||
message_html = ''
|
message_html = ''
|
||||||
if status_code == 503:
|
if status_code == 503:
|
||||||
message_html = ' '.join([
|
message_html = ' '.join([
|
||||||
|
126
jupyterhub/objects.py
Normal file
126
jupyterhub/objects.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""Some general objects for use in JupyterHub"""
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from traitlets import HasTraits
|
||||||
|
from . import orm
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
proto = Unicode('http')
|
||||||
|
port = Integer()
|
||||||
|
base_url = Unicode('/')
|
||||||
|
cookie_name = Unicode('')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_url(cls, url):
|
||||||
|
"""Create a Server from a given URL"""
|
||||||
|
urlinfo = urlparse(url)
|
||||||
|
proto = urlinfo.scheme
|
||||||
|
ip = urlinfo.hostname
|
||||||
|
port = urlinfo.port
|
||||||
|
if not port:
|
||||||
|
if proto == 'https':
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
port = 80
|
||||||
|
return cls(proto=proto, ip=ip, port=port)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
setattr(self.orm_server, change.name, change.new)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
ip = self.ip
|
||||||
|
if ip in {'', '0.0.0.0'}:
|
||||||
|
# when listening on all interfaces, connect to localhost
|
||||||
|
ip = '127.0.0.1'
|
||||||
|
return "{proto}://{ip}:{port}".format(
|
||||||
|
proto=self.proto,
|
||||||
|
ip=ip,
|
||||||
|
port=self.port,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return "{host}{uri}".format(
|
||||||
|
host=self.host,
|
||||||
|
uri=self.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bind_url(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('127.0.0.1', self.ip or '*', 1)
|
||||||
|
return self.url
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def wait_up(self, timeout=10, http=False):
|
||||||
|
"""Wait for this server to come up"""
|
||||||
|
if http:
|
||||||
|
yield wait_for_http_server(self.url, timeout=timeout)
|
||||||
|
else:
|
||||||
|
yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout)
|
||||||
|
|
||||||
|
def is_up(self):
|
||||||
|
"""Is the server accepting connections?"""
|
||||||
|
return can_connect(self.ip or '127.0.0.1', self.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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self):
|
||||||
|
"""backward-compat"""
|
||||||
|
return self
|
||||||
|
host = Unicode()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_url(self):
|
||||||
|
"""return the full API url (with proto://host...)"""
|
||||||
|
return url_path_join(self.server.url, 'api')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s %s:%s>" % (
|
||||||
|
self.__class__.__name__, self.server.ip, self.server.port,
|
||||||
|
)
|
@@ -79,240 +79,6 @@ class Server(Base):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||||
|
|
||||||
@property
|
|
||||||
def host(self):
|
|
||||||
ip = self.ip
|
|
||||||
if ip in {'', '0.0.0.0'}:
|
|
||||||
# when listening on all interfaces, connect to localhost
|
|
||||||
ip = '127.0.0.1'
|
|
||||||
return "{proto}://{ip}:{port}".format(
|
|
||||||
proto=self.proto,
|
|
||||||
ip=ip,
|
|
||||||
port=self.port,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return "{host}{uri}".format(
|
|
||||||
host=self.host,
|
|
||||||
uri=self.base_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bind_url(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('127.0.0.1', self.ip or '*', 1)
|
|
||||||
return self.url
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def wait_up(self, timeout=10, http=False):
|
|
||||||
"""Wait for this server to come up"""
|
|
||||||
if http:
|
|
||||||
yield wait_for_http_server(self.url, timeout=timeout)
|
|
||||||
else:
|
|
||||||
yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout)
|
|
||||||
|
|
||||||
def is_up(self):
|
|
||||||
"""Is the server accepting connections?"""
|
|
||||||
return can_connect(self.ip or '127.0.0.1', self.port)
|
|
||||||
|
|
||||||
|
|
||||||
class Proxy(Base):
|
|
||||||
"""A configurable-http-proxy instance.
|
|
||||||
|
|
||||||
A proxy consists of the API server info and the public-facing server info,
|
|
||||||
plus an auth token for configuring the proxy table.
|
|
||||||
"""
|
|
||||||
__tablename__ = 'proxies'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
auth_token = None
|
|
||||||
_public_server_id = Column(Integer, ForeignKey('servers.id'))
|
|
||||||
public_server = relationship(Server, primaryjoin=_public_server_id == Server.id)
|
|
||||||
_api_server_id = Column(Integer, ForeignKey('servers.id'))
|
|
||||||
api_server = relationship(Server, primaryjoin=_api_server_id == Server.id)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.public_server:
|
|
||||||
return "<%s %s:%s>" % (
|
|
||||||
self.__class__.__name__, self.public_server.ip, self.public_server.port,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return "<%s [unconfigured]>" % self.__class__.__name__
|
|
||||||
|
|
||||||
def api_request(self, path, method='GET', body=None, client=None):
|
|
||||||
"""Make an authenticated API request of the proxy"""
|
|
||||||
client = client or AsyncHTTPClient()
|
|
||||||
url = url_path_join(self.api_server.url, path)
|
|
||||||
|
|
||||||
if isinstance(body, dict):
|
|
||||||
body = json.dumps(body)
|
|
||||||
self.log.debug("Fetching %s %s", method, url)
|
|
||||||
req = HTTPRequest(url,
|
|
||||||
method=method,
|
|
||||||
headers={'Authorization': 'token {}'.format(self.auth_token)},
|
|
||||||
body=body,
|
|
||||||
)
|
|
||||||
|
|
||||||
return client.fetch(req)
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def add_service(self, service, client=None):
|
|
||||||
"""Add a service's server to the proxy table."""
|
|
||||||
if not service.server:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Service %s does not have an http endpoint to add to the proxy.", service.name)
|
|
||||||
|
|
||||||
self.log.info("Adding service %s to proxy %s => %s",
|
|
||||||
service.name, service.proxy_path, service.server.host,
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.proxy.add_route(
|
|
||||||
service.proxy_path,
|
|
||||||
service.server.host,
|
|
||||||
{'service': service.name}
|
|
||||||
)
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def delete_service(self, service, client=None):
|
|
||||||
"""Remove a service's server from the proxy table."""
|
|
||||||
self.log.info("Removing service %s from proxy", service.name)
|
|
||||||
yield self.proxy.delete_route(service.proxy_path)
|
|
||||||
|
|
||||||
# FIX-ME
|
|
||||||
# we need to add a reference to a specific server
|
|
||||||
@gen.coroutine
|
|
||||||
def add_user(self, user, client=None):
|
|
||||||
"""Add a user's server to the proxy table."""
|
|
||||||
self.log.info("Adding user %s to proxy %s => %s",
|
|
||||||
user.name, user.proxy_path, user.server.host,
|
|
||||||
)
|
|
||||||
|
|
||||||
if user.spawn_pending:
|
|
||||||
raise RuntimeError(
|
|
||||||
"User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name)
|
|
||||||
|
|
||||||
yield self.proxy.add_route(
|
|
||||||
user.proxy_path,
|
|
||||||
user.server.host,
|
|
||||||
{'user': user.name}
|
|
||||||
)
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def delete_user(self, user, client=None):
|
|
||||||
"""Remove a user's server from the proxy table."""
|
|
||||||
self.log.info("Removing user %s from proxy", user.name)
|
|
||||||
yield self.proxy.delete_route(
|
|
||||||
user.proxy_path
|
|
||||||
)
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def add_all_services(self, service_dict):
|
|
||||||
"""Update the proxy table from the database.
|
|
||||||
|
|
||||||
Used when loading up a new proxy.
|
|
||||||
"""
|
|
||||||
db = inspect(self).session
|
|
||||||
futures = []
|
|
||||||
for orm_service in db.query(Service):
|
|
||||||
service = service_dict[orm_service.name]
|
|
||||||
if service.server:
|
|
||||||
futures.append(self.add_service(service))
|
|
||||||
# wait after submitting them all
|
|
||||||
for f in futures:
|
|
||||||
yield f
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def add_all_users(self, user_dict):
|
|
||||||
"""Update the proxy table from the database.
|
|
||||||
|
|
||||||
Used when loading up a new proxy.
|
|
||||||
"""
|
|
||||||
db = inspect(self).session
|
|
||||||
futures = []
|
|
||||||
for orm_user in db.query(User):
|
|
||||||
user = user_dict[orm_user]
|
|
||||||
if user.running:
|
|
||||||
futures.append(self.add_user(user))
|
|
||||||
# wait after submitting them all
|
|
||||||
for f in futures:
|
|
||||||
yield f
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def get_routes(self, client=None):
|
|
||||||
"""Fetch the proxy's routes"""
|
|
||||||
return (yield self.proxy.get_all_routes())
|
|
||||||
|
|
||||||
# FIX-ME
|
|
||||||
# we need to add a reference to a specific server
|
|
||||||
@gen.coroutine
|
|
||||||
def check_routes(self, user_dict, service_dict, routes=None):
|
|
||||||
"""Check that all users are properly routed on the proxy"""
|
|
||||||
if not routes:
|
|
||||||
routes = yield self.get_routes()
|
|
||||||
|
|
||||||
user_routes = { r['user'] for r in routes.values() if 'user' in r }
|
|
||||||
futures = []
|
|
||||||
db = inspect(self).session
|
|
||||||
for orm_user in db.query(User):
|
|
||||||
user = user_dict[orm_user]
|
|
||||||
if user.running:
|
|
||||||
if user.name not in user_routes:
|
|
||||||
self.log.warning("Adding missing route for %s (%s)", user.name, user.server)
|
|
||||||
futures.append(self.add_user(user))
|
|
||||||
else:
|
|
||||||
# User not running, make sure it's not in the table
|
|
||||||
if user.name in user_routes:
|
|
||||||
self.log.warning("Removing route for not running %s", user.name)
|
|
||||||
futures.append(self.delete_user(user))
|
|
||||||
|
|
||||||
# check service routes
|
|
||||||
service_routes = { r['service'] for r in routes.values() if 'service' in r }
|
|
||||||
for orm_service in db.query(Service).filter(Service.server != None):
|
|
||||||
service = service_dict[orm_service.name]
|
|
||||||
if service.server is None:
|
|
||||||
# This should never be True, but seems to be on rare occasion.
|
|
||||||
# catch filter bug, either in sqlalchemy or my understanding of its behavior
|
|
||||||
self.log.error("Service %s has no server, but wasn't filtered out.", service)
|
|
||||||
continue
|
|
||||||
if service.name not in service_routes:
|
|
||||||
self.log.warning("Adding missing route for %s (%s)", service.name, service.server)
|
|
||||||
futures.append(self.add_service(service))
|
|
||||||
for f in futures:
|
|
||||||
yield f
|
|
||||||
|
|
||||||
|
|
||||||
class Hub(Base):
|
|
||||||
"""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.
|
|
||||||
"""
|
|
||||||
__tablename__ = 'hubs'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
_server_id = Column(Integer, ForeignKey('servers.id'))
|
|
||||||
server = relationship(Server, primaryjoin=_server_id == Server.id)
|
|
||||||
host = ''
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_url(self):
|
|
||||||
"""return the full API url (with proto://host...)"""
|
|
||||||
return url_path_join(self.server.url, 'api')
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.server:
|
|
||||||
return "<%s %s:%s>" % (
|
|
||||||
self.__class__.__name__, self.server.ip, self.server.port,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return "<%s [unconfigured]>" % self.__class__.__name__
|
|
||||||
|
|
||||||
|
|
||||||
# user:group many:many mapping table
|
# user:group many:many mapping table
|
||||||
user_group_map = Table('user_group_map', Base.metadata,
|
user_group_map = Table('user_group_map', Base.metadata,
|
||||||
|
@@ -496,8 +496,8 @@ class Spawner(LoggingConfigurable):
|
|||||||
args = [
|
args = [
|
||||||
'--user="%s"' % self.user.name,
|
'--user="%s"' % self.user.name,
|
||||||
'--base-url="%s"' % self.user.server.base_url,
|
'--base-url="%s"' % self.user.server.base_url,
|
||||||
'--hub-host="%s"' % self.hub.host,
|
'--hub-host="%s"' % self.hub.public_host,
|
||||||
'--hub-prefix="%s"' % self.hub.server.base_url,
|
'--hub-prefix="%s"' % self.hub.base_url,
|
||||||
'--hub-api-url="%s"' % self.hub.api_url,
|
'--hub-api-url="%s"' % self.hub.api_url,
|
||||||
]
|
]
|
||||||
if self.ip:
|
if self.ip:
|
||||||
|
@@ -165,7 +165,7 @@ class MockHub(JupyterHub):
|
|||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
yield super(MockHub, self).start()
|
yield super(MockHub, self).start()
|
||||||
yield self.hub.server.wait_up(http=True)
|
yield self.hub.wait_up(http=True)
|
||||||
self.io_loop.add_callback(evt.set)
|
self.io_loop.add_callback(evt.set)
|
||||||
|
|
||||||
def _start():
|
def _start():
|
||||||
|
@@ -83,7 +83,7 @@ def auth_header(db, name):
|
|||||||
@check_db_locks
|
@check_db_locks
|
||||||
def api_request(app, *api_path, **kwargs):
|
def api_request(app, *api_path, **kwargs):
|
||||||
"""Make an API request"""
|
"""Make an API request"""
|
||||||
base_url = app.hub.server.url
|
base_url = app.hub.url
|
||||||
headers = kwargs.setdefault('headers', {})
|
headers = kwargs.setdefault('headers', {})
|
||||||
|
|
||||||
if 'Authorization' not in headers:
|
if 'Authorization' not in headers:
|
||||||
@@ -94,7 +94,7 @@ def api_request(app, *api_path, **kwargs):
|
|||||||
f = getattr(requests, method)
|
f = getattr(requests, method)
|
||||||
resp = f(url, **kwargs)
|
resp = f(url, **kwargs)
|
||||||
assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy']
|
assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy']
|
||||||
assert ujoin(app.hub.server.base_url, "security/csp-report") in resp.headers['Content-Security-Policy']
|
assert ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy']
|
||||||
assert 'http' not in resp.headers['Content-Security-Policy']
|
assert 'http' not in resp.headers['Content-Security-Policy']
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ def test_auth_api(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_referer_check(app, io_loop):
|
def test_referer_check(app, io_loop):
|
||||||
url = ujoin(public_host(app), app.hub.server.base_url)
|
url = ujoin(public_host(app), app.hub.base_url)
|
||||||
host = urlparse(url).netloc
|
host = urlparse(url).netloc
|
||||||
user = find_user(app.db, 'admin')
|
user = find_user(app.db, 'admin')
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -779,7 +779,7 @@ def test_get_service(app, mockservice_url):
|
|||||||
|
|
||||||
|
|
||||||
def test_root_api(app):
|
def test_root_api(app):
|
||||||
base_url = app.hub.server.url
|
base_url = app.hub.url
|
||||||
url = ujoin(base_url, 'api')
|
url = ujoin(base_url, 'api')
|
||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@@ -58,8 +58,8 @@ def test_hub(db):
|
|||||||
)
|
)
|
||||||
db.add(hub)
|
db.add(hub)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert hub.server.ip == '1.2.3.4'
|
assert hub.ip == '1.2.3.4'
|
||||||
assert hub.server.port == 1234
|
assert hub.port == 1234
|
||||||
assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api'
|
assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api'
|
||||||
|
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ from .test_api import api_request
|
|||||||
|
|
||||||
def get_page(path, app, hub=True, **kw):
|
def get_page(path, app, hub=True, **kw):
|
||||||
if hub:
|
if hub:
|
||||||
prefix = app.hub.server.base_url
|
prefix = app.hub.base_url
|
||||||
else:
|
else:
|
||||||
prefix = app.base_url
|
prefix = app.base_url
|
||||||
base_url = ujoin(public_host(app), prefix)
|
base_url = ujoin(public_host(app), prefix)
|
||||||
@@ -24,11 +24,11 @@ def get_page(path, app, hub=True, **kw):
|
|||||||
return requests.get(ujoin(base_url, path), **kw)
|
return requests.get(ujoin(base_url, path), **kw)
|
||||||
|
|
||||||
def test_root_no_auth(app, io_loop):
|
def test_root_no_auth(app, io_loop):
|
||||||
print(app.hub.server.is_up())
|
print(app.hub.is_up())
|
||||||
routes = io_loop.run_sync(app.proxy.get_routes)
|
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||||
print(routes)
|
print(routes)
|
||||||
print(app.hub.server)
|
print(app.hub.server)
|
||||||
url = ujoin(public_host(app), app.hub.server.base_url)
|
url = ujoin(public_host(app), app.hub.base_url)
|
||||||
print(url)
|
print(url)
|
||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -123,7 +123,7 @@ def test_spawn_page(app):
|
|||||||
|
|
||||||
def test_spawn_form(app, io_loop):
|
def test_spawn_form(app, io_loop):
|
||||||
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
||||||
base_url = ujoin(public_host(app), app.hub.server.base_url)
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
cookies = app.login_user('jones')
|
cookies = app.login_user('jones')
|
||||||
orm_u = orm.User.find(app.db, 'jones')
|
orm_u = orm.User.find(app.db, 'jones')
|
||||||
u = app.users[orm_u]
|
u = app.users[orm_u]
|
||||||
@@ -145,7 +145,7 @@ def test_spawn_form(app, io_loop):
|
|||||||
|
|
||||||
def test_spawn_form_with_file(app, io_loop):
|
def test_spawn_form_with_file(app, io_loop):
|
||||||
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
|
||||||
base_url = ujoin(public_host(app), app.hub.server.base_url)
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
cookies = app.login_user('jones')
|
cookies = app.login_user('jones')
|
||||||
orm_u = orm.User.find(app.db, 'jones')
|
orm_u = orm.User.find(app.db, 'jones')
|
||||||
u = app.users[orm_u]
|
u = app.users[orm_u]
|
||||||
@@ -181,7 +181,7 @@ def test_user_redirect(app):
|
|||||||
assert path == ujoin(app.base_url, '/hub/login')
|
assert path == ujoin(app.base_url, '/hub/login')
|
||||||
query = urlparse(r.url).query
|
query = urlparse(r.url).query
|
||||||
assert query == urlencode({
|
assert query == urlencode({
|
||||||
'next': ujoin(app.hub.server.base_url, '/user-redirect/tree/top/')
|
'next': ujoin(app.hub.base_url, '/user-redirect/tree/top/')
|
||||||
})
|
})
|
||||||
|
|
||||||
r = get_page('/user-redirect/notebooks/test.ipynb', app, cookies=cookies)
|
r = get_page('/user-redirect/notebooks/test.ipynb', app, cookies=cookies)
|
||||||
@@ -320,7 +320,7 @@ def test_login_no_whitelist_adds_user(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_static_files(app):
|
def test_static_files(app):
|
||||||
base_url = ujoin(public_host(app), app.hub.server.base_url)
|
base_url = ujoin(public_host(app), app.hub.base_url)
|
||||||
r = requests.get(ujoin(base_url, 'logo'))
|
r = requests.get(ujoin(base_url, 'logo'))
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.headers['content-type'] == 'image/png'
|
assert r.headers['content-type'] == 'image/png'
|
||||||
|
@@ -25,7 +25,7 @@ def external_service(app, name='mockservice'):
|
|||||||
env = {
|
env = {
|
||||||
'JUPYTERHUB_API_TOKEN': hexlify(os.urandom(5)),
|
'JUPYTERHUB_API_TOKEN': hexlify(os.urandom(5)),
|
||||||
'JUPYTERHUB_SERVICE_NAME': name,
|
'JUPYTERHUB_SERVICE_NAME': name,
|
||||||
'JUPYTERHUB_API_URL': url_path_join(app.hub.server.url, 'api/'),
|
'JUPYTERHUB_API_URL': url_path_join(app.hub.url, 'api/'),
|
||||||
'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:%i' % random_port(),
|
'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:%i' % random_port(),
|
||||||
}
|
}
|
||||||
proc = Popen(mockservice_cmd, env=env)
|
proc = Popen(mockservice_cmd, env=env)
|
||||||
|
@@ -119,8 +119,7 @@ class User(HasTraits):
|
|||||||
hub = self.db.query(orm.Hub).first()
|
hub = self.db.query(orm.Hub).first()
|
||||||
|
|
||||||
self.allow_named_servers = self.settings.get('allow_named_servers', False)
|
self.allow_named_servers = self.settings.get('allow_named_servers', False)
|
||||||
|
self.cookie_name = '%s-%s' % (hub.cookie_name, quote(self.name, safe=''))
|
||||||
self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe=''))
|
|
||||||
self.base_url = url_path_join(
|
self.base_url = url_path_join(
|
||||||
self.settings.get('base_url', '/'), 'user', self.escaped_name)
|
self.settings.get('base_url', '/'), 'user', self.escaped_name)
|
||||||
|
|
||||||
|
@@ -37,6 +37,8 @@ def can_connect(ip, port):
|
|||||||
|
|
||||||
Return True if we can connect, False otherwise.
|
Return True if we can connect, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
if ip in {'', '0.0.0.0'}:
|
||||||
|
ip = '127.0.0.1'
|
||||||
try:
|
try:
|
||||||
socket.create_connection((ip, port))
|
socket.create_connection((ip, port))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
@@ -50,6 +52,8 @@ def can_connect(ip, port):
|
|||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def wait_for_server(ip, port, timeout=10):
|
def wait_for_server(ip, port, timeout=10):
|
||||||
"""Wait for any server to show up at ip:port."""
|
"""Wait for any server to show up at ip:port."""
|
||||||
|
if ip in {'', '0.0.0.0'}:
|
||||||
|
ip = '127.0.0.1'
|
||||||
loop = ioloop.IOLoop.current()
|
loop = ioloop.IOLoop.current()
|
||||||
tic = loop.time()
|
tic = loop.time()
|
||||||
while loop.time() - tic < timeout:
|
while loop.time() - tic < timeout:
|
||||||
|
Reference in New Issue
Block a user