Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ node_modules
dist
.vscode
package-lock.json
tests/resources/cert.pem
tests/resources/key.pem
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ install:
- pip3 install .
- pip3 install pytest
script:
- openssl req -x509 -newkey rsa:4096 -keyout ./tests/resources/key.pem -out ./tests/resources/cert.pem -days 365 -nodes -subj /CN=localhost
- JUPYTER_TOKEN=secret jupyter-notebook --config=./tests/resources/jupyter_server_config.py &
- sleep 5
- pytest -v
15 changes: 1 addition & 14 deletions jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import ssl

from .handlers import setup_handlers, SuperviseAndProxyHandler
from .config import ServerProxy, make_handlers, get_entrypoint_server_processes, make_server_process
from notebook.utils import url_path_join as ujoin
Expand Down Expand Up @@ -30,19 +28,8 @@ def load_jupyter_server_extension(nbapp):
server_handlers = make_handlers(base_url, server_proccesses)
nbapp.web_app.add_handlers('.*', server_handlers)

# Configure SSL support
ssl_options = None
if serverproxy.https:
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=serverproxy.cafile)
if serverproxy.certfile or serverproxy.keyfile:
ssl_context.load_cert_chain(serverproxy.certfile, serverproxy.keyfile or None)
else:
ssl_context.load_default_certs()
ssl_context.check_hostname = serverproxy.check_hostname
ssl_options = ssl_context

# Set up default handler
setup_handlers(nbapp.web_app, serverproxy.host_whitelist, ssl_options)
setup_handlers(nbapp.web_app, serverproxy.host_whitelist)

launcher_entries = []
icons = {}
Expand Down
24 changes: 22 additions & 2 deletions jupyter_server_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Traitlets based configuration for jupyter_server_proxy
"""
from notebook.utils import url_path_join as ujoin
import ssl
from traitlets import Bool, Dict, List, Unicode, Union, default
from traitlets.config import Configurable
from .handlers import SuperviseAndProxyHandler, AddSlashHandler
Expand All @@ -15,7 +16,7 @@
except ImportError:
from .utils import Callable

def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port, mappath):
def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port, mappath, ssl_options):
"""
Create a SuperviseAndProxyHandler subclass with given parameters
"""
Expand All @@ -28,6 +29,7 @@ def __init__(self, *args, **kwargs):
self.absolute_url = absolute_url
self.requested_port = port
self.mappath = mappath
self.ssl_options = ssl_options

@property
def process_args(self):
Expand Down Expand Up @@ -90,6 +92,7 @@ def make_handlers(base_url, server_processes):
sp.absolute_url,
sp.port,
sp.mappath,
sp.ssl_options,
)
handlers.append((
ujoin(base_url, sp.name, r'(.*)'), handler, dict(state={}),
Expand All @@ -101,7 +104,23 @@ def make_handlers(base_url, server_processes):

LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title'])
ServerProcess = namedtuple('ServerProcess', [
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'mappath', 'launcher_entry'])
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'mappath', 'ssl_options', 'launcher_entry'])


def _create_ssl_options(config):
# Configure SSL support
ssl_options = None
if config.get('https', False):
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=config.get('cafile'))
certfile = config.get('certfile')
keyfile = config.get('keyfile')
if certfile or keyfile:
ssl_context.load_cert_chain(certfile, keyfile)
else:
ssl_context.load_default_certs()
ssl_context.check_hostname = config.get('check_hostname', False)
ssl_options = ssl_context
return ssl_options

def make_server_process(name, server_process_config):
le = server_process_config.get('launcher_entry', {})
Expand All @@ -113,6 +132,7 @@ def make_server_process(name, server_process_config):
absolute_url=server_process_config.get('absolute_url', False),
port=server_process_config.get('port', 0),
mappath=server_process_config.get('mappath', {}),
ssl_options=_create_ssl_options(server_process_config),
launcher_entry=LauncherEntry(
enabled=le.get('enabled', True),
icon_path=le.get('icon_path'),
Expand Down
23 changes: 11 additions & 12 deletions jupyter_server_proxy/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,16 +471,19 @@ def get_timeout(self):
return 5

async def _http_ready_func(self, p):
url = 'http://localhost:{}'.format(self.port)
protocol = 'http' if self.ssl_options is None else 'https'
url = '{}://localhost:{}'.format(protocol, self.port)
async with aiohttp.ClientSession() as session:
try:
async with session.get(url) as resp:
# Disable ssl verification
async with session.get(url, ssl=False) as resp:
# We only care if we get back *any* response, not just 200
# If there's an error response, that can be shown directly to the user
self.log.debug('Got code {} back from {}'.format(resp.status, url))
return True
except aiohttp.ClientConnectionError:
except aiohttp.ClientConnectionError as err:
self.log.debug('Connection to {} refused'.format(url))
self.log.debug(err)
return False

async def ensure_process(self):
Expand Down Expand Up @@ -561,21 +564,17 @@ def options(self, path):
return self.proxy(self.port, path)


def setup_handlers(web_app, host_whitelist, ssl_options):
def setup_handlers(web_app, host_whitelist):
host_pattern = '.*$'
web_app.add_handlers('.*', [
(url_path_join(web_app.settings['base_url'], r'/proxy/(.*):(\d+)(.*)'),
RemoteProxyHandler, {'absolute_url': False, 'host_whitelist': host_whitelist,
'ssl_options': ssl_options}),
RemoteProxyHandler, {'absolute_url': False, 'host_whitelist': host_whitelist}),
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(.*):(\d+)(.*)'),
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist': host_whitelist,
'ssl_options': ssl_options}),
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist': host_whitelist}),
(url_path_join(web_app.settings['base_url'], r'/proxy/(\d+)(.*)'),
LocalProxyHandler, {'absolute_url': False,
'ssl_options': ssl_options}),
LocalProxyHandler, {'absolute_url': False}),
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(\d+)(.*)'),
LocalProxyHandler, {'absolute_url': True,
'ssl_options': ssl_options}),
LocalProxyHandler, {'absolute_url': True}),
])

# vim: set et ts=4 sw=4:
14 changes: 12 additions & 2 deletions tests/resources/httpinfo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from argparse import ArgumentParser
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl
import sys

class EchoRequestInfo(BaseHTTPRequestHandler):
Expand All @@ -11,7 +13,15 @@ def do_GET(self):


if __name__ == '__main__':
port = int(sys.argv[1])
server_address = ('', port)
parser = ArgumentParser()
parser.add_argument('port', type=int)
parser.add_argument('--keyfile')
parser.add_argument('--certfile')
args = parser.parse_args()
server_address = ('', args.port)
httpd = HTTPServer(server_address, EchoRequestInfo)
if args.keyfile or args.certfile:
httpd.socket = ssl.wrap_socket(
httpd.socket, server_side=True,
certfile=args.certfile, keyfile=args.keyfile)
httpd.serve_forever()
7 changes: 7 additions & 0 deletions tests/resources/jupyter_server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ def mappathf(path):
'command': ['python3', './tests/resources/httpinfo.py', '{port}'],
'mappath': mappathf,
},
'python-https' : {
'command': ['python3', './tests/resources/httpinfo.py', '--keyfile=./tests/resources/key.pem', '--certfile=./tests/resources/cert.pem', '{port}'],
'https': True,
# Self-signed cert, use certificate as ca
'cafile': './tests/resources/cert.pem',
'check_hostname': True,
},
'python-websocket' : {
'command': ['python3', './tests/resources/websocket.py', '--port={port}'],
}
Expand Down
9 changes: 9 additions & 0 deletions tests/test_proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ def test_server_proxy_mappath_callable(requestpath, expected):
assert 'X-Proxycontextpath: /python-http-mappathf\n' in s


def test_server_proxy_https():
r = request_get(PORT, '/python-https/abc', TOKEN)
assert r.code == 200
s = r.read().decode('ascii')
assert s.startswith('GET /abc?token=')
assert 'X-Forwarded-Context: /python-https\n' in s
assert 'X-Proxycontextpath: /python-https\n' in s


def test_server_proxy_remote():
r = request_get(PORT, '/newproxy', TOKEN, host='127.0.0.1')
assert r.code == 200
Expand Down