Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable SSL on forwarded requests #169

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
15 changes: 14 additions & 1 deletion jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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 @@ -28,8 +30,19 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we setup the context like this, do we also get all the "normal" CAs or only the one in the cafile?

"Normal" CAs in this case would be those that your OS trusts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=serverproxy.cafile)
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=serverproxy.cafile or None)

The default value of serverproxy.cafile is "". If this is handled as None then it should use the system CA:

cafile, capath, cadata represent optional CA certificates to trust for certificate verification, as in SSLContext.load_verify_locations(). If all three are None, this function can choose to trust the system’s default CA certificates instead.
https://docs.python.org/3.6/library/ssl.html#ssl.create_default_context

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)
setup_handlers(nbapp.web_app, serverproxy.host_whitelist, ssl_options)

launcher_entries = []
icons = {}
Expand Down
50 changes: 49 additions & 1 deletion jupyter_server_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Traitlets based configuration for jupyter_server_proxy
"""
from notebook.utils import url_path_join as ujoin
from traitlets import Dict, List, Union, default
from traitlets import Bool, Dict, List, Unicode, Union, default
from traitlets.config import Configurable
from .handlers import SuperviseAndProxyHandler, AddSlashHandler
import pkg_resources
Expand Down Expand Up @@ -203,3 +203,51 @@ def host_whitelist(handler, host):
@default("host_whitelist")
def _host_whitelist_default(self):
return ["localhost", "127.0.0.1"]

keyfile = Unicode(
"",
help="""
Path to optional SSL key.

Use with `https=True` and `certfile`.
""",
config=True
)

certfile = Unicode(
"",
help="""
Path to optional SSL cert.

Use with `https=True` and `keyfile`.
""",
config=True
)

cafile = Unicode(
"",
help="""
Path to optional CA file.

Use with `https=True`.
""",
config=True
)

https = Bool(
False,
help="""
Whether to use SSL for forwarded client requests.

If this is set to `True` then you should provide a path to an SSL key,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If this is set to `True` then you should provide a path to an SSL key,
If this is set to `True` then you can optionally provide a path to an SSL key,

cert, and CA. Use this if the proxied service expects to service
requests over SSL.
""",
config=True
)
Comment on lines +207 to +247

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to get all these SSL informations from mybinder (which uses SSL)?


check_hostname = Bool(
False,
help="Whether to check hostname.",
config=True
)
28 changes: 20 additions & 8 deletions jupyter_server_proxy/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self, *args, **kwargs):
self.absolute_url = kwargs.pop('absolute_url', False)
self.host_whitelist = kwargs.pop('host_whitelist', ['localhost', '127.0.0.1'])
self.subprotocols = None
self.ssl_options = kwargs.pop('ssl_options', None)
super().__init__(*args, **kwargs)

# Support all the methods that tornado does by default except for GET which
Expand Down Expand Up @@ -165,7 +166,8 @@ def _build_proxy_request(self, host, port, proxied_path, body):

headers = self.proxy_request_headers()

client_uri = self.get_client_uri('http', host, port, proxied_path)
protocol = 'http' if self.ssl_options is None else 'https'
client_uri = self.get_client_uri(protocol, host, port, proxied_path)
# Some applications check X-Forwarded-Context and X-ProxyContextPath
# headers to see if and where they are being proxied from.
if not self.absolute_url:
Expand Down Expand Up @@ -276,7 +278,8 @@ async def proxy_open(self, host, port, proxied_path=''):
if not proxied_path.startswith('/'):
proxied_path = '/' + proxied_path

client_uri = self.get_client_uri('ws', host, port, proxied_path)
protocol = 'ws' if self.ssl_options is None else 'wss'
client_uri = self.get_client_uri(protocol, host, port, proxied_path)
headers = self.request.headers
current_loop = ioloop.IOLoop.current()
ws_connected = current_loop.asyncio_loop.create_future()
Expand Down Expand Up @@ -307,7 +310,8 @@ def ping_cb(data):
async def start_websocket_connection():
self.log.info('Trying to establish websocket connection to {}'.format(client_uri))
self._record_activity()
request = httpclient.HTTPRequest(url=client_uri, headers=headers)
request = httpclient.HTTPRequest(url=client_uri, headers=headers,
ssl_options=self.ssl_options)
self.ws = await pingable_ws_connect(request=request,
on_message_callback=message_cb, on_ping_callback=ping_cb,
subprotocols=self.subprotocols)
Expand All @@ -330,7 +334,11 @@ def proxy_request_headers(self):
def proxy_request_options(self):
'''A dictionary of options to be used when constructing
a tornado.httpclient.HTTPRequest instance for the proxy request.'''
<<<<<<< HEAD

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rcthomas this might be your culprit, pesky git 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh bother, thought I got them all...

return dict(follow_redirects=False, connect_timeout=250.0, request_timeout=300.0)
=======
return dict(follow_redirects=False, ssl_options=self.ssl_options)
>>>>>>> Enable SSL on forwarded requests

def check_xsrf_cookie(self):
'''
Expand Down Expand Up @@ -556,17 +564,21 @@ def options(self, path):
return self.proxy(self.port, path)


def setup_handlers(web_app, host_whitelist):
def setup_handlers(web_app, host_whitelist, ssl_options):
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}),
RemoteProxyHandler, {'absolute_url': False, 'host_whitelist': host_whitelist,
'ssl_options': ssl_options}),
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(.*):(\d+)(.*)'),
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist': host_whitelist}),
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist': host_whitelist,
'ssl_options': ssl_options}),
(url_path_join(web_app.settings['base_url'], r'/proxy/(\d+)(.*)'),
LocalProxyHandler, {'absolute_url': False}),
LocalProxyHandler, {'absolute_url': False,
'ssl_options': ssl_options}),
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(\d+)(.*)'),
LocalProxyHandler, {'absolute_url': True}),
LocalProxyHandler, {'absolute_url': True,
'ssl_options': ssl_options}),
])

# vim: set et ts=4 sw=4: