mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 21:13:01 +00:00
Merge pull request #436 from minrk/subdomains
allow running single-user servers on subdomains
This commit is contained in:
@@ -15,3 +15,7 @@ script:
|
||||
- py.test --cov jupyterhub jupyterhub/tests -v
|
||||
after_success:
|
||||
- codecov
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.5
|
||||
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://127.0.0.1.xip.io:8000
|
||||
|
@@ -86,7 +86,7 @@ class APIHandler(BaseHandler):
|
||||
model = {
|
||||
'name': user.name,
|
||||
'admin': user.admin,
|
||||
'server': user.server.base_url if user.running else None,
|
||||
'server': user.url if user.running else None,
|
||||
'pending': None,
|
||||
'last_activity': user.last_activity.isoformat(),
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ class ProxyAPIHandler(APIHandler):
|
||||
@gen.coroutine
|
||||
def post(self):
|
||||
"""POST checks the proxy to ensure"""
|
||||
yield self.proxy.check_routes()
|
||||
yield self.proxy.check_routes(self.users)
|
||||
|
||||
|
||||
@admin_only
|
||||
@@ -59,7 +59,7 @@ class ProxyAPIHandler(APIHandler):
|
||||
self.proxy.auth_token = model['auth_token']
|
||||
self.db.commit()
|
||||
self.log.info("Updated proxy at %s", server.bind_url)
|
||||
yield self.proxy.check_routes()
|
||||
yield self.proxy.check_routes(self.users)
|
||||
|
||||
|
||||
|
||||
|
@@ -16,6 +16,7 @@ from datetime import datetime
|
||||
from distutils.version import LooseVersion as V
|
||||
from getpass import getuser
|
||||
from subprocess import Popen
|
||||
from urllib.parse import urlparse
|
||||
|
||||
if sys.version_info[:2] < (3,3):
|
||||
raise ValueError("Python < 3.3 not supported: %s" % sys.version)
|
||||
@@ -230,8 +231,21 @@ class JupyterHub(Application):
|
||||
"""
|
||||
)
|
||||
ip = Unicode('', config=True,
|
||||
help="The public facing ip of the proxy"
|
||||
help="The public facing ip of the whole application (the proxy)"
|
||||
)
|
||||
|
||||
subdomain_host = Unicode('', config=True,
|
||||
help="""The public-facing host (domain[:port]) on which the Hub will run.
|
||||
|
||||
Only used when subdomains are involved.
|
||||
"""
|
||||
)
|
||||
def _subdomain_host_changed(self, name, old, new):
|
||||
if new and '://' not in new:
|
||||
# host should include '://'
|
||||
# if not specified, assume https: You have to be really explicit about HTTP!
|
||||
self.subdomain_host = 'https://' + new
|
||||
|
||||
port = Integer(8000, config=True,
|
||||
help="The public facing port of the proxy"
|
||||
)
|
||||
@@ -248,6 +262,18 @@ class JupyterHub(Application):
|
||||
help="Supply extra arguments that will be passed to Jinja environment."
|
||||
)
|
||||
|
||||
use_subdomains = Bool(False, config=True,
|
||||
help="""Run single-user servers on subdomains.
|
||||
|
||||
Provides additional cross-site protections for client-side js.
|
||||
|
||||
Requires <username>.hub.domain.tld to resolve to the same host as hub.domain.tld.
|
||||
|
||||
In general, this is most easily achieved with wildcard DNS.
|
||||
|
||||
When using SSL (i.e. always) this also requires a wildcard cert.
|
||||
""")
|
||||
|
||||
proxy_cmd = Command('configurable-http-proxy', config=True,
|
||||
help="""The command to start the http proxy.
|
||||
|
||||
@@ -288,7 +314,6 @@ class JupyterHub(Application):
|
||||
hub_ip = Unicode('127.0.0.1', config=True,
|
||||
help="The ip for this process"
|
||||
)
|
||||
|
||||
hub_prefix = URLPrefix('/hub/', config=True,
|
||||
help="The prefix for the hub server. Must not be '/'"
|
||||
)
|
||||
@@ -574,11 +599,15 @@ class JupyterHub(Application):
|
||||
q = self.db.query(orm.Hub)
|
||||
assert q.count() <= 1
|
||||
self._local.hub = q.first()
|
||||
if self.use_subdomains 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.use_subdomains:
|
||||
hub.host = self.subdomain_host
|
||||
|
||||
@property
|
||||
def proxy(self):
|
||||
@@ -631,6 +660,10 @@ class JupyterHub(Application):
|
||||
server.ip = self.hub_ip
|
||||
server.port = self.hub_port
|
||||
server.base_url = self.hub_prefix
|
||||
if self.use_subdomains:
|
||||
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()
|
||||
|
||||
@@ -809,6 +842,8 @@ class JupyterHub(Application):
|
||||
'--api-port', str(self.proxy.api_server.port),
|
||||
'--default-target', self.hub.server.host,
|
||||
]
|
||||
if self.use_subdomains:
|
||||
cmd.append('--host-routing')
|
||||
if self.debug_proxy:
|
||||
cmd.extend(['--log-level', 'debug'])
|
||||
if self.ssl_key:
|
||||
@@ -867,7 +902,7 @@ class JupyterHub(Application):
|
||||
)
|
||||
yield self.start_proxy()
|
||||
self.log.info("Setting up routes on new proxy")
|
||||
yield self.proxy.add_all_users()
|
||||
yield self.proxy.add_all_users(self.users)
|
||||
self.log.info("New proxy back up, and good to go")
|
||||
|
||||
def init_tornado_settings(self):
|
||||
@@ -893,6 +928,8 @@ class JupyterHub(Application):
|
||||
else:
|
||||
version_hash=datetime.now().strftime("%Y%m%d%H%M%S"),
|
||||
|
||||
subdomain_host = self.subdomain_host
|
||||
domain = urlparse(subdomain_host).hostname
|
||||
settings = dict(
|
||||
log_function=log_request,
|
||||
config=self.config,
|
||||
@@ -915,6 +952,9 @@ class JupyterHub(Application):
|
||||
template_path=self.template_paths,
|
||||
jinja2_env=jinja_env,
|
||||
version_hash=version_hash,
|
||||
use_subdomains=self.use_subdomains,
|
||||
subdomain_host=subdomain_host,
|
||||
domain=domain,
|
||||
)
|
||||
# allow configured settings to have priority
|
||||
settings.update(self.tornado_settings)
|
||||
@@ -1051,7 +1091,7 @@ class JupyterHub(Application):
|
||||
user.last_activity = max(user.last_activity, dt)
|
||||
|
||||
self.db.commit()
|
||||
yield self.proxy.check_routes(routes)
|
||||
yield self.proxy.check_routes(routes, self.users)
|
||||
|
||||
@gen.coroutine
|
||||
def start(self):
|
||||
@@ -1086,7 +1126,7 @@ class JupyterHub(Application):
|
||||
self.exit(1)
|
||||
return
|
||||
|
||||
loop.add_callback(self.proxy.add_all_users)
|
||||
loop.add_callback(self.proxy.add_all_users, self.users)
|
||||
|
||||
if self.proxy_process:
|
||||
# only check / restart the proxy if we started it in the first place.
|
||||
|
@@ -51,6 +51,14 @@ class BaseHandler(RequestHandler):
|
||||
def version_hash(self):
|
||||
return self.settings.get('version_hash', '')
|
||||
|
||||
@property
|
||||
def use_subdomains(self):
|
||||
return self.settings.get('use_subdomains', False)
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
return self.settings['domain']
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self.settings['db']
|
||||
@@ -199,42 +207,44 @@ class BaseHandler(RequestHandler):
|
||||
user = self.get_current_user()
|
||||
else:
|
||||
user = self.find_user(name)
|
||||
kwargs = {}
|
||||
if self.use_subdomains:
|
||||
kwargs['domain'] = self.domain
|
||||
if user and user.server:
|
||||
self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
|
||||
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
|
||||
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)
|
||||
|
||||
def _set_user_cookie(self, user, server):
|
||||
# tornado <4.2 have a bug that consider secure==True as soon as
|
||||
# 'secure' kwarg is passed to set_secure_cookie
|
||||
if self.request.protocol == 'https':
|
||||
kwargs = {'secure': True}
|
||||
else:
|
||||
kwargs = {}
|
||||
if self.use_subdomains:
|
||||
kwargs['domain'] = self.domain
|
||||
self.log.debug("Setting cookie for %s: %s, %s", user.name, server.cookie_name, kwargs)
|
||||
self.set_secure_cookie(
|
||||
server.cookie_name,
|
||||
user.cookie_id,
|
||||
path=server.base_url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def set_server_cookie(self, user):
|
||||
"""set the login cookie for the single-user server"""
|
||||
# tornado <4.2 have a bug that consider secure==True as soon as
|
||||
# 'secure' kwarg is passed to set_secure_cookie
|
||||
if self.request.protocol == 'https':
|
||||
kwargs = {'secure':True}
|
||||
else:
|
||||
kwargs = {}
|
||||
self.set_secure_cookie(
|
||||
user.server.cookie_name,
|
||||
user.cookie_id,
|
||||
path=user.server.base_url,
|
||||
**kwargs
|
||||
)
|
||||
self._set_user_cookie(user, user.server)
|
||||
|
||||
def set_hub_cookie(self, user):
|
||||
"""set the login cookie for the Hub"""
|
||||
# tornado <4.2 have a bug that consider secure==True as soon as
|
||||
# 'secure' kwarg is passed to set_secure_cookie
|
||||
if self.request.protocol == 'https':
|
||||
kwargs = {'secure':True}
|
||||
else:
|
||||
kwargs = {}
|
||||
self.set_secure_cookie(
|
||||
self.hub.server.cookie_name,
|
||||
user.cookie_id,
|
||||
path=self.hub.server.base_url,
|
||||
**kwargs
|
||||
)
|
||||
self._set_user_cookie(user, self.hub.server)
|
||||
|
||||
def set_login_cookie(self, user):
|
||||
"""Set login cookies for the Hub and single-user server."""
|
||||
if self.use_subdomains and not self.request.host.startswith(self.domain):
|
||||
self.log.warning(
|
||||
"Possibly setting cookie on wrong domain: %s != %s",
|
||||
self.request.host, self.domain)
|
||||
# create and set a new cookie token for the single-user server
|
||||
if user.server:
|
||||
self.set_server_cookie(user)
|
||||
@@ -472,6 +482,8 @@ class UserSpawnHandler(BaseHandler):
|
||||
self.set_login_cookie(current_user)
|
||||
without_prefix = self.request.uri[len(self.hub.server.base_url):]
|
||||
target = url_path_join(self.base_url, without_prefix)
|
||||
if self.use_subdomains:
|
||||
target = current_user.host + target
|
||||
self.redirect(target)
|
||||
else:
|
||||
# not logged in to the right user,
|
||||
|
@@ -25,7 +25,7 @@ class RootHandler(BaseHandler):
|
||||
user = self.get_current_user()
|
||||
if user:
|
||||
if user.running:
|
||||
url = user.server.base_url
|
||||
url = user.url
|
||||
self.log.debug("User is running: %s", url)
|
||||
else:
|
||||
url = url_path_join(self.hub.server.base_url, 'home')
|
||||
@@ -67,7 +67,7 @@ class SpawnHandler(BaseHandler):
|
||||
"""GET renders form for spawning with user-specified options"""
|
||||
user = self.get_current_user()
|
||||
if user.running:
|
||||
url = user.server.base_url
|
||||
url = user.url
|
||||
self.log.debug("User is running: %s", url)
|
||||
self.redirect(url)
|
||||
return
|
||||
@@ -84,7 +84,7 @@ class SpawnHandler(BaseHandler):
|
||||
"""POST spawns with user-specified options"""
|
||||
user = self.get_current_user()
|
||||
if user.running:
|
||||
url = user.server.base_url
|
||||
url = user.url
|
||||
self.log.warning("User is already running: %s", url)
|
||||
self.redirect(url)
|
||||
return
|
||||
@@ -101,7 +101,7 @@ class SpawnHandler(BaseHandler):
|
||||
self.finish(self._render_form(str(e)))
|
||||
return
|
||||
self.set_login_cookie(user)
|
||||
url = user.server.base_url
|
||||
url = user.url
|
||||
self.redirect(url)
|
||||
|
||||
class AdminHandler(BaseHandler):
|
||||
|
@@ -176,10 +176,10 @@ class Proxy(Base):
|
||||
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.server.base_url, user.server.host,
|
||||
user.name, user.proxy_path, user.server.host,
|
||||
)
|
||||
|
||||
yield self.api_request(user.server.base_url,
|
||||
yield self.api_request(user.proxy_path,
|
||||
method='POST',
|
||||
body=dict(
|
||||
target=user.server.host,
|
||||
@@ -192,26 +192,11 @@ class Proxy(Base):
|
||||
def delete_user(self, user, client=None):
|
||||
"""Remove a user's server to the proxy table."""
|
||||
self.log.info("Removing user %s from proxy", user.name)
|
||||
yield self.api_request(user.server.base_url,
|
||||
yield self.api_request(user.proxy_path,
|
||||
method='DELETE',
|
||||
client=client,
|
||||
)
|
||||
|
||||
@gen.coroutine
|
||||
def add_all_users(self):
|
||||
"""Update the proxy table from the database.
|
||||
|
||||
Used when loading up a new proxy.
|
||||
"""
|
||||
db = inspect(self).session
|
||||
futures = []
|
||||
for user in db.query(User):
|
||||
if (user.server):
|
||||
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"""
|
||||
@@ -219,15 +204,32 @@ class Proxy(Base):
|
||||
return json.loads(resp.body.decode('utf8', 'replace'))
|
||||
|
||||
@gen.coroutine
|
||||
def check_routes(self, routes=None):
|
||||
"""Check that all users are properly"""
|
||||
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.server):
|
||||
futures.append(self.add_user(user))
|
||||
# wait after submitting them all
|
||||
for f in futures:
|
||||
yield f
|
||||
|
||||
@gen.coroutine
|
||||
def check_routes(self, user_dict, routes=None):
|
||||
"""Check that all users are properly routed on the proxy"""
|
||||
if not routes:
|
||||
routes = yield self.get_routes()
|
||||
|
||||
have_routes = { r['user'] for r in routes.values() if 'user' in r }
|
||||
futures = []
|
||||
db = inspect(self).session
|
||||
for user in db.query(User).filter(User.server != None):
|
||||
for orm_user in db.query(User).filter(User.server != None):
|
||||
user = user_dict[orm_user]
|
||||
if user.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
|
||||
@@ -253,6 +255,7 @@ class Hub(Base):
|
||||
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):
|
||||
|
@@ -199,6 +199,7 @@ class Spawner(LoggingConfigurable):
|
||||
'--port=%i' % self.user.server.port,
|
||||
'--cookie-name=%s' % self.user.server.cookie_name,
|
||||
'--base-url=%s' % self.user.server.base_url,
|
||||
'--hub-host=%s' % self.hub.host,
|
||||
'--hub-prefix=%s' % self.hub.server.base_url,
|
||||
'--hub-api-url=%s' % self.hub.api_url,
|
||||
]
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""mock utilities for testing"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from tempfile import NamedTemporaryFile
|
||||
import threading
|
||||
|
||||
@@ -13,10 +13,11 @@ from tornado import gen
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..app import JupyterHub
|
||||
from ..auth import PAMAuthenticator
|
||||
from .. import orm
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..utils import url_path_join
|
||||
|
||||
from pamela import PAMError
|
||||
|
||||
@@ -110,6 +111,12 @@ class MockHub(JupyterHub):
|
||||
db_file = None
|
||||
confirm_no_ssl = True
|
||||
|
||||
def _subdomain_host_default(self):
|
||||
return os.environ.get('JUPYTERHUB_TEST_SUBDOMAIN_HOST', '')
|
||||
|
||||
def _use_subdomains_default(self):
|
||||
return bool(self.subdomain_host)
|
||||
|
||||
def _ip_default(self):
|
||||
return '127.0.0.1'
|
||||
|
||||
@@ -161,7 +168,8 @@ class MockHub(JupyterHub):
|
||||
self.db_file.close()
|
||||
|
||||
def login_user(self, name):
|
||||
r = requests.post(self.proxy.public_server.url + 'hub/login',
|
||||
base_url = public_url(self)
|
||||
r = requests.post(base_url + 'hub/login',
|
||||
data={
|
||||
'username': name,
|
||||
'password': name,
|
||||
@@ -171,3 +179,21 @@ class MockHub(JupyterHub):
|
||||
assert r.cookies
|
||||
return r.cookies
|
||||
|
||||
|
||||
def public_host(app):
|
||||
if app.use_subdomains:
|
||||
return app.subdomain_host
|
||||
else:
|
||||
return app.proxy.public_server.host
|
||||
|
||||
|
||||
def public_url(app):
|
||||
return public_host(app) + app.proxy.public_server.base_url
|
||||
|
||||
|
||||
def user_url(user, app):
|
||||
if app.use_subdomains:
|
||||
host = user.host
|
||||
else:
|
||||
host = public_host(app)
|
||||
return host + user.server.base_url
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from queue import Queue
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -14,6 +13,7 @@ from .. import orm
|
||||
from ..user import User
|
||||
from ..utils import url_path_join as ujoin
|
||||
from . import mocking
|
||||
from .mocking import public_url, user_url
|
||||
|
||||
|
||||
def check_db_locks(func):
|
||||
@@ -105,7 +105,7 @@ def test_auth_api(app):
|
||||
|
||||
|
||||
def test_referer_check(app, io_loop):
|
||||
url = app.hub.server.url
|
||||
url = ujoin(public_url(app), app.hub.server.base_url)
|
||||
host = urlparse(url).netloc
|
||||
user = find_user(app.db, 'admin')
|
||||
if user is None:
|
||||
@@ -352,15 +352,19 @@ def test_spawn(app, io_loop):
|
||||
assert status is None
|
||||
|
||||
assert user.server.base_url == '/user/%s' % name
|
||||
r = requests.get(ujoin(app.proxy.public_server.url, user.server.base_url))
|
||||
url = user_url(user, app)
|
||||
print(url)
|
||||
r = requests.get(url)
|
||||
assert r.status_code == 200
|
||||
assert r.text == user.server.base_url
|
||||
|
||||
r = requests.get(ujoin(app.proxy.public_server.url, user.server.base_url, 'args'))
|
||||
r = requests.get(ujoin(url, 'args'))
|
||||
assert r.status_code == 200
|
||||
argv = r.json()
|
||||
for expected in ['--user=%s' % name, '--base-url=%s' % user.server.base_url]:
|
||||
assert expected in argv
|
||||
if app.use_subdomains:
|
||||
assert '--hub-host=%s' % app.subdomain_host in argv
|
||||
|
||||
r = api_request(app, 'users', name, 'server', method='delete')
|
||||
assert r.status_code == 204
|
||||
|
@@ -8,12 +8,11 @@ from ..utils import url_path_join as ujoin
|
||||
from .. import orm
|
||||
|
||||
import mock
|
||||
from .mocking import FormSpawner
|
||||
from .mocking import FormSpawner, public_url, public_host, user_url
|
||||
from .test_api import api_request
|
||||
|
||||
|
||||
def get_page(path, app, **kw):
|
||||
base_url = ujoin(app.proxy.public_server.host, app.hub.server.base_url)
|
||||
base_url = ujoin(public_url(app), app.hub.server.base_url)
|
||||
print(base_url)
|
||||
return requests.get(ujoin(base_url, path), **kw)
|
||||
|
||||
@@ -22,15 +21,17 @@ def test_root_no_auth(app, io_loop):
|
||||
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||
print(routes)
|
||||
print(app.hub.server)
|
||||
r = requests.get(app.proxy.public_server.host)
|
||||
url = public_url(app)
|
||||
print(url)
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
assert r.url == ujoin(app.proxy.public_server.host, app.hub.server.base_url, 'login')
|
||||
assert r.url == ujoin(url, app.hub.server.base_url, 'login')
|
||||
|
||||
def test_root_auth(app):
|
||||
cookies = app.login_user('river')
|
||||
r = requests.get(app.proxy.public_server.host, cookies=cookies)
|
||||
r = requests.get(public_url(app), cookies=cookies)
|
||||
r.raise_for_status()
|
||||
assert r.url == ujoin(app.proxy.public_server.host, '/user/river')
|
||||
assert r.url == user_url(app.users['river'], app)
|
||||
|
||||
def test_home_no_auth(app):
|
||||
r = get_page('home', app, allow_redirects=False)
|
||||
@@ -100,7 +101,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(app.proxy.public_server.host, app.hub.server.base_url)
|
||||
base_url = ujoin(public_url(app), app.hub.server.base_url)
|
||||
cookies = app.login_user('jones')
|
||||
orm_u = orm.User.find(app.db, 'jones')
|
||||
u = app.users[orm_u]
|
||||
@@ -121,7 +122,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(app.proxy.public_server.host, app.hub.server.base_url)
|
||||
base_url = ujoin(public_url(app), app.hub.server.base_url)
|
||||
cookies = app.login_user('jones')
|
||||
orm_u = orm.User.find(app.db, 'jones')
|
||||
u = app.users[orm_u]
|
||||
@@ -149,7 +150,7 @@ def test_spawn_form_with_file(app, io_loop):
|
||||
|
||||
|
||||
def test_static_files(app):
|
||||
base_url = ujoin(app.proxy.public_server.url, app.hub.server.base_url)
|
||||
base_url = ujoin(public_url(app), app.hub.server.base_url)
|
||||
print(base_url)
|
||||
r = requests.get(ujoin(base_url, 'logo'))
|
||||
r.raise_for_status()
|
||||
|
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
from queue import Queue
|
||||
from subprocess import Popen
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import orm
|
||||
from .mocking import MockHub
|
||||
@@ -34,6 +35,8 @@ def test_external_proxy(request, io_loop):
|
||||
'--api-port', str(proxy_port),
|
||||
'--default-target', 'http://%s:%i' % (app.hub_ip, app.hub_port),
|
||||
]
|
||||
if app.use_subdomains:
|
||||
cmd.append('--host-routing')
|
||||
proxy = Popen(cmd, env=env)
|
||||
def _cleanup_proxy():
|
||||
if proxy.poll() is None:
|
||||
@@ -60,7 +63,11 @@ def test_external_proxy(request, io_loop):
|
||||
r.raise_for_status()
|
||||
|
||||
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||
assert sorted(routes.keys()) == ['/', '/user/river']
|
||||
user_path = '/user/river'
|
||||
if app.use_subdomains:
|
||||
domain = urlparse(app.subdomain_host).hostname
|
||||
user_path = '/%s.%s' % (name, domain) + user_path
|
||||
assert sorted(routes.keys()) == ['/', user_path]
|
||||
|
||||
# teardown the proxy and start a new one in the same place
|
||||
proxy.terminate()
|
||||
@@ -76,7 +83,7 @@ def test_external_proxy(request, io_loop):
|
||||
|
||||
# check that the routes are correct
|
||||
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||
assert sorted(routes.keys()) == ['/', '/user/river']
|
||||
assert sorted(routes.keys()) == ['/', user_path]
|
||||
|
||||
# teardown the proxy again, and start a new one with different auth and port
|
||||
proxy.terminate()
|
||||
@@ -90,7 +97,8 @@ def test_external_proxy(request, io_loop):
|
||||
'--api-port', str(proxy_port),
|
||||
'--default-target', 'http://%s:%i' % (app.hub_ip, app.hub_port),
|
||||
]
|
||||
|
||||
if app.use_subdomains:
|
||||
cmd.append('--host-routing')
|
||||
proxy = Popen(cmd, env=env)
|
||||
wait_for_proxy()
|
||||
|
||||
@@ -113,7 +121,7 @@ def test_external_proxy(request, io_loop):
|
||||
|
||||
# check that the routes are correct
|
||||
routes = io_loop.run_sync(app.proxy.get_routes)
|
||||
assert sorted(routes.keys()) == ['/', '/user/river']
|
||||
assert sorted(routes.keys()) == ['/', user_path]
|
||||
|
||||
def test_check_routes(app, io_loop):
|
||||
proxy = app.proxy
|
||||
@@ -123,13 +131,14 @@ def test_check_routes(app, io_loop):
|
||||
r.raise_for_status()
|
||||
zoe = orm.User.find(app.db, 'zoe')
|
||||
assert zoe is not None
|
||||
zoe = app.users[zoe]
|
||||
before = sorted(io_loop.run_sync(app.proxy.get_routes))
|
||||
assert '/user/zoe' in before
|
||||
io_loop.run_sync(app.proxy.check_routes)
|
||||
assert zoe.proxy_path in before
|
||||
io_loop.run_sync(lambda : app.proxy.check_routes(app.users))
|
||||
io_loop.run_sync(lambda : proxy.delete_user(zoe))
|
||||
during = sorted(io_loop.run_sync(app.proxy.get_routes))
|
||||
assert '/user/zoe' not in during
|
||||
io_loop.run_sync(app.proxy.check_routes)
|
||||
assert zoe.proxy_path not in during
|
||||
io_loop.run_sync(lambda : app.proxy.check_routes(app.users))
|
||||
after = sorted(io_loop.run_sync(app.proxy.get_routes))
|
||||
assert '/user/zoe' in after
|
||||
assert zoe.proxy_path in after
|
||||
assert before == after
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from tornado import gen
|
||||
from tornado.log import app_log
|
||||
@@ -38,6 +38,12 @@ class UserDict(dict):
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, User):
|
||||
key = key.id
|
||||
elif isinstance(key, str):
|
||||
orm_user = self.db.query(orm.User).filter(orm.User.name==key).first()
|
||||
if orm_user is None:
|
||||
raise KeyError("No such user: %s" % name)
|
||||
else:
|
||||
key = orm_user
|
||||
if isinstance(key, orm.User):
|
||||
# users[orm_user] returns User(orm_user)
|
||||
orm_user = key
|
||||
@@ -148,6 +154,41 @@ class User(HasTraits):
|
||||
"""My name, escaped for use in URLs, cookies, etc."""
|
||||
return quote(self.name, safe='@')
|
||||
|
||||
@property
|
||||
def proxy_path(self):
|
||||
if self.settings.get('use_subdomains'):
|
||||
return url_path_join('/' + self.domain, self.server.base_url)
|
||||
else:
|
||||
return self.server.base_url
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
"""Get the domain for my server."""
|
||||
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
|
||||
return self.escaped_name + '.' + self.settings['domain']
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Get the *host* for my server (domain[:port])"""
|
||||
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
|
||||
parsed = urlparse(self.settings['subdomain_host'])
|
||||
h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)
|
||||
return h
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""My URL
|
||||
|
||||
Full name.domain/path if using subdomains, otherwise just my /base/url
|
||||
"""
|
||||
if self.settings.get('use_subdomains'):
|
||||
return '{host}{path}'.format(
|
||||
host=self.host,
|
||||
path=self.server.base_url,
|
||||
)
|
||||
else:
|
||||
return self.server.base_url
|
||||
|
||||
@gen.coroutine
|
||||
def spawn(self, options=None):
|
||||
"""Start the user's spawner"""
|
||||
|
@@ -104,7 +104,9 @@ class JupyterHubLoginHandler(LoginHandler):
|
||||
|
||||
class JupyterHubLogoutHandler(LogoutHandler):
|
||||
def get(self):
|
||||
self.redirect(url_path_join(self.settings['hub_prefix'], 'logout'))
|
||||
self.redirect(
|
||||
self.settings['hub_host'] +
|
||||
url_path_join(self.settings['hub_prefix'], 'logout'))
|
||||
|
||||
|
||||
# register new hub related command-line aliases
|
||||
@@ -113,6 +115,7 @@ aliases.update({
|
||||
'user' : 'SingleUserNotebookApp.user',
|
||||
'cookie-name': 'SingleUserNotebookApp.cookie_name',
|
||||
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
||||
'hub-host': 'SingleUserNotebookApp.hub_host',
|
||||
'hub-api-url': 'SingleUserNotebookApp.hub_api_url',
|
||||
'base-url': 'SingleUserNotebookApp.base_url',
|
||||
})
|
||||
@@ -141,6 +144,7 @@ class SingleUserNotebookApp(NotebookApp):
|
||||
self.log.name = new
|
||||
cookie_name = Unicode(config=True)
|
||||
hub_prefix = Unicode(config=True)
|
||||
hub_host = Unicode(config=True)
|
||||
hub_api_url = Unicode(config=True)
|
||||
aliases = aliases
|
||||
open_browser = False
|
||||
@@ -194,22 +198,22 @@ class SingleUserNotebookApp(NotebookApp):
|
||||
s['user'] = self.user
|
||||
s['hub_api_key'] = env.pop('JPY_API_TOKEN')
|
||||
s['hub_prefix'] = self.hub_prefix
|
||||
s['hub_host'] = self.hub_host
|
||||
s['cookie_name'] = self.cookie_name
|
||||
s['login_url'] = self.hub_prefix
|
||||
s['login_url'] = self.hub_host + self.hub_prefix
|
||||
s['hub_api_url'] = self.hub_api_url
|
||||
s['csp_report_uri'] = url_path_join(self.hub_prefix, 'security/csp-report')
|
||||
|
||||
s['csp_report_uri'] = self.hub_host + url_path_join(self.hub_prefix, 'security/csp-report')
|
||||
super(SingleUserNotebookApp, self).init_webapp()
|
||||
self.patch_templates()
|
||||
|
||||
def patch_templates(self):
|
||||
"""Patch page templates to add Hub-related buttons"""
|
||||
|
||||
self.jinja_template_vars['logo_url'] = url_path_join(self.hub_prefix, 'logo')
|
||||
self.jinja_template_vars['logo_url'] = self.hub_host + url_path_join(self.hub_prefix, 'logo')
|
||||
env = self.web_app.settings['jinja2_env']
|
||||
|
||||
env.globals['hub_control_panel_url'] = \
|
||||
url_path_join(self.hub_prefix, 'home')
|
||||
self.hub_host + url_path_join(self.hub_prefix, 'home')
|
||||
|
||||
# patch jinja env loading to modify page template
|
||||
def get_page(name):
|
||||
|
Reference in New Issue
Block a user