Merge pull request #2005 from minrk/require-api-token

forward-port xsrf fixes
Min RK 9 years ago committed by GitHub
commit 5cafbfe822

@ -11,7 +11,8 @@ For more detailed information, see
Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to
upgrade to the latest release.
.. _release-4.3.1:
4.3.1
@ -19,28 +20,46 @@ For more detailed information, see
4.3.1 is a patch release with a security patch, a couple bug fixes, and improvements to the newly-released token authentication.
**Security fix**:
- CVE-2016-9971. Fix CSRF vulnerability,
where malicious forms could create untitled files and start kernels
(no remote execution or modification of existing files)
for users of certain browsers (Firefox, Internet Explorer / Edge).
All previous notebook releases are affected.
Bug fixes:
- Fix carriage return handling
- Make the font size more robust against fickle brow
- Make the font size more robust against fickle browsers
- Ignore resize events that bubbled up and didn't come from window
- Add Authorization to allowed CORS headers
- Downgrade CodeMirror to 5.16 while we figure out issues in Safari
Other improvements:
- Better docs for token-based authentication
- Further highlight token info in log output when autogenerated
- Add Authorization to allowed CORS headers
See the 4.3 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.1%20>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.1%20>`__ involved in this release.
See the 4.3.1 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3.1>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3.1>`__ involved in this release.
.. _release-4.3:
4.3
---
4.3.0
-----
4.3 is a minor release with many bug fixes and improvements.
The biggest user-facing change is the addition of token authentication,
which is enabled by default.
A token is generated and used when your browser is opened automatically,
so you shouldn't have to enter anything in the default circumstances.
If you see a login page
(e.g. by switching browsers, or launching on a new port with ``--no-browser``),
you get a login URL with the token from the command ``jupyter notebook list``,
which you can paste into your browser.
Highlights:
@ -88,6 +107,7 @@ See the 4.3 milestone on GitHub for a complete list of
`issues <https://github.com/jupyter/notebook/issues?utf8=%E2%9C%93&q=is%3Aissue%20milestone%3A4.3%20>`__
and `pull requests <https://github.com/jupyter/notebook/pulls?utf8=%E2%9C%93&q=is%3Apr%20milestone%3A4.3%20>`__ involved in this release.
.. _release-4.2.3:
4.2.3

@ -97,7 +97,7 @@ class LoginHandler(IPythonHandler):
auth_header_pat = re.compile('token\s+(.+)', re.IGNORECASE)
@classmethod
def get_user_token(cls, handler):
def get_token(cls, handler):
"""Get the user token from a request
Default:
@ -117,14 +117,29 @@ class LoginHandler(IPythonHandler):
@classmethod
def should_check_origin(cls, handler):
"""Should the Handler check for CORS origin validation?
Origin check should be skipped for token-authenticated requests.
Returns:
- True, if Handler must check for valid CORS origin.
- False, if Handler should skip origin check since requests are token-authenticated.
"""
return not cls.is_token_authenticated(handler)
@classmethod
def is_token_authenticated(cls, handler):
"""Returns True if handler has been token authenticated. Otherwise, False.
Login with a token is used to signal certain things, such as:
- permit access to REST API
- xsrf protection
- skip origin-checks for scripts
"""
if getattr(handler, '_user_id', None) is None:
# ensure get_user has been called, so we know if we're token-authenticated
handler.get_current_user()
token_authenticated = getattr(handler, '_token_authenticated', False)
return not token_authenticated
return getattr(handler, '_token_authenticated', False)
@classmethod
def get_user(cls, handler):
@ -136,40 +151,56 @@ class LoginHandler(IPythonHandler):
# called on LoginHandler itself.
if getattr(handler, '_user_id', None):
return handler._user_id
user_id = handler.get_secure_cookie(handler.cookie_name)
if not user_id:
user_id = cls.get_user_token(handler)
if user_id is None:
user_id = handler.get_secure_cookie(handler.cookie_name)
else:
cls.set_login_cookie(handler, user_id)
# Record that the current request has been authenticated with a token.
# Used in is_token_authenticated above.
handler._token_authenticated = True
if user_id is None:
# prevent extra Invalid cookie sig warnings:
handler.clear_login_cookie()
token = handler.token
if not token and not handler.login_available:
if not handler.login_available:
# Completely insecure! No authentication at all.
# No need to warn here, though; validate_security will have already done that.
return 'anonymous'
if token:
# check login token from URL argument or Authorization header
user_token = cls.get_user_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.info("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
if authenticated:
user_id = uuid.uuid4().hex
cls.set_login_cookie(handler, user_id)
# Record that we've been authenticated with a token.
# Used in should_check_origin above.
handler._token_authenticated = True
user_id = 'anonymous'
# cache value for future retrievals on the same request
handler._user_id = user_id
return user_id
@classmethod
def get_user_token(cls, handler):
"""Identify the user based on a token in the URL or Authorization header
Returns:
- uuid if authenticated
- None if not
"""
token = handler.token
if not token:
return
# check login token from URL argument or Authorization header
user_token = cls.get_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
if authenticated:
return uuid.uuid4().hex
else:
return None
@classmethod
def validate_security(cls, app, ssl_options=None):

@ -48,7 +48,7 @@ def log():
class AuthenticatedHandler(web.RequestHandler):
"""A RequestHandler with an authenticated user."""
@property
def content_security_policy(self):
"""The default Content-Security-Policy header
@ -95,6 +95,13 @@ class AuthenticatedHandler(web.RequestHandler):
return False
return not self.login_handler.should_check_origin(self)
@property
def token_authenticated(self):
"""Have I been authenticated with a token?"""
if self.login_handler is None or not hasattr(self.login_handler, 'is_token_authenticated'):
return False
return self.login_handler.is_token_authenticated(self)
@property
def cookie_name(self):
default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
@ -288,8 +295,12 @@ class IPythonHandler(AuthenticatedHandler):
host = self.request.headers.get("Host")
origin = self.request.headers.get("Origin")
# If no header is provided, assume it comes from a script/curl.
# We are only concerned with cross-site browser stuff here.
# If no header is provided, let the request through.
# Origin can be None for:
# - same-origin (IE, Firefox)
# - Cross-site POST form (IE, Firefox)
# - Scripts
# The cross-site POST (XSRF) case is handled by tornado's xsrf_token
if origin is None or host is None:
return True
@ -313,7 +324,15 @@ class IPythonHandler(AuthenticatedHandler):
self.request.path, origin, host,
)
return allow
def check_xsrf_cookie(self):
"""Bypass xsrf cookie checks when token-authenticated"""
if self.token_authenticated or self.settings.get('disable_check_xsrf', False):
# Token-authenticated requests do not need additional XSRF-check
# Servers without authentication are vulnerable to XSRF
return
return super(IPythonHandler, self).check_xsrf_cookie()
#---------------------------------------------------------------
# template rendering
#---------------------------------------------------------------
@ -340,6 +359,10 @@ class IPythonHandler(AuthenticatedHandler):
sys_info=sys_info,
contents_js_source=self.contents_js_source,
version_hash=self.version_hash,
ignore_minified_js=self.ignore_minified_js,
xsrf_form_html=self.xsrf_form_html,
token=self.token,
xsrf_token=self.xsrf_token.decode('utf8'),
**self.jinja_template_vars
)

@ -4,10 +4,8 @@
# Distributed under the terms of the Modified BSD License.
import io
import requests
from os.path import join as pjoin
from notebook.utils import url_path_join
from notebook.tests.launchnotebook import NotebookTestBase
from nbformat import write
from nbformat.v4 import (
@ -45,20 +43,20 @@ class BundleAPITest(NotebookTestBase):
def test_missing_bundler_arg(self):
"""Should respond with 400 error about missing bundler arg"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'fake.ipynb'))
resp = self.request('GET', 'bundle/fake.ipynb')
self.assertEqual(resp.status_code, 400)
self.assertIn('Missing argument bundler', resp.text)
def test_notebook_not_found(self):
"""Shoudl respond with 404 error about missing notebook"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'fake.ipynb'),
resp = self.request('GET', 'bundle/fake.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 404)
self.assertIn('Not Found', resp.text)
def test_bundler_not_enabled(self):
"""Should respond with 400 error about disabled bundler"""
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 400)
self.assertIn('Bundler fake_bundler not enabled', resp.text)
@ -67,7 +65,7 @@ class BundleAPITest(NotebookTestBase):
"""Should respond with 500 error about failure to load bundler module"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'fake_module'}
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
mock.assert_called_with('fake_bundler')
self.assertEqual(resp.status_code, 500)
@ -77,7 +75,7 @@ class BundleAPITest(NotebookTestBase):
"""Should respond with 200 and output from test bundler stub"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'notebook.bundler.tests.test_bundler_api'}
resp = requests.get(url_path_join(self.base_url(), 'bundle', 'testnb.ipynb'),
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'stub_bundler'})
mock.assert_called_with('stub_bundler')
self.assertEqual(resp.status_code, 200)

@ -26,12 +26,12 @@ except ImportError: #PY2
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'nbconvert', path),
response = self.request(verb,
url_path_join('nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -84,7 +84,7 @@ class APITest(NotebookTestBase):
encoding='utf-8') as f:
write(nb, f, version=4)
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
@onlyif_cmds_exist('pandoc')
def test_from_file(self):
@ -118,8 +118,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
self.assertEqual(r.status_code, 200)
@ -133,8 +132,7 @@ class APITest(NotebookTestBase):
@onlyif_cmds_exist('pandoc')
def test_from_post_zip(self):
nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
nbmodel = requests.get(nbmodel_url).json()
nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json()
r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
self.assertIn(u'application/zip', r.headers['Content-Type'])

@ -225,6 +225,8 @@ class NotebookWebApplication(web.Application):
login_handler_class=jupyter_app.login_handler_class,
logout_handler_class=jupyter_app.logout_handler_class,
password=jupyter_app.password,
xsrf_cookies=True,
disable_check_xsrf=jupyter_app.disable_check_xsrf,
# managers
kernel_manager=kernel_manager,
@ -631,6 +633,22 @@ class NotebookApp(JupyterApp):
"""
)
disable_check_xsrf = Bool(False, config=True,
help="""Disable cross-site-request-forgery protection
Jupyter notebook 4.3.1 introduces protection from cross-site request forgeries,
requiring API requests to either:
- originate from pages served by this server (validated with XSRF cookie and token), or
- authenticate with a token
Some anonymous compute resources still desire the ability to run code,
completely without authentication.
These services can disable all authentication and security checks,
with the full knowledge of what that implies.
"""
)
open_browser = Bool(True, config=True,
help="""Whether to open in a browser after starting.
The specific browser used is platform dependent and

@ -11,12 +11,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class ConfigAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, section, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/config', section),
response = self.request(verb,
url_path_join('api/config', section),
data=body,
)
response.raise_for_status()
@ -34,7 +34,7 @@ class ConfigAPI(object):
class APITest(NotebookTestBase):
"""Test the config web service API"""
def setUp(self):
self.config_api = ConfigAPI(self.base_url())
self.config_api = ConfigAPI(self.request)
def test_create_retrieve_config(self):
sample = {'foo': 'bar', 'baz': 73}

@ -50,12 +50,12 @@ def dirs_only(dir_model):
class API(object):
"""Wrapper for contents API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/contents', path),
response = self.request(verb,
url_path_join('api/contents', path),
data=body, params=params,
)
response.raise_for_status()
@ -220,7 +220,7 @@ class APITest(NotebookTestBase):
self.make_blob(blobname, blob)
self.addCleanup(partial(self.delete_file, blobname))
self.api = API(self.base_url())
self.api = API(self.request)
def test_list_notebooks(self):
nbs = notebooks_only(self.api.list().json())

@ -10,12 +10,12 @@ from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
class KernelAPI(object):
"""Wrapper for kernel REST API requests"""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/kernels', path), data=body)
response = self.request(verb,
url_path_join('api/kernels', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -48,7 +48,7 @@ class KernelAPI(object):
class KernelAPITest(NotebookTestBase):
"""Test the kernels web service API"""
def setUp(self):
self.kern_api = KernelAPI(self.base_url())
self.kern_api = KernelAPI(self.request)
def tearDown(self):
for k in self.kern_api.list().json():

@ -26,12 +26,12 @@ some_resource = u"The very model of a modern major general"
class KernelSpecAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, path),
response = self.request(verb,
path,
data=body,
)
response.raise_for_status()
@ -52,7 +52,7 @@ class APITest(NotebookTestBase):
def setUp(self):
self.create_spec('sample')
self.create_spec('sample 2')
self.ks_api = KernelSpecAPI(self.base_url())
self.ks_api = KernelSpecAPI(self.request)
def create_spec(self, name):
sample_kernel_dir = pjoin(self.data_dir.name, 'kernels', name)

@ -5,12 +5,12 @@ from notebook.tests.launchnotebook import NotebookTestBase
class NbconvertAPI(object):
"""Wrapper for nbconvert API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None, params=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/nbconvert', path),
response = self.request(verb,
url_path_join('api/nbconvert', path),
data=body, params=params,
)
response.raise_for_status()
@ -21,7 +21,7 @@ class NbconvertAPI(object):
class APITest(NotebookTestBase):
def setUp(self):
self.nbconvert_api = NbconvertAPI(self.base_url())
self.nbconvert_api = NbconvertAPI(self.request)
def test_list_formats(self):
formats = self.nbconvert_api.list_formats().json()

@ -18,12 +18,12 @@ from nbformat import write
class SessionAPI(object):
"""Wrapper for notebook API calls."""
def __init__(self, base_url):
self.base_url = base_url
def __init__(self, request):
self.request = request
def _req(self, verb, path, body=None):
response = requests.request(verb,
url_path_join(self.base_url, 'api/sessions', path), data=body)
response = self.request(verb,
url_path_join('api/sessions', path), data=body)
if 400 <= response.status_code < 600:
try:
@ -95,7 +95,7 @@ class SessionAPITest(NotebookTestBase):
nb = new_notebook()
write(nb, f, version=4)
self.sess_api = SessionAPI(self.base_url())
self.sess_api = SessionAPI(self.request)
@self.addCleanup
def cleanup_sessions():
@ -154,7 +154,7 @@ class SessionAPITest(NotebookTestBase):
def test_create_with_kernel_id(self):
# create a new kernel
r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
@ -222,7 +222,7 @@ class SessionAPITest(NotebookTestBase):
self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
self.assertEqual(kernel_list, [after['kernel']])
@ -232,7 +232,7 @@ class SessionAPITest(NotebookTestBase):
sid = before['id']
# create a new kernel
r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('POST', 'api/kernels')
r.raise_for_status()
kernel = r.json()
@ -245,7 +245,7 @@ class SessionAPITest(NotebookTestBase):
self.assertEqual(after['kernel']['id'], kernel['id'])
# check kernel list, to be sure previous kernel was cleaned up
r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
r = self.request('GET', 'api/kernels')
r.raise_for_status()
kernel_list = r.json()
self.assertEqual(kernel_list, [kernel])

@ -603,7 +603,7 @@ define([
var to_absolute_cursor_pos = function (cm, cursor) {
console.warn('`utils.to_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.indexFromPos(cursor)`');
return cm.indexFromPos(cusrsor);
return cm.indexFromPos(cursor);
};
var from_absolute_cursor_pos = function (cm, cursor_pos) {
@ -752,6 +752,35 @@ define([
return wrapped_error;
};
var ajax = function (url, settings) {
// like $.ajax, but ensure Authorization header is set
settings = _add_auth_header(settings);
return $.ajax(url, settings);
};
var _get_cookie = function (name) {
// from tornado docs: http://www.tornadoweb.org/en/stable/guide/security.html
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var _add_auth_header = function (settings) {
/**
* Adds auth header to jquery ajax settings
*/
settings = settings || {};
if (!settings.headers) {
settings.headers = {};
}
if (!settings.headers.Authorization) {
var xsrf_token = _get_cookie('_xsrf');
if (xsrf_token) {
settings.headers['X-XSRFToken'] = xsrf_token;
}
}
return settings;
};
var promising_ajax = function(url, settings) {
/**
* Like $.ajax, but returning an ES6 promise. success and error settings
@ -766,7 +795,7 @@ define([
log_ajax_error(jqXHR, status, error);
reject(wrap_ajax_error(jqXHR, status, error));
};
$.ajax(url, settings);
ajax(url, settings);
});
};
@ -1010,10 +1039,11 @@ define([
is_or_has : is_or_has,
is_focused : is_focused,
mergeopt: mergeopt,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
requireCodeMirrorMode : requireCodeMirrorMode,
XHR_ERROR : XHR_ERROR,
ajax : ajax,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
wrap_ajax_error : wrap_ajax_error,
promising_ajax : promising_ajax,
WrappedError: WrappedError,

@ -152,7 +152,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Kernel.prototype.list = function (success, error) {
$.ajax(this.kernel_service_url, {
utils.ajax(this.kernel_service_url, {
processData: false,
cache: false,
type: "GET",
@ -194,7 +194,7 @@ define([
}
};
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",
@ -218,7 +218,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Kernel.prototype.get_info = function (success, error) {
$.ajax(this.kernel_url, {
utils.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "GET",
@ -244,7 +244,7 @@ define([
Kernel.prototype.kill = function (success, error) {
this.events.trigger('kernel_killed.Kernel', {kernel: this});
this._kernel_dead();
$.ajax(this.kernel_url, {
utils.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "DELETE",
@ -278,7 +278,7 @@ define([
};
var url = utils.url_path_join(this.kernel_url, 'interrupt');
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",
@ -323,7 +323,7 @@ define([
};
var url = utils.url_path_join(this.kernel_url, 'restart');
$.ajax(url, {
utils.ajax(url, {
processData: false,
cache: false,
type: "POST",

@ -76,7 +76,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.list = function (success, error) {
$.ajax(this.session_service_url, {
utils.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "GET",
@ -117,7 +117,7 @@ define([
}
};
$.ajax(this.session_service_url, {
utils.ajax(this.session_service_url, {
processData: false,
cache: false,
type: "POST",
@ -139,7 +139,7 @@ define([
* @param {function} [error] - functon executed on ajax error
*/
Session.prototype.get_info = function (success, error) {
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "GET",
@ -165,7 +165,7 @@ define([
this.notebook_model.path = path;
}
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "PATCH",
@ -192,7 +192,7 @@ define([
this.kernel._kernel_dead();
}
$.ajax(this.session_url, {
utils.ajax(this.session_url, {
processData: false,
cache: false,
type: "DELETE",

@ -734,7 +734,7 @@ define([
'api/sessions',
encodeURIComponent(session.id)
);
$.ajax(url, settings);
utils.ajax(url, settings);
}
};

@ -62,7 +62,7 @@ define([
error : utils.log_ajax_error,
};
var url = utils.url_path_join(this.base_url, 'api/sessions');
$.ajax(url, settings);
utils.ajax(url, settings);
};
SesssionList.prototype.sessions_loaded = function(data){

@ -60,12 +60,12 @@ define([
this.base_url,
'api/terminals'
);
$.ajax(url, settings);
utils.ajax(url, settings);
};
TerminalList.prototype.load_terminals = function() {
var url = utils.url_path_join(this.base_url, 'api/terminals');
$.ajax(url, {
utils.ajax(url, {
type: "GET",
cache: false,
dataType: "json",
@ -113,7 +113,7 @@ define([
};
var url = utils.url_path_join(that.base_url, 'api/terminals',
utils.encode_uri_components(name));
$.ajax(url, settings);
utils.ajax(url, settings);
return false;
});
item.find(".item_buttons").text("").append(shutdown_button);

@ -22,6 +22,7 @@
<div class="center-nav">
<p class="navbar-text nav">Password{% if token_available %} or token{% endif %}:</p>
<form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
{{ xsrf_form_html() | safe }}
<input type="password" name="password" id="password_input" class="form-control">
<button type="submit" id="login_submit">Log in</button>
</form>

@ -197,7 +197,13 @@
</head>
<body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
<body class="{% block bodyclasses %}{% endblock %}"
{% block params %}
{% if logged_in and token %}
data-jupyter-api-token="{{token | urlencode}}"
{% endif %}
{% endblock params %}
>
<noscript>
<div id='noscript'>

@ -2,12 +2,12 @@
from __future__ import print_function
from binascii import hexlify
from contextlib import contextmanager
import os
import sys
import time
import requests
from contextlib import contextmanager
from threading import Thread, Event
import time
from unittest import TestCase
pjoin = os.path.join
@ -17,12 +17,14 @@ try:
except ImportError:
from mock import patch #py2
import requests
from tornado.ioloop import IOLoop
import zmq
import jupyter_core.paths
from traitlets.config import Config
from ..notebookapp import NotebookApp
from ..utils import url_path_join
from ipython_genutils.tempdir import TemporaryDirectory
MAX_WAITTIME = 30 # seconds to wait for notebook server to start
@ -68,6 +70,20 @@ class NotebookTestBase(TestCase):
cls.notebook_thread.join(timeout=MAX_WAITTIME)
if cls.notebook_thread.is_alive():
raise TimeoutError("Undead notebook server")
@classmethod
def request(self, verb, path, **kwargs):
"""Send a request to my server
with authentication and everything.
"""
headers = kwargs.setdefault('headers', {})
# kwargs.setdefault('allow_redirects', False)
headers.setdefault('Authorization', 'token %s' % self.token)
response = requests.request(verb,
url_path_join(self.base_url(), path),
**kwargs)
return response
@classmethod
def setup_class(cls):
@ -86,9 +102,12 @@ class NotebookTestBase(TestCase):
cls.data_dir = data_dir
cls.runtime_dir = TemporaryDirectory()
cls.notebook_dir = TemporaryDirectory()
config = cls.config or Config()
config.NotebookNotary.db_file = ':memory:'
cls.token = hexlify(os.urandom(4)).decode('ascii')
started = Event()
def start_thread():
app = cls.notebook = NotebookApp(
@ -102,7 +121,7 @@ class NotebookTestBase(TestCase):
base_url=cls.url_prefix,
config=config,
allow_root=True,
token='',
token=cls.token,
)
# don't register signal handler during tests
app.init_signal = lambda : None

@ -41,27 +41,25 @@ class FilesTest(NotebookTestBase):
f.write('foo')
with open(pjoin(path, '.foo'), 'w') as f:
f.write('.foo')
url = self.base_url()
for d in not_hidden:
path = pjoin(nbdir, d.replace('/', os.sep))
r = requests.get(url_path_join(url, 'files', d, 'foo'))
r = self.request('GET', url_path_join('files', d, 'foo'))
r.raise_for_status()
self.assertEqual(r.text, 'foo')
r = requests.get(url_path_join(url, 'files', d, '.foo'))
r = self.request('GET', url_path_join('files', d, '.foo'))
self.assertEqual(r.status_code, 404)
for d in hidden:
path = pjoin(nbdir, d.replace('/', os.sep))
for foo in ('foo', '.foo'):
r = requests.get(url_path_join(url, 'files', d, foo))
r = self.request('GET', url_path_join('files', d, foo))
self.assertEqual(r.status_code, 404)
def test_contents_manager(self):
"make sure ContentsManager returns right files (ipynb, bin, txt)."
nbdir = self.notebook_dir.name
base = self.base_url()
nb = new_notebook(
cells=[
@ -84,35 +82,34 @@ class FilesTest(NotebookTestBase):
f.write(u'foobar')
f.close()
r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
r = self.request('GET', 'files/testnb.ipynb')
self.assertEqual(r.status_code, 200)
self.assertIn('print(2*6)', r.text)
json.loads(r.text)
r = requests.get(url_path_join(base, 'files', 'test.bin'))
r = self.request('GET', 'files/test.bin')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers['content-type'], 'application/octet-stream')
self.assertEqual(r.content[:1], b'\xff')
self.assertEqual(len(r.content), 6)
r = requests.get(url_path_join(base, 'files', 'test.txt'))
r = self.request('GET', 'files/test.txt')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers['content-type'], 'text/plain')
self.assertEqual(r.text, 'foobar')
def test_download(self):
nbdir = self.notebook_dir.name
base = self.base_url()
text = 'hello'
with open(pjoin(nbdir, 'test.txt'), 'w') as f:
f.write(text)
r = requests.get(url_path_join(base, 'files', 'test.txt'))
r = self.request('GET', 'files/test.txt')
disposition = r.headers.get('Content-Disposition', '')
self.assertNotIn('attachment', disposition)
r = requests.get(url_path_join(base, 'files', 'test.txt') + '?download=1')
r = self.request('GET', 'files/test.txt?download=1')
disposition = r.headers.get('Content-Disposition', '')
self.assertIn('attachment', disposition)
self.assertIn('filename="test.txt"', disposition)
@ -120,7 +117,6 @@ class FilesTest(NotebookTestBase):
def test_old_files_redirect(self):
"""pre-2.0 'files/' prefixed links are properly redirected"""
nbdir = self.notebook_dir.name
base = self.base_url()
os.mkdir(pjoin(nbdir, 'files'))
os.makedirs(pjoin(nbdir, 'sub', 'files'))
@ -134,19 +130,19 @@ class FilesTest(NotebookTestBase):
f.write(prefix + '/f2')
with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
f.write(prefix + '/f3')
url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
r = requests.get(url)
url = url_path_join('notebooks', prefix, 'files', 'f1.txt')
r = self.request('GET', url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, prefix + '/files/f1')
url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
r = requests.get(url)
url = url_path_join('notebooks', prefix, 'files', 'f2.txt')
r = self.request('GET', url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, prefix + '/files/f2')
url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
r = requests.get(url)
url = url_path_join('notebooks', prefix, 'files', 'f3.txt')
r = self.request('GET', url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, prefix + '/f3')

@ -25,8 +25,8 @@ class TreeTest(NotebookTestBase):
self.base_url()
def test_redirect(self):
r = requests.get(url_path_join(self.base_url(), 'tree/foo/bar.ipynb'))
r = self.request('GET', 'tree/foo/bar.ipynb')
self.assertEqual(r.url, self.base_url() + 'notebooks/foo/bar.ipynb')
r = requests.get(url_path_join(self.base_url(), 'tree/foo/baz.txt'))
r = self.request('GET', 'tree/foo/baz.txt')
self.assertEqual(r.url, url_path_join(self.base_url(), 'files/foo/baz.txt'))

Loading…
Cancel
Save