Compare commits

...

18 Commits
1.3.0 ... 0.9.6

Author SHA1 Message Date
Min RK
5980ff1011 0.9.6 2019-04-01 12:17:49 +02:00
Min RK
2e8781c35b Changelog for 0.9.6
replace 0.9.5 which has only a partial fix

issue is now confirmed to affect all browsers
2019-04-01 12:17:40 +02:00
Min RK
3f1332e38f Further login redirect validation 2019-04-01 12:12:15 +02:00
Min RK
db851cd230 Merge pull request #2488 from minrk/post_push
Docker hook fixes
2019-04-01 12:06:49 +02:00
Min RK
8c8e26802a fix unbound variable in post_push 2019-03-28 13:00:09 +01:00
Min RK
6a4900c468 release 0.9.5 2019-03-28 11:07:09 +01:00
Min RK
efbb692540 changelog for 0.9.5 2019-03-28 11:04:00 +01:00
Min RK
244ab813fe protect against some browsers' buggy handling of backslash as slash 2019-03-28 10:30:36 +01:00
Min RK
b1111363fd release 0.9.4 2018-09-24 13:02:36 +02:00
Min RK
6c99b807c2 update changelog for 0.9.4 2018-09-24 13:00:27 +02:00
Min RK
8d650f594e changelog for 0.9.4 2018-09-24 12:58:16 +02:00
Min RK
04a0a3a2e5 fix oauth client cleanup
- delete oauth clients for servers when they shutdown
- avoid deleting oauth clients for servers still running across an 0.8 -> 0.9 upgrade, when the oauth client ids changed from `user-NAME` to `jupyterhub-user-NAME`
2018-09-24 12:58:10 +02:00
Min RK
9cebfd6367 Fix content-type on API endpoints
and includes content-type header checks in tests to catch regressions
2018-09-24 12:57:26 +02:00
Min RK
587cd70221 omit pdf builds on rtd due to bug in sphinx 2018-09-24 12:57:01 +02:00
Min RK
e94f5e043a release 0.9.3 2018-09-12 09:46:02 +02:00
Min RK
5456fb6356 remove spurious print from keepalive code
and send keepalive every 8 seconds

to protect against possibly aggressive proxies dropping connections after 10 seconds of inactivity
2018-09-12 09:46:02 +02:00
Min RK
fb75b9a392 write needs no await 2018-09-11 16:42:29 +02:00
Min RK
90d341e6f7 changelog for 0.9.3
Mainly small fixes, but the token page could be completely broken

This release will include the spawner.handler addition,
but not the oauthlib change currently in master
2018-09-11 16:42:21 +02:00
13 changed files with 119 additions and 16 deletions

View File

@@ -3,7 +3,7 @@ swagger: '2.0'
info: info:
title: JupyterHub title: JupyterHub
description: The REST API for JupyterHub description: The REST API for JupyterHub
version: 0.9.0dev version: 0.9.4
license: license:
name: BSD-3-Clause name: BSD-3-Clause
schemes: schemes:

View File

@@ -9,6 +9,38 @@ command line for details.
## 0.9 ## 0.9
### [0.9.6] 2019-04-01
JupyterHub 0.9.6 is a security release.
- Fixes an Open Redirect vulnerability (CVE-2019-10255).
JupyterHub 0.9.5 included a partial fix for this issue.
### [0.9.4] 2018-09-24
JupyterHub 0.9.4 is a small bugfix release.
- Fixes an issue that required all running user servers to be restarted
when performing an upgrade from 0.8 to 0.9.
- Fixes content-type for API endpoints back to `application/json`.
It was `text/html` in 0.9.0-0.9.3.
### [0.9.3] 2018-09-12
JupyterHub 0.9.3 contains small bugfixes and improvements
- Fix token page and model handling of `expires_at`.
This field was missing from the REST API model for tokens
and could cause the token page to not render
- Add keep-alive to progress event stream to avoid proxies dropping
the connection due to inactivity
- Documentation and example improvements
- Disable quit button when using notebook 5.6
- Prototype new feature (may change prior to 1.0):
pass requesting Handler to Spawners during start,
accessible as `self.handler`
### [0.9.2] 2018-08-10 ### [0.9.2] 2018-08-10
JupyterHub 0.9.2 contains small bugfixes and improvements. JupyterHub 0.9.2 contains small bugfixes and improvements.
@@ -402,7 +434,10 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...HEAD [Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.9.6...HEAD
[0.9.6]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...0.9.6
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4
[0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3
[0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2 [0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2
[0.9.1]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...0.9.1 [0.9.1]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...0.9.1
[0.9.0]: https://github.com/jupyterhub/jupyterhub/compare/0.8.1...0.9.0 [0.9.0]: https://github.com/jupyterhub/jupyterhub/compare/0.8.1...0.9.0

View File

@@ -12,8 +12,11 @@ function get_hub_version() {
split=( ${hub_xyz//./ } ) split=( ${hub_xyz//./ } )
hub_xy="${split[0]}.${split[1]}" hub_xy="${split[0]}.${split[1]}"
# add .dev on hub_xy so it's 1.0.dev # add .dev on hub_xy so it's 1.0.dev
if [[ ! -z "${split[3]}" ]]; then if [[ ! -z "${split[3]:-}" ]]; then
hub_xy="${hub_xy}.${split[3]}" hub_xy="${hub_xy}.${split[3]}"
latest=0
else
latest=1
fi fi
} }
@@ -31,3 +34,11 @@ docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
docker push $DOCKER_REPO:$hub_xy docker push $DOCKER_REPO:$hub_xy
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:$hub_xy docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:$hub_xy
docker push $ONBUILD:$hub_xyz docker push $ONBUILD:$hub_xyz
# if building a stable release, tag latest as well
if [[ "$latest" == "1" ]]; then
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:latest
docker push $DOCKER_REPO:latest
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:latest
docker push $ONBUILD:latest
fi

View File

@@ -4,11 +4,11 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
version_info = ( version_info = (
1,
0,
0, 0,
9,
6,
"", # release (b1, rc1, or "" for final or dev) "", # release (b1, rc1, or "" for final or dev)
"dev", # dev or nothing # "dev", # dev or nothing
) )
# pep 440 version: no dot before beta/rc, but before .dev # pep 440 version: no dot before beta/rc, but before .dev

View File

@@ -30,6 +30,9 @@ class APIHandler(BaseHandler):
def content_security_policy(self): def content_security_policy(self):
return '; '.join([super().content_security_policy, "default-src 'none'"]) return '; '.join([super().content_security_policy, "default-src 'none'"])
def get_content_type(self):
return 'application/json'
def check_referer(self): def check_referer(self):
"""Check Origin for cross-site API requests. """Check Origin for cross-site API requests.
@@ -265,3 +268,13 @@ class APIHandler(BaseHandler):
def options(self, *args, **kwargs): def options(self, *args, **kwargs):
self.finish() self.finish()
class API404(APIHandler):
"""404 for API requests
Ensures JSON 404 errors for malformed URLs
"""
async def prepare(self):
await super().prepare()
raise web.HTTPError(404)

View File

@@ -429,7 +429,7 @@ class UserAdminAccessAPIHandler(APIHandler):
class SpawnProgressAPIHandler(APIHandler): class SpawnProgressAPIHandler(APIHandler):
"""EventStream handler for pending spawns""" """EventStream handler for pending spawns"""
keepalive_interval = 10 keepalive_interval = 8
def get_content_type(self): def get_content_type(self):
return 'text/event-stream' return 'text/event-stream'
@@ -445,7 +445,6 @@ class SpawnProgressAPIHandler(APIHandler):
_finished = False _finished = False
def on_finish(self): def on_finish(self):
print("on finish")
self._finished = True self._finished = True
async def keepalive(self): async def keepalive(self):
@@ -456,7 +455,7 @@ class SpawnProgressAPIHandler(APIHandler):
""" """
while not self._finished: while not self._finished:
try: try:
await self.write("\n\n") self.write("\n\n")
except (StreamClosedError, RuntimeError): except (StreamClosedError, RuntimeError):
return return
await asyncio.sleep(self.keepalive_interval) await asyncio.sleep(self.keepalive_interval)

View File

@@ -973,6 +973,8 @@ class JupyterHub(Application):
h.extend(self.extra_handlers) h.extend(self.extra_handlers)
h.append((r'/logo', LogoHandler, {'path': self.logo_file})) h.append((r'/logo', LogoHandler, {'path': self.logo_file}))
h.append((r'/api/(.*)', apihandlers.base.API404))
self.handlers = self.add_url_prefix(self.hub_prefix, h) self.handlers = self.add_url_prefix(self.hub_prefix, h)
# some extra handlers, outside hub_prefix # some extra handlers, outside hub_prefix
self.handlers.extend([ self.handlers.extend([
@@ -1506,6 +1508,10 @@ class JupyterHub(Application):
for user in self.users.values(): for user in self.users.values():
for spawner in user.spawners.values(): for spawner in user.spawners.values():
oauth_client_ids.add(spawner.oauth_client_id) oauth_client_ids.add(spawner.oauth_client_id)
# avoid deleting clients created by 0.8
# 0.9 uses `jupyterhub-user-...` for the client id, while
# 0.8 uses just `user-...`
oauth_client_ids.add(spawner.oauth_client_id.split('-', 1)[1])
client_store = self.oauth_provider.client_authenticator.client_store client_store = self.oauth_provider.client_authenticator.client_store
for i, oauth_client in enumerate(self.db.query(orm.OAuthClient)): for i, oauth_client in enumerate(self.db.query(orm.OAuthClient)):

View File

@@ -427,6 +427,8 @@ class BaseHandler(RequestHandler):
- else: /hub/home - else: /hub/home
""" """
next_url = self.get_argument('next', default='') next_url = self.get_argument('next', default='')
# protect against some browsers' buggy handling of backslash as slash
next_url = next_url.replace('\\', '%5C')
if (next_url + '/').startswith( if (next_url + '/').startswith(
( (
'%s://%s/' % (self.request.protocol, self.request.host), '%s://%s/' % (self.request.protocol, self.request.host),
@@ -434,15 +436,23 @@ class BaseHandler(RequestHandler):
) )
): ):
# treat absolute URLs for our host as absolute paths: # treat absolute URLs for our host as absolute paths:
# below, redirects that aren't strictly paths
parsed = urlparse(next_url) parsed = urlparse(next_url)
next_url = parsed.path next_url = parsed.path
if parsed.query: if parsed.query:
next_url = next_url + '?' + parsed.query next_url = next_url + '?' + parsed.query
if parsed.hash: if parsed.fragment:
next_url = next_url + '#' + parsed.hash next_url = next_url + '#' + parsed.fragment
if next_url and (urlparse(next_url).netloc or not next_url.startswith('/')):
# if it still has host info, it didn't match our above check for *this* host
if next_url and (
'://' in next_url
or next_url.startswith('//')
or not next_url.startswith('/')
):
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url) self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
next_url = '' next_url = ''
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')): if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
# add /hub/ prefix, to ensure we redirect to the right user's server. # add /hub/ prefix, to ensure we redirect to the right user's server.
# The next request will be handled by SpawnHandler, # The next request will be handled by SpawnHandler,

View File

@@ -100,6 +100,8 @@ def api_request(app, *api_path, **kwargs):
assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy'] assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy']
assert ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy'] assert ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy']
assert 'http' not in resp.headers['Content-Security-Policy'] assert 'http' not in resp.headers['Content-Security-Policy']
if not kwargs.get('stream', False) and resp.content:
assert resp.headers.get('content-type') == 'application/json'
return resp return resp
@@ -746,6 +748,8 @@ def test_progress(request, app, no_patience, slow_spawn):
r = yield api_request(app, 'users', name, 'server/progress', stream=True) r = yield api_request(app, 'users', name, 'server/progress', stream=True)
r.raise_for_status() r.raise_for_status()
request.addfinalizer(r.close) request.addfinalizer(r.close)
assert r.headers['content-type'] == 'text/event-stream'
ex = async_requests.executor ex = async_requests.executor
line_iter = iter(r.iter_lines(decode_unicode=True)) line_iter = iter(r.iter_lines(decode_unicode=True))
evt = yield ex.submit(next_event, line_iter) evt = yield ex.submit(next_event, line_iter)
@@ -807,6 +811,7 @@ def test_progress_ready(request, app):
r = yield api_request(app, 'users', name, 'server/progress', stream=True) r = yield api_request(app, 'users', name, 'server/progress', stream=True)
r.raise_for_status() r.raise_for_status()
request.addfinalizer(r.close) request.addfinalizer(r.close)
assert r.headers['content-type'] == 'text/event-stream'
ex = async_requests.executor ex = async_requests.executor
line_iter = iter(r.iter_lines(decode_unicode=True)) line_iter = iter(r.iter_lines(decode_unicode=True))
evt = yield ex.submit(next_event, line_iter) evt = yield ex.submit(next_event, line_iter)
@@ -826,6 +831,7 @@ def test_progress_bad(request, app, no_patience, bad_spawn):
r = yield api_request(app, 'users', name, 'server/progress', stream=True) r = yield api_request(app, 'users', name, 'server/progress', stream=True)
r.raise_for_status() r.raise_for_status()
request.addfinalizer(r.close) request.addfinalizer(r.close)
assert r.headers['content-type'] == 'text/event-stream'
ex = async_requests.executor ex = async_requests.executor
line_iter = iter(r.iter_lines(decode_unicode=True)) line_iter = iter(r.iter_lines(decode_unicode=True))
evt = yield ex.submit(next_event, line_iter) evt = yield ex.submit(next_event, line_iter)
@@ -847,6 +853,7 @@ def test_progress_bad_slow(request, app, no_patience, slow_bad_spawn):
r = yield api_request(app, 'users', name, 'server/progress', stream=True) r = yield api_request(app, 'users', name, 'server/progress', stream=True)
r.raise_for_status() r.raise_for_status()
request.addfinalizer(r.close) request.addfinalizer(r.close)
assert r.headers['content-type'] == 'text/event-stream'
ex = async_requests.executor ex = async_requests.executor
line_iter = iter(r.iter_lines(decode_unicode=True)) line_iter = iter(r.iter_lines(decode_unicode=True))
evt = yield ex.submit(next_event, line_iter) evt = yield ex.submit(next_event, line_iter)

View File

@@ -409,10 +409,13 @@ def test_login_strip(app):
(False, '/has?query#andhash', '/has?query#andhash'), (False, '/has?query#andhash', '/has?query#andhash'),
# next_url outside is not allowed # next_url outside is not allowed
(False, 'relative/path', ''),
(False, 'https://other.domain', ''), (False, 'https://other.domain', ''),
(False, 'ftp://other.domain', ''), (False, 'ftp://other.domain', ''),
(False, '//other.domain', ''), (False, '//other.domain', ''),
] (False, '///other.domain/triple', ''),
(False, '\\\\other.domain/backslashes', ''),
],
) )
@pytest.mark.gen_test @pytest.mark.gen_test
def test_login_redirect(app, running, next_url, location): def test_login_redirect(app, running, next_url, location):
@@ -426,7 +429,7 @@ def test_login_redirect(app, running, next_url, location):
url = 'login' url = 'login'
if next_url: if next_url:
if '//' not in next_url: if '//' not in next_url and next_url.startswith('/'):
next_url = ujoin(app.base_url, next_url, '') next_url = ujoin(app.base_url, next_url, '')
url = url_concat(url, dict(next=next_url)) url = url_concat(url, dict(next=next_url))

View File

@@ -558,11 +558,25 @@ class User:
# remove server entry from db # remove server entry from db
spawner.server = None spawner.server = None
if not spawner.will_resume: if not spawner.will_resume:
# find and remove the API token if the spawner isn't # find and remove the API token and oauth client if the spawner isn't
# going to re-use it next time # going to re-use it next time
orm_token = orm.APIToken.find(self.db, api_token) orm_token = orm.APIToken.find(self.db, api_token)
if orm_token: if orm_token:
self.db.delete(orm_token) self.db.delete(orm_token)
# remove oauth client as well
# handle upgrades from 0.8, where client id will be `user-USERNAME`,
# not just `jupyterhub-user-USERNAME`
client_ids = (
spawner.oauth_client_id,
spawner.oauth_client_id.split('-', 1)[1],
)
for oauth_client in (
self.db
.query(orm.OAuthClient)
.filter(orm.OAuthClient.identifier.in_(client_ids))
):
self.log.debug("Deleting oauth client %s", oauth_client.identifier)
self.db.delete(oauth_client)
self.db.commit() self.db.commit()
finally: finally:
spawner.orm_spawner.started = None spawner.orm_spawner.started = None

View File

@@ -4,3 +4,8 @@ conda:
file: docs/environment.yml file: docs/environment.yml
python: python:
version: 3 version: 3
formats:
- htmlzip
- epub
# pdf disabled due to bug in sphinx 1.8 + recommonmark
# - pdf

View File

@@ -14,7 +14,7 @@ function get_hub_version() {
split=( ${hub_xyz//./ } ) split=( ${hub_xyz//./ } )
hub_xy="${split[0]}.${split[1]}" hub_xy="${split[0]}.${split[1]}"
# add .dev on hub_xy so it's 1.0.dev # add .dev on hub_xy so it's 1.0.dev
if [[ ! -z "${split[3]}" ]]; then if [[ ! -z "${split[3]:-}" ]]; then
hub_xy="${hub_xy}.${split[3]}" hub_xy="${hub_xy}.${split[3]}"
fi fi
} }