diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index a07e71bc..72c21702 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -32,7 +32,7 @@ class APIHandler(BaseHandler): self.log.warning("Blocking API request with no referer") 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] if not (referer_path + '/').startswith(host_path): self.log.warning("Blocking Cross Origin API request. Referer: %s, Host: %s", diff --git a/jupyterhub/app.py b/jupyterhub/app.py index b6b89293..05e82904 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -62,6 +62,7 @@ from .utils import ( # classes for config from .auth import Authenticator, PAMAuthenticator from .spawner import Spawner, LocalProcessSpawner +from .objects import Server, Hub # For faking stats from .emptyclass import EmptyClass @@ -805,36 +806,6 @@ class JupyterHub(Application): self._local.db = scoped_session(self.session_factory)() 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): """Create the database connection""" self.log.debug("Connecting to db: %s", self.db_url) @@ -861,28 +832,13 @@ class JupyterHub(Application): def init_hub(self): """Load the Hub config into the database""" - self.hub = self.db.query(orm.Hub).first() - if self.hub is None: - self.hub = orm.Hub( - server=orm.Server( - ip=self.hub_ip, - port=self.hub_port, - 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() + self.hub = Hub( + ip=self.hub_ip, + port=self.hub_port, + base_url=self.hub_prefix, + cookie_name='jupyter-hub-token', + public_host=self.subdomain_host, + ) @gen.coroutine def init_users(self): @@ -1231,8 +1187,8 @@ class JupyterHub(Application): '--port', str(self.proxy.public_server.port), '--api-ip', self.proxy.api_server.ip, '--api-port', str(self.proxy.api_server.port), - '--default-target', self.hub.server.host, - '--error-target', url_path_join(self.hub.server.url, 'error'), + '--default-target', self.hub.host, + '--error-target', url_path_join(self.hub.url, 'error'), ] if self.subdomain_host: cmd.append('--host-routing') @@ -1299,7 +1255,7 @@ class JupyterHub(Application): def init_tornado_settings(self): """Set up the tornado settings dict.""" - base_url = self.hub.server.base_url + base_url = self.hub.base_url jinja_options = dict( autoescape=True, ) @@ -1337,7 +1293,7 @@ class JupyterHub(Application): login_url=login_url, logout_url=logout_url, 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, template_path=self.template_paths, jinja2_env=jinja_env, @@ -1530,10 +1486,10 @@ class JupyterHub(Application): try: self.http_server.listen(self.hub_port, address=self.hub_ip) 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 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 try: diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 97c3255a..6c89fcfd 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -103,7 +103,7 @@ class BaseHandler(RequestHandler): @property def csp_report_uri(self): 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 @@ -184,7 +184,7 @@ class BaseHandler(RequestHandler): max_age_days=self.cookie_max_age_days, ) 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 self.get_cookie(cookie_name): @@ -208,7 +208,7 @@ class BaseHandler(RequestHandler): def get_current_user_cookie(self): """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): """get current username""" @@ -247,7 +247,7 @@ class BaseHandler(RequestHandler): kwargs['domain'] = self.domain if user and user.server: 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')) def _set_user_cookie(self, user, server): @@ -436,7 +436,7 @@ class BaseHandler(RequestHandler): def template_namespace(self): user = self.get_current_user() return dict( - base_url=self.hub.server.base_url, + base_url=self.hub.base_url, prefix=self.base_url, user=user, login_url=self.settings['login_url'], @@ -502,7 +502,7 @@ class PrefixRedirectHandler(BaseHandler): else: path = self.request.path self.redirect(url_path_join( - self.hub.server.base_url, path, + self.hub.base_url, path, ), permanent=False) @@ -528,7 +528,7 @@ class UserSpawnHandler(BaseHandler): port = host_info.port if not port: 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(""" Detected possible direct connection to Hub's private ip: %s, bypassing proxy. This will result in a redirect loop. @@ -548,14 +548,14 @@ class UserSpawnHandler(BaseHandler): status = yield current_user.spawner.poll() if status is not None: 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})) return else: yield self.spawn_single_user(current_user) # set login cookie anew 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) if self.subdomain_host: target = current_user.host + target diff --git a/jupyterhub/handlers/login.py b/jupyterhub/handlers/login.py index b8e7b558..1bcdf256 100644 --- a/jupyterhub/handlers/login.py +++ b/jupyterhub/handlers/login.py @@ -54,7 +54,7 @@ class LoginHandler(BaseHandler): if user.running: next_url = user.url else: - next_url = self.hub.server.base_url + next_url = self.hub.base_url # set new login cookie # because single-user cookie may have been cleared or incorrect self.set_login_cookie(self.get_current_user()) @@ -98,7 +98,7 @@ class LoginHandler(BaseHandler): next_url = self.get_argument('next', default='') if not next_url.startswith('/'): 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.log.info("User logged in: %s", username) else: diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 3e857943..dbd3a434 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -37,7 +37,7 @@ class RootHandler(BaseHandler): # The next request will be handled by UserSpawnHandler, # ultimately redirecting to the logged-in user's server. 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.request.uri, next_url, ) @@ -50,7 +50,7 @@ class RootHandler(BaseHandler): self.log.debug("User is running: %s", url) self.set_login_cookie(user) # set cookie 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) else: url = self.settings['login_url'] @@ -215,7 +215,7 @@ class ProxyErrorHandler(BaseHandler): status_message = responses.get(status_code, 'Unknown HTTP Error') # 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 = '' if status_code == 503: message_html = ' '.join([ diff --git a/jupyterhub/objects.py b/jupyterhub/objects.py new file mode 100644 index 00000000..cf2201ba --- /dev/null +++ b/jupyterhub/objects.py @@ -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, + ) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 1207d260..f92d157b 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -79,240 +79,6 @@ class Server(Base): def __repr__(self): return "" % (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_map = Table('user_group_map', Base.metadata, diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index ce184de6..3349edbf 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -496,8 +496,8 @@ class Spawner(LoggingConfigurable): args = [ '--user="%s"' % self.user.name, '--base-url="%s"' % self.user.server.base_url, - '--hub-host="%s"' % self.hub.host, - '--hub-prefix="%s"' % self.hub.server.base_url, + '--hub-host="%s"' % self.hub.public_host, + '--hub-prefix="%s"' % self.hub.base_url, '--hub-api-url="%s"' % self.hub.api_url, ] if self.ip: diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 9045357a..0bc4712e 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -165,7 +165,7 @@ class MockHub(JupyterHub): self.db.add(user) self.db.commit() 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) def _start(): diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 512f81d6..0f0d1a64 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -83,7 +83,7 @@ def auth_header(db, name): @check_db_locks def api_request(app, *api_path, **kwargs): """Make an API request""" - base_url = app.hub.server.url + base_url = app.hub.url headers = kwargs.setdefault('headers', {}) if 'Authorization' not in headers: @@ -94,7 +94,7 @@ def api_request(app, *api_path, **kwargs): f = getattr(requests, method) resp = f(url, **kwargs) 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'] return resp @@ -132,7 +132,7 @@ def test_auth_api(app): 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 user = find_user(app.db, 'admin') if user is None: @@ -779,7 +779,7 @@ def test_get_service(app, mockservice_url): def test_root_api(app): - base_url = app.hub.server.url + base_url = app.hub.url url = ujoin(base_url, 'api') r = requests.get(url) r.raise_for_status() diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 531e3523..7d7eb839 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -58,8 +58,8 @@ def test_hub(db): ) db.add(hub) db.commit() - assert hub.server.ip == '1.2.3.4' - assert hub.server.port == 1234 + assert hub.ip == '1.2.3.4' + assert hub.port == 1234 assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api' diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 0f0d3b3c..abcf0e05 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -16,7 +16,7 @@ from .test_api import api_request def get_page(path, app, hub=True, **kw): if hub: - prefix = app.hub.server.base_url + prefix = app.hub.base_url else: prefix = app.base_url 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) 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) print(routes) 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) r = requests.get(url) r.raise_for_status() @@ -123,7 +123,7 @@ def test_spawn_page(app): def test_spawn_form(app, io_loop): 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') orm_u = orm.User.find(app.db, 'jones') 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): 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') orm_u = orm.User.find(app.db, 'jones') u = app.users[orm_u] @@ -181,7 +181,7 @@ def test_user_redirect(app): assert path == ujoin(app.base_url, '/hub/login') query = urlparse(r.url).query 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) @@ -320,7 +320,7 @@ def test_login_no_whitelist_adds_user(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.raise_for_status() assert r.headers['content-type'] == 'image/png' diff --git a/jupyterhub/tests/test_services.py b/jupyterhub/tests/test_services.py index 337f2cc3..6b8602d5 100644 --- a/jupyterhub/tests/test_services.py +++ b/jupyterhub/tests/test_services.py @@ -25,7 +25,7 @@ def external_service(app, name='mockservice'): env = { 'JUPYTERHUB_API_TOKEN': hexlify(os.urandom(5)), '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(), } proc = Popen(mockservice_cmd, env=env) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index c0727d31..5a9a78d6 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -117,10 +117,9 @@ class User(HasTraits): super().__init__(**kwargs) hub = self.db.query(orm.Hub).first() - - self.allow_named_servers = self.settings.get('allow_named_servers', False) - self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe='')) + self.allow_named_servers = self.settings.get('allow_named_servers', False) + self.cookie_name = '%s-%s' % (hub.cookie_name, quote(self.name, safe='')) self.base_url = url_path_join( self.settings.get('base_url', '/'), 'user', self.escaped_name) diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 82b58151..1c68bdfb 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -37,6 +37,8 @@ def can_connect(ip, port): Return True if we can connect, False otherwise. """ + if ip in {'', '0.0.0.0'}: + ip = '127.0.0.1' try: socket.create_connection((ip, port)) except socket.error as e: @@ -50,6 +52,8 @@ def can_connect(ip, port): @gen.coroutine def wait_for_server(ip, port, timeout=10): """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() tic = loop.time() while loop.time() - tic < timeout: