Use encoded URL when redirecting user notebooks

Otherwise it breaks for usernames that have url unsafe
characters.
This commit is contained in:
YuviPanda
2016-03-08 17:08:23 -08:00
parent f7dab558e4
commit e0219d0363
3 changed files with 53 additions and 51 deletions

View File

@@ -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):

View File

@@ -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),

View File

@@ -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 %}