Skip to content

Commit bc49c6e

Browse files
authored
Merge pull request #337 from takluyver/unix-sock
Support proxying to a server process via a Unix socket
2 parents a2462d4 + a74fe18 commit bc49c6e

File tree

8 files changed

+340
-139
lines changed

8 files changed

+340
-139
lines changed

docs/source/server-process.rst

+28-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Server Process options
1818
Server Processes are configured with a dictionary of key value
1919
pairs.
2020

21+
.. _server-process-cmd:
22+
2123
``command``
2224
^^^^^^^^^^^
2325

@@ -26,7 +28,11 @@ pairs.
2628
* A list of strings that is the command used to start the
2729
process. The following template strings will be replaced:
2830

29-
* ``{port}`` the port the process should listen on.
31+
* ``{port}`` the port that the process should listen on. This will be 0 if
32+
it should use a Unix socket instead.
33+
34+
* ``{unix_socket}`` the path at which the process should listen on a Unix
35+
socket. This will be an empty string if it should use a TCP port.
3036

3137
* ``{base_url}`` the base URL of the notebook
3238

@@ -54,8 +60,8 @@ pairs.
5460

5561
* A dictionary of strings that are passed in as the environment to
5662
the started process, in addition to the environment of the notebook
57-
process itself. The strings ``{port}`` and ``{base_url}`` will be
58-
replaced as for **command**.
63+
process itself. The strings ``{port}``, ``{unix_socket}`` and
64+
``{base_url}`` will be replaced as for **command**.
5965

6066
* A callable that takes any :ref:`callable arguments <server-process/callable-arguments>`,
6167
and returns a dictionary of strings that are used & treated same as above.
@@ -95,6 +101,25 @@ pairs.
95101
Set the port that the service will listen on. The default is to
96102
automatically select an unused port.
97103

104+
.. _server-process-unix-socket:
105+
106+
``unix_socket``
107+
^^^^^^^^^^^^^^^
108+
109+
This option uses a Unix socket on a filesystem path, instead of a TCP
110+
port. It can be passed as a string specifying the socket path, or *True* for
111+
Jupyter Server Proxy to create a temporary directory to hold the socket,
112+
ensuring that only the user running Jupyter can connect to it.
113+
114+
If this is used, the ``{unix_socket}`` argument in the command template
115+
(see :ref:`server-process-cmd`) will be a filesystem path. The server should
116+
create a Unix socket bound to this path and listen for HTTP requests on it.
117+
The ``port`` configuration key will be ignored.
118+
119+
.. note::
120+
121+
Proxying websockets over a Unix socket requires Tornado >= 6.3.
122+
98123

99124
``mappath``
100125
^^^^^^^^^^^

jupyter_server_proxy/config.py

+65-65
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from traitlets.config import Configurable
77
from tornado import httpclient
88
from warnings import warn
9-
from .handlers import SuperviseAndProxyHandler, AddSlashHandler, RewritableResponse
9+
from .handlers import (
10+
NamedLocalProxyHandler, SuperviseAndProxyHandler, AddSlashHandler,
11+
)
1012
import pkg_resources
1113
from collections import namedtuple
1214
from .utils import call_with_asked_args
@@ -17,62 +19,56 @@
1719
except ImportError:
1820
from .utils import Callable
1921

20-
def _make_serverproxy_handler(name, command, environment, timeout, absolute_url, port, mappath, request_headers_override, rewrite_response):
22+
23+
LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title', 'path_info'])
24+
ServerProcess = namedtuple('ServerProcess', [
25+
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'unix_socket',
26+
'mappath', 'launcher_entry', 'new_browser_tab', 'request_headers_override', 'rewrite_response',
27+
])
28+
29+
30+
def _make_namedproxy_handler(sp: ServerProcess):
31+
class _Proxy(NamedLocalProxyHandler):
32+
def __init__(self, *args, **kwargs):
33+
super().__init__(*args, **kwargs)
34+
self.name = sp.name
35+
self.proxy_base = sp.name
36+
self.absolute_url = sp.absolute_url
37+
self.port = sp.port
38+
self.unix_socket = sp.unix_socket
39+
self.mappath = sp.mappath
40+
self.rewrite_response = sp.rewrite_response
41+
42+
def get_request_headers_override(self):
43+
return self._realize_rendered_template(sp.request_headers_override)
44+
45+
return _Proxy
46+
47+
def _make_supervisedproxy_handler(sp: ServerProcess):
2148
"""
2249
Create a SuperviseAndProxyHandler subclass with given parameters
2350
"""
2451
# FIXME: Set 'name' properly
2552
class _Proxy(SuperviseAndProxyHandler):
2653
def __init__(self, *args, **kwargs):
2754
super().__init__(*args, **kwargs)
28-
self.name = name
29-
self.command = command
30-
self.proxy_base = name
31-
self.absolute_url = absolute_url
32-
self.requested_port = port
33-
self.mappath = mappath
34-
self.rewrite_response = rewrite_response
35-
36-
@property
37-
def process_args(self):
38-
return {
39-
'port': self.port,
40-
'base_url': self.base_url,
41-
}
42-
43-
def _render_template(self, value):
44-
args = self.process_args
45-
if type(value) is str:
46-
return value.format(**args)
47-
elif type(value) is list:
48-
return [self._render_template(v) for v in value]
49-
elif type(value) is dict:
50-
return {
51-
self._render_template(k): self._render_template(v)
52-
for k, v in value.items()
53-
}
54-
else:
55-
raise ValueError('Value of unrecognized type {}'.format(type(value)))
56-
57-
def _realize_rendered_template(self, attribute):
58-
'''Call any callables, then render any templated values.'''
59-
if callable(attribute):
60-
attribute = self._render_template(
61-
call_with_asked_args(attribute, self.process_args)
62-
)
63-
return self._render_template(attribute)
64-
65-
def get_cmd(self):
66-
return self._realize_rendered_template(self.command)
55+
self.name = sp.name
56+
self.command = sp.command
57+
self.proxy_base = sp.name
58+
self.absolute_url = sp.absolute_url
59+
self.requested_port = sp.port
60+
self.requested_unix_socket = sp.unix_socket
61+
self.mappath = sp.mappath
62+
self.rewrite_response = sp.rewrite_response
6763

6864
def get_env(self):
69-
return self._realize_rendered_template(environment)
65+
return self._realize_rendered_template(sp.environment)
7066

7167
def get_request_headers_override(self):
72-
return self._realize_rendered_template(request_headers_override)
68+
return self._realize_rendered_template(sp.request_headers_override)
7369

7470
def get_timeout(self):
75-
return timeout
71+
return sp.timeout
7672

7773
return _Proxy
7874

@@ -93,30 +89,25 @@ def make_handlers(base_url, server_processes):
9389
"""
9490
handlers = []
9591
for sp in server_processes:
96-
handler = _make_serverproxy_handler(
97-
sp.name,
98-
sp.command,
99-
sp.environment,
100-
sp.timeout,
101-
sp.absolute_url,
102-
sp.port,
103-
sp.mappath,
104-
sp.request_headers_override,
105-
sp.rewrite_response,
106-
)
92+
if sp.command:
93+
handler = _make_supervisedproxy_handler(sp)
94+
kwargs = dict(state={})
95+
else:
96+
if not (sp.port or isinstance(sp.unix_socket, str)):
97+
warn(f"Server proxy {sp.name} does not have a command, port "
98+
f"number or unix_socket path. At least one of these is "
99+
f"required.")
100+
continue
101+
handler = _make_namedproxy_handler(sp)
102+
kwargs = {}
107103
handlers.append((
108-
ujoin(base_url, sp.name, r'(.*)'), handler, dict(state={}),
104+
ujoin(base_url, sp.name, r'(.*)'), handler, kwargs,
109105
))
110106
handlers.append((
111107
ujoin(base_url, sp.name), AddSlashHandler
112108
))
113109
return handlers
114110

115-
LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title', 'path_info'])
116-
ServerProcess = namedtuple('ServerProcess', [
117-
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port',
118-
'mappath', 'launcher_entry', 'new_browser_tab', 'request_headers_override', 'rewrite_response',
119-
])
120111

121112
def make_server_process(name, server_process_config, serverproxy_config):
122113
le = server_process_config.get('launcher_entry', {})
@@ -127,6 +118,7 @@ def make_server_process(name, server_process_config, serverproxy_config):
127118
timeout=server_process_config.get('timeout', 5),
128119
absolute_url=server_process_config.get('absolute_url', False),
129120
port=server_process_config.get('port', 0),
121+
unix_socket=server_process_config.get('unix_socket', None),
130122
mappath=server_process_config.get('mappath', {}),
131123
launcher_entry=LauncherEntry(
132124
enabled=le.get('enabled', True),
@@ -154,8 +146,9 @@ class ServerProxy(Configurable):
154146
Value should be a dictionary with the following keys:
155147
command
156148
An optional list of strings that should be the full command to be executed.
157-
The optional template arguments {{port}} and {{base_url}} will be substituted with the
158-
port the process should listen on and the base-url of the notebook.
149+
The optional template arguments {{port}}, {{unix_socket}} and {{base_url}}
150+
will be substituted with the port or Unix socket path the process should
151+
listen on and the base-url of the notebook.
159152
160153
Could also be a callable. It should return a list.
161154
@@ -165,7 +158,7 @@ class ServerProxy(Configurable):
165158
166159
environment
167160
A dictionary of environment variable mappings. As with the command
168-
traitlet, {{port}} and {{base_url}} will be substituted.
161+
traitlet, {{port}}, {{unix_socket}} and {{base_url}} will be substituted.
169162
170163
Could also be a callable. It should return a dictionary.
171164
@@ -179,6 +172,13 @@ class ServerProxy(Configurable):
179172
port
180173
Set the port that the service will listen on. The default is to automatically select an unused port.
181174
175+
unix_socket
176+
If set, the service will listen on a Unix socket instead of a TCP port.
177+
Set to True to use a socket in a new temporary folder, or a string
178+
path to a socket. This overrides port.
179+
180+
Proxying websockets over a Unix socket requires Tornado >= 6.3.
181+
182182
mappath
183183
Map request paths to proxied paths.
184184
Either a dictionary of request paths to proxied paths,
@@ -210,7 +210,7 @@ class ServerProxy(Configurable):
210210
211211
request_headers_override
212212
A dictionary of additional HTTP headers for the proxy request. As with
213-
the command traitlet, {{port}} and {{base_url}} will be substituted.
213+
the command traitlet, {{port}}, {{unix_socket}} and {{base_url}} will be substituted.
214214
215215
rewrite_response
216216
An optional function to rewrite the response for the given service.

0 commit comments

Comments
 (0)