mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 05:53:00 +00:00
Use encoded URL when redirecting user notebooks
Otherwise it breaks for usernames that have url unsafe characters.
This commit is contained in:
@@ -46,35 +46,35 @@ class BaseHandler(RequestHandler):
|
|||||||
@property
|
@property
|
||||||
def base_url(self):
|
def base_url(self):
|
||||||
return self.settings.get('base_url', '/')
|
return self.settings.get('base_url', '/')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version_hash(self):
|
def version_hash(self):
|
||||||
return self.settings.get('version_hash', '')
|
return self.settings.get('version_hash', '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subdomain_host(self):
|
def subdomain_host(self):
|
||||||
return self.settings.get('subdomain_host', '')
|
return self.settings.get('subdomain_host', '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def domain(self):
|
def domain(self):
|
||||||
return self.settings['domain']
|
return self.settings['domain']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
return self.settings['db']
|
return self.settings['db']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self):
|
def users(self):
|
||||||
return self.settings.setdefault('users', {})
|
return self.settings.setdefault('users', {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hub(self):
|
def hub(self):
|
||||||
return self.settings['hub']
|
return self.settings['hub']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def proxy(self):
|
def proxy(self):
|
||||||
return self.settings['proxy']
|
return self.settings['proxy']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def authenticator(self):
|
def authenticator(self):
|
||||||
return self.settings.get('authenticator', None)
|
return self.settings.get('authenticator', None)
|
||||||
@@ -83,28 +83,28 @@ class BaseHandler(RequestHandler):
|
|||||||
"""Roll back any uncommitted transactions from the handler."""
|
"""Roll back any uncommitted transactions from the handler."""
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
super().finish(*args, **kwargs)
|
super().finish(*args, **kwargs)
|
||||||
|
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
# Security policies
|
# Security policies
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
|
|
||||||
@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.server.base_url, 'security/csp-report')
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_security_policy(self):
|
def content_security_policy(self):
|
||||||
"""The default Content-Security-Policy header
|
"""The default Content-Security-Policy header
|
||||||
|
|
||||||
Can be overridden by defining Content-Security-Policy in settings['headers']
|
Can be overridden by defining Content-Security-Policy in settings['headers']
|
||||||
"""
|
"""
|
||||||
return '; '.join([
|
return '; '.join([
|
||||||
"frame-ancestors 'self'",
|
"frame-ancestors 'self'",
|
||||||
"report-uri " + self.csp_report_uri,
|
"report-uri " + self.csp_report_uri,
|
||||||
])
|
])
|
||||||
|
|
||||||
def set_default_headers(self):
|
def set_default_headers(self):
|
||||||
"""
|
"""
|
||||||
Set any headers passed as tornado_settings['headers'].
|
Set any headers passed as tornado_settings['headers'].
|
||||||
@@ -113,7 +113,7 @@ class BaseHandler(RequestHandler):
|
|||||||
"""
|
"""
|
||||||
headers = self.settings.get('headers', {})
|
headers = self.settings.get('headers', {})
|
||||||
headers.setdefault("Content-Security-Policy", self.content_security_policy)
|
headers.setdefault("Content-Security-Policy", self.content_security_policy)
|
||||||
|
|
||||||
for header_name, header_content in headers.items():
|
for header_name, header_content in headers.items():
|
||||||
self.set_header(header_name, header_content)
|
self.set_header(header_name, header_content)
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ class BaseHandler(RequestHandler):
|
|||||||
@property
|
@property
|
||||||
def admin_users(self):
|
def admin_users(self):
|
||||||
return self.settings.setdefault('admin_users', set())
|
return self.settings.setdefault('admin_users', set())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookie_max_age_days(self):
|
def cookie_max_age_days(self):
|
||||||
return self.settings.get('cookie_max_age_days', None)
|
return self.settings.get('cookie_max_age_days', None)
|
||||||
@@ -141,7 +141,7 @@ class BaseHandler(RequestHandler):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return orm_token.user
|
return orm_token.user
|
||||||
|
|
||||||
def _user_for_cookie(self, cookie_name, cookie_value=None):
|
def _user_for_cookie(self, cookie_name, cookie_value=None):
|
||||||
"""Get the User for a given cookie, if there is one"""
|
"""Get the User for a given cookie, if there is one"""
|
||||||
cookie_id = self.get_secure_cookie(
|
cookie_id = self.get_secure_cookie(
|
||||||
@@ -151,7 +151,7 @@ class BaseHandler(RequestHandler):
|
|||||||
)
|
)
|
||||||
def clear():
|
def clear():
|
||||||
self.clear_cookie(cookie_name, path=self.hub.server.base_url)
|
self.clear_cookie(cookie_name, path=self.hub.server.base_url)
|
||||||
|
|
||||||
if cookie_id is None:
|
if cookie_id is None:
|
||||||
if self.get_cookie(cookie_name):
|
if self.get_cookie(cookie_name):
|
||||||
self.log.warn("Invalid or expired cookie token")
|
self.log.warn("Invalid or expired cookie token")
|
||||||
@@ -165,27 +165,27 @@ class BaseHandler(RequestHandler):
|
|||||||
# have cookie, but it's not valid. Clear it and start over.
|
# have cookie, but it's not valid. Clear it and start over.
|
||||||
clear()
|
clear()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def _user_from_orm(self, orm_user):
|
def _user_from_orm(self, orm_user):
|
||||||
"""return User wrapper from orm.User object"""
|
"""return User wrapper from orm.User object"""
|
||||||
if orm_user is None:
|
if orm_user is None:
|
||||||
return
|
return
|
||||||
return self.users[orm_user]
|
return self.users[orm_user]
|
||||||
|
|
||||||
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.server.cookie_name)
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
"""get current username"""
|
"""get current username"""
|
||||||
user = self.get_current_user_token()
|
user = self.get_current_user_token()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return user
|
return user
|
||||||
return self.get_current_user_cookie()
|
return self.get_current_user_cookie()
|
||||||
|
|
||||||
def find_user(self, name):
|
def find_user(self, name):
|
||||||
"""Get a user by name
|
"""Get a user by name
|
||||||
|
|
||||||
return None if no such user
|
return None if no such user
|
||||||
"""
|
"""
|
||||||
orm_user = orm.User.find(db=self.db, name=name)
|
orm_user = orm.User.find(db=self.db, name=name)
|
||||||
@@ -201,7 +201,7 @@ class BaseHandler(RequestHandler):
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
user = self._user_from_orm(u)
|
user = self._user_from_orm(u)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def clear_login_cookie(self, name=None):
|
def clear_login_cookie(self, name=None):
|
||||||
if name is None:
|
if name is None:
|
||||||
user = self.get_current_user()
|
user = self.get_current_user()
|
||||||
@@ -213,7 +213,7 @@ class BaseHandler(RequestHandler):
|
|||||||
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.server.cookie_name, path=self.hub.server.base_url, **kwargs)
|
||||||
|
|
||||||
def _set_user_cookie(self, user, server):
|
def _set_user_cookie(self, user, server):
|
||||||
# tornado <4.2 have a bug that consider secure==True as soon as
|
# tornado <4.2 have a bug that consider secure==True as soon as
|
||||||
# 'secure' kwarg is passed to set_secure_cookie
|
# 'secure' kwarg is passed to set_secure_cookie
|
||||||
@@ -230,15 +230,15 @@ class BaseHandler(RequestHandler):
|
|||||||
path=server.base_url,
|
path=server.base_url,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_server_cookie(self, user):
|
def set_server_cookie(self, user):
|
||||||
"""set the login cookie for the single-user server"""
|
"""set the login cookie for the single-user server"""
|
||||||
self._set_user_cookie(user, user.server)
|
self._set_user_cookie(user, user.server)
|
||||||
|
|
||||||
def set_hub_cookie(self, user):
|
def set_hub_cookie(self, user):
|
||||||
"""set the login cookie for the Hub"""
|
"""set the login cookie for the Hub"""
|
||||||
self._set_user_cookie(user, self.hub.server)
|
self._set_user_cookie(user, self.hub.server)
|
||||||
|
|
||||||
def set_login_cookie(self, user):
|
def set_login_cookie(self, user):
|
||||||
"""Set login cookies for the Hub and single-user server."""
|
"""Set login cookies for the Hub and single-user server."""
|
||||||
if self.subdomain_host and not self.request.host.startswith(self.domain):
|
if self.subdomain_host and not self.request.host.startswith(self.domain):
|
||||||
@@ -248,11 +248,11 @@ class BaseHandler(RequestHandler):
|
|||||||
# create and set a new cookie token for the single-user server
|
# create and set a new cookie token for the single-user server
|
||||||
if user.server:
|
if user.server:
|
||||||
self.set_server_cookie(user)
|
self.set_server_cookie(user)
|
||||||
|
|
||||||
# create and set a new cookie token for the hub
|
# create and set a new cookie token for the hub
|
||||||
if not self.get_current_user_cookie():
|
if not self.get_current_user_cookie():
|
||||||
self.set_hub_cookie(user)
|
self.set_hub_cookie(user)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def authenticate(self, data):
|
def authenticate(self, data):
|
||||||
auth = self.authenticator
|
auth = self.authenticator
|
||||||
@@ -278,7 +278,7 @@ class BaseHandler(RequestHandler):
|
|||||||
@property
|
@property
|
||||||
def spawner_class(self):
|
def spawner_class(self):
|
||||||
return self.settings.get('spawner_class', LocalProcessSpawner)
|
return self.settings.get('spawner_class', LocalProcessSpawner)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def spawn_single_user(self, user, options=None):
|
def spawn_single_user(self, user, options=None):
|
||||||
if user.spawn_pending:
|
if user.spawn_pending:
|
||||||
@@ -290,7 +290,7 @@ class BaseHandler(RequestHandler):
|
|||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def finish_user_spawn(f=None):
|
def finish_user_spawn(f=None):
|
||||||
"""Finish the user spawn by registering listeners and notifying the proxy.
|
"""Finish the user spawn by registering listeners and notifying the proxy.
|
||||||
|
|
||||||
If the spawner is slow to start, this is passed as an async callback,
|
If the spawner is slow to start, this is passed as an async callback,
|
||||||
otherwise it is called immediately.
|
otherwise it is called immediately.
|
||||||
"""
|
"""
|
||||||
@@ -301,7 +301,7 @@ class BaseHandler(RequestHandler):
|
|||||||
self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic)
|
self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic)
|
||||||
yield self.proxy.add_user(user)
|
yield self.proxy.add_user(user)
|
||||||
user.spawner.add_poll_callback(self.user_stopped, user)
|
user.spawner.add_poll_callback(self.user_stopped, user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), f)
|
yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), f)
|
||||||
except gen.TimeoutError:
|
except gen.TimeoutError:
|
||||||
@@ -325,7 +325,7 @@ class BaseHandler(RequestHandler):
|
|||||||
raise web.HTTPError(500, "Spawner failed to start [status=%s]" % status)
|
raise web.HTTPError(500, "Spawner failed to start [status=%s]" % status)
|
||||||
else:
|
else:
|
||||||
yield finish_user_spawn()
|
yield finish_user_spawn()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def user_stopped(self, user):
|
def user_stopped(self, user):
|
||||||
"""Callback that fires when the spawner has stopped"""
|
"""Callback that fires when the spawner has stopped"""
|
||||||
@@ -337,7 +337,7 @@ class BaseHandler(RequestHandler):
|
|||||||
)
|
)
|
||||||
yield self.proxy.delete_user(user)
|
yield self.proxy.delete_user(user)
|
||||||
yield user.stop()
|
yield user.stop()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def stop_single_user(self, user):
|
def stop_single_user(self, user):
|
||||||
if user.stop_pending:
|
if user.stop_pending:
|
||||||
@@ -348,7 +348,7 @@ class BaseHandler(RequestHandler):
|
|||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def finish_stop(f=None):
|
def finish_stop(f=None):
|
||||||
"""Finish the stop action by noticing that the user is stopped.
|
"""Finish the stop action by noticing that the user is stopped.
|
||||||
|
|
||||||
If the spawner is slow to stop, this is passed as an async callback,
|
If the spawner is slow to stop, this is passed as an async callback,
|
||||||
otherwise it is called immediately.
|
otherwise it is called immediately.
|
||||||
"""
|
"""
|
||||||
@@ -357,7 +357,7 @@ class BaseHandler(RequestHandler):
|
|||||||
return
|
return
|
||||||
toc = IOLoop.current().time()
|
toc = IOLoop.current().time()
|
||||||
self.log.info("User %s server took %.3f seconds to stop", user.name, toc-tic)
|
self.log.info("User %s server took %.3f seconds to stop", user.name, toc-tic)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), f)
|
yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), f)
|
||||||
except gen.TimeoutError:
|
except gen.TimeoutError:
|
||||||
@@ -443,7 +443,7 @@ class Template404(BaseHandler):
|
|||||||
|
|
||||||
class PrefixRedirectHandler(BaseHandler):
|
class PrefixRedirectHandler(BaseHandler):
|
||||||
"""Redirect anything outside a prefix inside.
|
"""Redirect anything outside a prefix inside.
|
||||||
|
|
||||||
Redirects /foo to /prefix/foo, etc.
|
Redirects /foo to /prefix/foo, etc.
|
||||||
"""
|
"""
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@@ -8,17 +8,17 @@ from tornado import web, gen
|
|||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import admin_only, url_path_join
|
from ..utils import admin_only, url_path_join
|
||||||
from .base import BaseHandler
|
from .base import BaseHandler
|
||||||
from .login import LoginHandler
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
class RootHandler(BaseHandler):
|
class RootHandler(BaseHandler):
|
||||||
"""Render the Hub root page.
|
"""Render the Hub root page.
|
||||||
|
|
||||||
If logged in, redirects to:
|
If logged in, redirects to:
|
||||||
|
|
||||||
- single-user server if running
|
- single-user server if running
|
||||||
- hub home, otherwise
|
- hub home, otherwise
|
||||||
|
|
||||||
Otherwise, renders login page.
|
Otherwise, renders login page.
|
||||||
"""
|
"""
|
||||||
def get(self):
|
def get(self):
|
||||||
@@ -49,9 +49,9 @@ class HomeHandler(BaseHandler):
|
|||||||
|
|
||||||
class SpawnHandler(BaseHandler):
|
class SpawnHandler(BaseHandler):
|
||||||
"""Handle spawning of single-user servers via form.
|
"""Handle spawning of single-user servers via form.
|
||||||
|
|
||||||
GET renders the form, POST handles form submission.
|
GET renders the form, POST handles form submission.
|
||||||
|
|
||||||
Only enabled when Spawner.options_form is defined.
|
Only enabled when Spawner.options_form is defined.
|
||||||
"""
|
"""
|
||||||
def _render_form(self, message=''):
|
def _render_form(self, message=''):
|
||||||
@@ -75,9 +75,11 @@ class SpawnHandler(BaseHandler):
|
|||||||
self.finish(self._render_form())
|
self.finish(self._render_form())
|
||||||
else:
|
else:
|
||||||
# not running, no form. Trigger spawn.
|
# not running, no form. Trigger spawn.
|
||||||
url = url_path_join(self.base_url, 'user', user.name)
|
# Creating the URL manually since the server does not
|
||||||
|
# exist yet
|
||||||
|
url = url_path_join(self.base_url, 'user', quote(user.name))
|
||||||
self.redirect(url)
|
self.redirect(url)
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -122,14 +124,14 @@ class AdminHandler(BaseHandler):
|
|||||||
}
|
}
|
||||||
sorts = self.get_arguments('sort') or default_sort
|
sorts = self.get_arguments('sort') or default_sort
|
||||||
orders = self.get_arguments('order')
|
orders = self.get_arguments('order')
|
||||||
|
|
||||||
for bad in set(sorts).difference(available):
|
for bad in set(sorts).difference(available):
|
||||||
self.log.warn("ignoring invalid sort: %r", bad)
|
self.log.warn("ignoring invalid sort: %r", bad)
|
||||||
sorts.remove(bad)
|
sorts.remove(bad)
|
||||||
for bad in set(orders).difference({'asc', 'desc'}):
|
for bad in set(orders).difference({'asc', 'desc'}):
|
||||||
self.log.warn("ignoring invalid order: %r", bad)
|
self.log.warn("ignoring invalid order: %r", bad)
|
||||||
orders.remove(bad)
|
orders.remove(bad)
|
||||||
|
|
||||||
# add default sort as secondary
|
# add default sort as secondary
|
||||||
for s in default_sort:
|
for s in default_sort:
|
||||||
if s not in sorts:
|
if s not in sorts:
|
||||||
@@ -139,17 +141,17 @@ class AdminHandler(BaseHandler):
|
|||||||
orders.append(default_order[col])
|
orders.append(default_order[col])
|
||||||
else:
|
else:
|
||||||
orders = orders[:len(sorts)]
|
orders = orders[:len(sorts)]
|
||||||
|
|
||||||
# this could be one incomprehensible nested list comprehension
|
# this could be one incomprehensible nested list comprehension
|
||||||
# get User columns
|
# get User columns
|
||||||
cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
|
cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
|
||||||
# get User.col.desc() order objects
|
# get User.col.desc() order objects
|
||||||
ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
|
ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
|
||||||
|
|
||||||
users = self.db.query(orm.User).order_by(*ordered)
|
users = self.db.query(orm.User).order_by(*ordered)
|
||||||
users = [ self._user_from_orm(u) for u in users ]
|
users = [ self._user_from_orm(u) for u in users ]
|
||||||
running = [ u for u in users if u.running ]
|
running = [ u for u in users if u.running ]
|
||||||
|
|
||||||
html = self.render_template('admin.html',
|
html = self.render_template('admin.html',
|
||||||
user=self.get_current_user(),
|
user=self.get_current_user(),
|
||||||
admin_access=self.settings.get('admin_access', False),
|
admin_access=self.settings.get('admin_access', False),
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a id="start" class="btn btn-lg btn-success"
|
<a id="start" class="btn btn-lg btn-success"
|
||||||
{% if user.running %}
|
{% if user.running %}
|
||||||
href="{{base_url}}user/{{user.name}}/"
|
href="{{ user.server.base_url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
href="{{base_url}}spawn"
|
href="{{base_url}}spawn"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Reference in New Issue
Block a user