Skip to content

Allow proxying to remote host #154

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

Merged
merged 12 commits into from
Nov 15, 2019
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.pyc
*.egg-info/
docs/_build
node_modules
13 changes: 9 additions & 4 deletions docs/arbitrary-ports.rst → docs/arbitrary-ports-hosts.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.. _arbitrary-ports:

=========================
Accessing Arbitrary Ports
=========================
==================================
Accessing Arbitrary Ports or Hosts
==================================

If you already have a server running on localhost listening on
a port, you can access it through the notebook at
Expand All @@ -15,6 +15,11 @@ URL in the request.

This works for all ports listening on the local machine.

You can also specify arbitrary hosts in order to proxy traffic from
another machine on the network ``<notebook-base>/proxy/<host>:<port>``.

For security reasons the host must match an entry in the whitelist in your configuration.

With JupyterHub
===============

Expand All @@ -38,7 +43,7 @@ Without JupyterHub
==================

A very similar set up works when you don't use JupyterHub. You
can construct the URL with ``<notebook-url>/proxy/<port>``.
can construct the URL with ``<notebook-url>/proxy/<port>``.

If your notebook url is ``http://localhost:8888`` and you have
a process running listening on port 8080, you can access it with
Expand Down
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The primary use cases are:
#. Allow access from frontend javascript (in classic notebook or
JupyterLab extensions) to access web APIs of other processes
running locally in a safe manner. This is used by the `JupyterLab
extension <https://github.com/dask/dask-labextension>`_ for
extension <https://github.com/dask/dask-labextension>`_ for
`dask <https://dask.org/>`_.


Expand All @@ -33,7 +33,7 @@ Contents
install
server-process
launchers
arbitrary-ports
arbitrary-ports-hosts


Convenience packages for popular applications
Expand Down
4 changes: 2 additions & 2 deletions jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def load_jupyter_server_extension(nbapp):
nbapp.web_app.add_handlers('.*', server_handlers)

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

launcher_entries = []
icons = {}
Expand All @@ -40,4 +40,4 @@ def load_jupyter_server_extension(nbapp):
nbapp.web_app.add_handlers('.*', [
(ujoin(base_url, 'server-proxy/servers-info'), ServersInfoHandler, {'server_processes': server_proccesses}),
(ujoin(base_url, 'server-proxy/icon/(.*)'), IconHandler, {'icons': icons})
])
])
24 changes: 23 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
from traitlets import Any, Dict
from traitlets.config import Configurable
from .handlers import SuperviseAndProxyHandler, AddSlashHandler
import pkg_resources
Expand Down Expand Up @@ -162,3 +162,25 @@ class ServerProxy(Configurable):
""",
config=True
)

host_whitelist_hook = Any(
lambda handler, host: host in ['localhost', '127.0.0.1'],
help="""
Verify that a host should be proxied.

This should be a callable that checks whether a host should be proxied
and returns True if so (False otherwise). It could be a very simple
check that the host is present in a list of allowed hosts, or it could
be a more complex verification against a regular expression. It should
probably not be a slow check against an external service. Here is an
example that could be placed in a site-wide Jupyter notebook config:

def hook(handler, host):
handler.log.info("Request to proxy to host " + host)
return host.startswith("10.")
c.ServerProxy.host_whitelist_hook = hook

The default check is to return True if the host is localhost.
""",
config=True
)
59 changes: 58 additions & 1 deletion jupyter_server_proxy/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class ProxyHandler(WebSocketHandlerMixin, IPythonHandler):
def __init__(self, *args, **kwargs):
self.proxy_base = ''
self.absolute_url = kwargs.pop('absolute_url', False)
self.host_whitelist_hook = kwargs.pop('host_whitelist_hook',
lambda handler, host: host in ['localhost', '127.0.0.1'])
super().__init__(*args, **kwargs)

# Support all the methods that torando does by default except for GET which
Expand Down Expand Up @@ -166,6 +168,9 @@ def _build_proxy_request(self, host, port, proxied_path, body):
headers=headers, **self.proxy_request_options())
return req

def _check_host_whitelist(self, host):
return self.host_whitelist_hook(self, host)

@web.authenticated
async def proxy(self, host, port, proxied_path):
'''
Expand All @@ -175,6 +180,12 @@ async def proxy(self, host, port, proxied_path):
{base_url}/{proxy_base}/{proxied_path}
'''

if not self._check_host_whitelist(host):
self.set_status(403)
self.write("Host '{host}' is not whitelisted. "
"See https://jupyter-server-proxy.readthedocs.io/en/latest/arbitrary-ports-hosts.html for info.".format(host=host))
return

if 'Proxy-Connection' in self.request.headers:
del self.request.headers['Proxy-Connection']

Expand Down Expand Up @@ -226,6 +237,14 @@ async def proxy_open(self, host, port, proxied_path=''):
We establish a websocket connection to the proxied backend &
set up a callback to relay messages through.
"""

if not self._check_host_whitelist(host):
self.set_status(403)
self.log.info("Host '{host}' is not whitelisted. "
"See https://jupyter-server-proxy.readthedocs.io/en/latest/arbitrary-ports-hosts.html for info.".format(host=host))
self.close()
return

if not proxied_path.startswith('/'):
proxied_path = '/' + proxied_path

Expand Down Expand Up @@ -334,6 +353,40 @@ def proxy(self, port, proxied_path):
return super().proxy('localhost', port, proxied_path)


class RemoteProxyHandler(ProxyHandler):
"""
A tornado request handler that proxies HTTP and websockets
from a port on a specified remote system.
"""

async def http_get(self, host, port, proxied_path):
return await self.proxy(host, port, proxied_path)

def post(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

def put(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

def delete(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

def head(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

def patch(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

def options(self, host, port, proxied_path):
return self.proxy(host, port, proxied_path)

async def open(self, host, port, proxied_path):
return await self.proxy_open(host, port, proxied_path)

def proxy(self, host, port, proxied_path):
return super().proxy(host, port, proxied_path)


# FIXME: Move this to its own file. Too many packages now import this from nbrserverproxy.handlers
class SuperviseAndProxyHandler(LocalProxyHandler):
'''Manage a given process and requests to it '''
Expand Down Expand Up @@ -467,9 +520,13 @@ def options(self, path):
return self.proxy(self.port, path)


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