Skip to content

Commit 0e67e1a

Browse files
authored
Merge pull request #151 from manics/indexpage
Optionally map requested → proxied paths
2 parents e9a132d + 1f22ccf commit 0e67e1a

File tree

5 files changed

+78
-4
lines changed

5 files changed

+78
-4
lines changed

docs/server-process.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ pairs.
9090
automatically select an unused port.
9191

9292

93+
#. **mappath**
94+
95+
Map request paths to proxied paths.
96+
Either a dictionary of request paths to proxied paths,
97+
or a callable that takes parameter ``path`` and returns the proxied path.
98+
99+
93100
#. **launcher_entry**
94101

95102
A dictionary with options on if / how an entry in the classic Jupyter Notebook

jupyter_server_proxy/config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from collections import namedtuple
1010
from .utils import call_with_asked_args
1111

12-
def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port):
12+
def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port, mappath):
1313
"""
1414
Create a SuperviseAndProxyHandler subclass with given parameters
1515
"""
@@ -21,6 +21,7 @@ def __init__(self, *args, **kwargs):
2121
self.proxy_base = name
2222
self.absolute_url = absolute_url
2323
self.requested_port = port
24+
self.mappath = mappath
2425

2526
@property
2627
def process_args(self):
@@ -82,6 +83,7 @@ def make_handlers(base_url, server_processes):
8283
sp.timeout,
8384
sp.absolute_url,
8485
sp.port,
86+
sp.mappath,
8587
)
8688
handlers.append((
8789
ujoin(base_url, sp.name, r'(.*)'), handler, dict(state={}),
@@ -93,7 +95,7 @@ def make_handlers(base_url, server_processes):
9395

9496
LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title'])
9597
ServerProcess = namedtuple('ServerProcess', [
96-
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'launcher_entry'])
98+
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'mappath', 'launcher_entry'])
9799

98100
def make_server_process(name, server_process_config):
99101
le = server_process_config.get('launcher_entry', {})
@@ -104,6 +106,7 @@ def make_server_process(name, server_process_config):
104106
timeout=server_process_config.get('timeout', 5),
105107
absolute_url=server_process_config.get('absolute_url', False),
106108
port=server_process_config.get('port', 0),
109+
mappath=server_process_config.get('mappath', {}),
107110
launcher_entry=LauncherEntry(
108111
enabled=le.get('enabled', True),
109112
icon_path=le.get('icon_path'),
@@ -144,6 +147,11 @@ class ServerProxy(Configurable):
144147
port
145148
Set the port that the service will listen on. The default is to automatically select an unused port.
146149
150+
mappath
151+
Map request paths to proxied paths.
152+
Either a dictionary of request paths to proxied paths,
153+
or a callable that takes parameter ``path`` and returns the proxied path.
154+
147155
launcher_entry
148156
A dictionary of various options for entries in classic notebook / jupyterlab launchers.
149157

jupyter_server_proxy/handlers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from notebook.utils import url_path_join
1717
from notebook.base.handlers import IPythonHandler, utcnow
1818

19+
from .utils import call_with_asked_args
1920
from .websocket import WebSocketHandlerMixin, pingable_ws_connect
2021
from simpervisor import SupervisedProcess
2122

@@ -44,7 +45,7 @@ def __init__(self, *args, **kwargs):
4445
self.absolute_url = kwargs.pop('absolute_url', False)
4546
super().__init__(*args, **kwargs)
4647

47-
# Support all the methods that torando does by default except for GET which
48+
# Support all the methods that tornado does by default except for GET which
4849
# is passed to WebSocketHandlerMixin and then to WebSocketHandler.
4950

5051
async def open(self, port, proxied_path):
@@ -340,6 +341,7 @@ class SuperviseAndProxyHandler(LocalProxyHandler):
340341

341342
def __init__(self, *args, **kwargs):
342343
self.requested_port = 0
344+
self.mappath = {}
343345
super().__init__(*args, **kwargs)
344346

345347
def initialize(self, state):
@@ -435,6 +437,11 @@ async def ensure_process(self):
435437
async def proxy(self, port, path):
436438
if not path.startswith('/'):
437439
path = '/' + path
440+
if self.mappath:
441+
if callable(self.mappath):
442+
path = call_with_asked_args(self.mappath, {'path': path})
443+
else:
444+
path = self.mappath.get(path, path)
438445

439446
await self.ensure_process()
440447

tests/resources/jupyter_server_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
def mappathf(path):
2+
p = path + 'mapped'
3+
return p
4+
15
c.ServerProxy.servers = {
26
'python-http': {
37
'command': ['python3', './tests/resources/httpinfo.py', '{port}'],
@@ -10,6 +14,16 @@
1014
'command': ['python3', './tests/resources/httpinfo.py', '{port}'],
1115
'port': 54321,
1216
},
17+
'python-http-mappath': {
18+
'command': ['python3', './tests/resources/httpinfo.py', '{port}'],
19+
'mappath': {
20+
'/': '/index.html',
21+
}
22+
},
23+
'python-http-mappathf': {
24+
'command': ['python3', './tests/resources/httpinfo.py', '{port}'],
25+
'mappath': mappathf,
26+
},
1327
}
1428

1529
import sys

tests/test_proxies.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import os
22
from http.client import HTTPConnection
3+
import pytest
34

45
PORT = os.getenv('TEST_PORT', 8888)
56
TOKEN = os.getenv('JUPYTER_TOKEN', 'secret')
67

78

89
def request_get(port, path, token, host='localhost'):
910
h = HTTPConnection(host, port, 10)
10-
h.request('GET', '{}?token={}'.format(path, token))
11+
if '?' in path:
12+
url = '{}&token={}'.format(path, token)
13+
else:
14+
url = '{}?token={}'.format(path, token)
15+
h.request('GET', url)
1116
return h.getresponse()
1217

1318

@@ -58,6 +63,39 @@ def test_server_proxy_port_absolute():
5863
assert 'X-Forwarded-Context' not in s
5964
assert 'X-Proxycontextpath' not in s
6065

66+
67+
@pytest.mark.parametrize(
68+
"requestpath,expected", [
69+
('/', '/index.html?token='),
70+
('/?q=1', '/index.html?q=1&token='),
71+
('/pqr?q=2', '/pqr?q=2&token='),
72+
]
73+
)
74+
def test_server_proxy_mappath_dict(requestpath, expected):
75+
r = request_get(PORT, '/python-http-mappath' + requestpath, TOKEN)
76+
assert r.code == 200
77+
s = r.read().decode('ascii')
78+
assert s.startswith('GET ' + expected)
79+
assert 'X-Forwarded-Context: /python-http-mappath\n' in s
80+
assert 'X-Proxycontextpath: /python-http-mappath\n' in s
81+
82+
83+
@pytest.mark.parametrize(
84+
"requestpath,expected", [
85+
('/', '/mapped?token='),
86+
('/?q=1', '/mapped?q=1&token='),
87+
('/stu?q=2', '/stumapped?q=2&token='),
88+
]
89+
)
90+
def test_server_proxy_mappath_callable(requestpath, expected):
91+
r = request_get(PORT, '/python-http-mappathf' + requestpath, TOKEN)
92+
assert r.code == 200
93+
s = r.read().decode('ascii')
94+
assert s.startswith('GET ' + expected)
95+
assert 'X-Forwarded-Context: /python-http-mappathf\n' in s
96+
assert 'X-Proxycontextpath: /python-http-mappathf\n' in s
97+
98+
6199
def test_server_proxy_remote():
62100
r = request_get(PORT, '/newproxy', TOKEN, host='127.0.0.1')
63101
assert r.code == 200

0 commit comments

Comments
 (0)