Skip to content

Commit 35dd072

Browse files
Merge pull request #2 from rcthomas/allow-remote-proxy
Change whitelist to allow for list or callable
2 parents d1feb45 + c110604 commit 35dd072

File tree

4 files changed

+54
-22
lines changed

4 files changed

+54
-22
lines changed

jupyter_server_proxy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def load_jupyter_server_extension(nbapp):
2929
nbapp.web_app.add_handlers('.*', server_handlers)
3030

3131
# Set up default handler
32-
setup_handlers(nbapp.web_app, serverproxy.host_whitelist_hook)
32+
setup_handlers(nbapp.web_app, serverproxy.host_whitelist)
3333

3434
launcher_entries = []
3535
icons = {}

jupyter_server_proxy/config.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
Traitlets based configuration for jupyter_server_proxy
33
"""
44
from notebook.utils import url_path_join as ujoin
5-
from traitlets import Any, Dict
5+
from traitlets import Dict, List, Union, default
66
from traitlets.config import Configurable
77
from .handlers import SuperviseAndProxyHandler, AddSlashHandler
88
import pkg_resources
99
from collections import namedtuple
1010
from .utils import call_with_asked_args
1111

12+
try:
13+
# Traitlets >= 4.3.3
14+
from traitlets import Callable
15+
except ImportError:
16+
from .utils import Callable
17+
1218
def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port):
1319
"""
1420
Create a SuperviseAndProxyHandler subclass with given parameters
@@ -163,24 +169,29 @@ class ServerProxy(Configurable):
163169
config=True
164170
)
165171

166-
host_whitelist_hook = Any(
167-
lambda handler, host: host in ['localhost', '127.0.0.1'],
172+
host_whitelist = Union(
173+
trait_types=[List(), Callable()],
168174
help="""
169-
Verify that a host should be proxied.
175+
List of allowed hosts.
176+
Can also be a function that decides whether a host can be proxied.
170177
171-
This should be a callable that checks whether a host should be proxied
172-
and returns True if so (False otherwise). It could be a very simple
173-
check that the host is present in a list of allowed hosts, or it could
174-
be a more complex verification against a regular expression. It should
175-
probably not be a slow check against an external service. Here is an
176-
example that could be placed in a site-wide Jupyter notebook config:
178+
If implemented as a function, this should return True if a host should
179+
be proxied and False if it should not. Such a function could verify
180+
that the host matches a particular regular expression pattern or falls
181+
into a specific subnet. It should probably not be a slow check against
182+
some external service. Here is an example that could be placed in a
183+
site-wide Jupyter notebook config:
177184
178-
def hook(handler, host):
185+
def host_whitelist(handler, host):
179186
handler.log.info("Request to proxy to host " + host)
180187
return host.startswith("10.")
181-
c.ServerProxy.host_whitelist_hook = hook
182-
183-
The default check is to return True if the host is localhost.
184-
""",
188+
c.ServerProxy.host_whitelist = host_whitelist
189+
190+
Defaults to a list of ["localhost", "127.0.0.1"].
191+
""",
185192
config=True
186193
)
194+
195+
@default("host_whitelist")
196+
def _host_whitelist_default(self):
197+
return ["localhost", "127.0.0.1"]

jupyter_server_proxy/handlers.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ class ProxyHandler(WebSocketHandlerMixin, IPythonHandler):
4242
def __init__(self, *args, **kwargs):
4343
self.proxy_base = ''
4444
self.absolute_url = kwargs.pop('absolute_url', False)
45-
self.host_whitelist_hook = kwargs.pop('host_whitelist_hook',
46-
lambda handler, host: host in ['localhost', '127.0.0.1'])
45+
self.host_whitelist = kwargs.pop('host_whitelist', ['localhost', '127.0.0.1'])
4746
super().__init__(*args, **kwargs)
4847

4948
# Support all the methods that torando does by default except for GET which
@@ -169,7 +168,10 @@ def _build_proxy_request(self, host, port, proxied_path, body):
169168
return req
170169

171170
def _check_host_whitelist(self, host):
172-
return self.host_whitelist_hook(self, host)
171+
if callable(self.host_whitelist):
172+
return self.host_whitelist(self, host)
173+
else:
174+
return host in self.host_whitelist
173175

174176
@web.authenticated
175177
async def proxy(self, host, port, proxied_path):
@@ -520,13 +522,13 @@ def options(self, path):
520522
return self.proxy(self.port, path)
521523

522524

523-
def setup_handlers(web_app, host_whitelist_hook):
525+
def setup_handlers(web_app, host_whitelist):
524526
host_pattern = '.*$'
525527
web_app.add_handlers('.*', [
526528
(url_path_join(web_app.settings['base_url'], r'/proxy/(.*):(\d+)(.*)'),
527-
RemoteProxyHandler, {'absolute_url': False, 'host_whitelist_hook': host_whitelist_hook}),
529+
RemoteProxyHandler, {'absolute_url': False, 'host_whitelist': host_whitelist}),
528530
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(.*):(\d+)(.*)'),
529-
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist_hook': host_whitelist_hook}),
531+
RemoteProxyHandler, {'absolute_url': True, 'host_whitelist': host_whitelist}),
530532
(url_path_join(web_app.settings['base_url'], r'/proxy/(\d+)(.*)'),
531533
LocalProxyHandler, {'absolute_url': False}),
532534
(url_path_join(web_app.settings['base_url'], r'/proxy/absolute/(\d+)(.*)'),

jupyter_server_proxy/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from traitlets import TraitType
2+
import six
3+
14
def call_with_asked_args(callback, args):
25
"""
36
Call callback with only the args it wants from args
@@ -29,3 +32,19 @@ def call_with_asked_args(callback, args):
2932
)
3033
)
3134
return callback(*asked_arg_values)
35+
36+
# copy-pasted from the master of Traitlets source
37+
class Callable(TraitType):
38+
"""A trait which is callable.
39+
Notes
40+
-----
41+
Classes are callable, as are instances
42+
with a __call__() method."""
43+
44+
info_text = 'a callable'
45+
46+
def validate(self, obj, value):
47+
if six.callable(value):
48+
return value
49+
else:
50+
self.error(obj, value)

0 commit comments

Comments
 (0)