Skip to content

Commit 7ed8974

Browse files
committed
Refactor and reuse Proxy generation in standalone
1 parent 0228e86 commit 7ed8974

File tree

3 files changed

+167
-150
lines changed

3 files changed

+167
-150
lines changed

jupyter_server_proxy/config.py

+84-59
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Traitlets based configuration for jupyter_server_proxy
33
"""
44

5+
from __future__ import annotations
6+
57
import sys
68
from textwrap import dedent, indent
79
from warnings import warn
@@ -263,60 +265,83 @@ def cats_only(response, path):
263265
""",
264266
).tag(config=True)
265267

268+
def get_proxy_base_class(self) -> tuple[type | None, dict]:
269+
"""
270+
Return the appropriate ProxyHandler Subclass and its kwargs
271+
"""
272+
if self.command:
273+
return (
274+
SuperviseAndRawSocketHandler
275+
if self.raw_socket_proxy
276+
else SuperviseAndProxyHandler
277+
), dict(state={})
278+
279+
if not (self.port or isinstance(self.unix_socket, str)):
280+
warn(
281+
f"""Server proxy {self.name} does not have a command, port number or unix_socket path.
282+
At least one of these is required."""
283+
)
284+
return None, dict()
285+
286+
return (
287+
RawSocketHandler if self.raw_socket_proxy else NamedLocalProxyHandler
288+
), dict()
266289

267-
def _make_proxy_handler(sp: ServerProcess):
268-
"""
269-
Create an appropriate handler with given parameters
270-
"""
271-
if sp.command:
272-
cls = (
273-
SuperviseAndRawSocketHandler
274-
if sp.raw_socket_proxy
275-
else SuperviseAndProxyHandler
276-
)
277-
args = dict(state={})
278-
elif not (sp.port or isinstance(sp.unix_socket, str)):
279-
warn(
280-
f"Server proxy {sp.name} does not have a command, port "
281-
f"number or unix_socket path. At least one of these is "
282-
f"required."
283-
)
284-
return
285-
else:
286-
cls = RawSocketHandler if sp.raw_socket_proxy else NamedLocalProxyHandler
287-
args = {}
288-
289-
# FIXME: Set 'name' properly
290-
class _Proxy(cls):
291-
kwargs = args
292-
293-
def __init__(self, *args, **kwargs):
294-
super().__init__(*args, **kwargs)
295-
self.name = sp.name
296-
self.command = sp.command
297-
self.proxy_base = sp.name
298-
self.absolute_url = sp.absolute_url
299-
if sp.command:
300-
self.requested_port = sp.port
301-
self.requested_unix_socket = sp.unix_socket
302-
else:
303-
self.port = sp.port
304-
self.unix_socket = sp.unix_socket
305-
self.mappath = sp.mappath
306-
self.rewrite_response = sp.rewrite_response
307-
self.update_last_activity = sp.update_last_activity
308-
309-
def get_request_headers_override(self):
310-
return self._realize_rendered_template(sp.request_headers_override)
311-
312-
# these two methods are only used in supervise classes, but do no harm otherwise
313-
def get_env(self):
314-
return self._realize_rendered_template(sp.environment)
315-
316-
def get_timeout(self):
317-
return sp.timeout
318-
319-
return _Proxy
290+
def get_proxy_attributes(self) -> dict:
291+
"""
292+
Return the required attributes, which will be set on the proxy handler
293+
"""
294+
attributes = {
295+
"name": self.name,
296+
"command": self.command,
297+
"proxy_base": self.name,
298+
"absolute_url": self.absolute_url,
299+
"mappath": self.mappath,
300+
"rewrite_response": self.rewrite_response,
301+
"update_last_activity": self.update_last_activity,
302+
"request_headers_override": self.request_headers_override,
303+
}
304+
305+
if self.command:
306+
attributes["requested_port"] = self.port
307+
attributes["requested_unix_socket"] = self.unix_socket
308+
attributes["environment"] = self.environment
309+
attributes["timeout"] = self.timeout
310+
else:
311+
attributes["port"] = self.port
312+
attributes["unix_socket"] = self.unix_socket
313+
314+
return attributes
315+
316+
def make_proxy_handler(self) -> tuple[type | None, dict]:
317+
"""
318+
Create an appropriate handler for this ServerProxy Configuration
319+
"""
320+
cls, proxy_kwargs = self.get_proxy_base_class()
321+
if cls is None:
322+
return None, proxy_kwargs
323+
324+
# FIXME: Set 'name' properly
325+
attributes = self.get_proxy_attributes()
326+
327+
class _Proxy(cls):
328+
def __init__(self, *args, **kwargs):
329+
super().__init__(*args, **kwargs)
330+
331+
for name, value in attributes.items():
332+
setattr(self, name, value)
333+
334+
def get_request_headers_override(self):
335+
return self._realize_rendered_template(self.request_headers_override)
336+
337+
# these two methods are only used in supervise classes, but do no harm otherwise
338+
def get_env(self):
339+
return self._realize_rendered_template(self.environment)
340+
341+
def get_timeout(self):
342+
return self.timeout
343+
344+
return _Proxy, proxy_kwargs
320345

321346

322347
def get_entrypoint_server_processes(serverproxy_config):
@@ -332,21 +357,21 @@ def get_entrypoint_server_processes(serverproxy_config):
332357
return sps
333358

334359

335-
def make_handlers(base_url, server_processes):
360+
def make_handlers(base_url: str, server_processes: list[ServerProcess]):
336361
"""
337362
Get tornado handlers for registered server_processes
338363
"""
339364
handlers = []
340-
for sp in server_processes:
341-
handler = _make_proxy_handler(sp)
365+
for server in server_processes:
366+
handler, kwargs = server.make_proxy_handler()
342367
if not handler:
343368
continue
344-
handlers.append((ujoin(base_url, sp.name, r"(.*)"), handler, handler.kwargs))
345-
handlers.append((ujoin(base_url, sp.name), AddSlashHandler))
369+
handlers.append((ujoin(base_url, server.name, r"(.*)"), handler, kwargs))
370+
handlers.append((ujoin(base_url, server.name), AddSlashHandler))
346371
return handlers
347372

348373

349-
def make_server_process(name, server_process_config, serverproxy_config):
374+
def make_server_process(name: str, server_process_config: dict, serverproxy_config):
350375
return ServerProcess(name=name, **server_process_config)
351376

352377

jupyter_server_proxy/standalone/app.py

+20-14
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from ..config import ServerProcess
1818
from .activity import start_activity_update
19-
from .proxy import make_proxy
19+
from .proxy import make_standalone_proxy
2020

2121

2222
class StandaloneProxyServer(TraitletsApplication, ServerProcess):
@@ -128,8 +128,8 @@ def _default_command(self):
128128
# ToDo: Find a better way to do this
129129
return self.extra_args
130130

131-
def __init__(self):
132-
super().__init__()
131+
def __init__(self, **kwargs):
132+
super().__init__(**kwargs)
133133

134134
# Flags for CLI
135135
self.flags = {
@@ -174,7 +174,21 @@ def __init__(self):
174174
"websocket_max_message_size": "StandaloneProxyServer.websocket_max_message_size",
175175
}
176176

177-
def _create_app(self) -> web.Application:
177+
def get_proxy_base_class(self) -> tuple[type | None, dict]:
178+
cls, kwargs = super().get_proxy_base_class()
179+
if cls is None:
180+
return None, kwargs
181+
182+
return make_standalone_proxy(cls, kwargs)
183+
184+
def get_proxy_attributes(self) -> dict:
185+
attributes = super().get_proxy_attributes()
186+
attributes["requested_port"] = self.server_port
187+
attributes["skip_authentication"] = self.skip_authentication
188+
189+
return attributes
190+
191+
def create_app(self) -> web.Application:
178192
self.log.debug(f"Process will use port = {self.port}")
179193
self.log.debug(f"Process will use unix_socket = {self.unix_socket}")
180194
self.log.debug(f"Process environment: {self.environment}")
@@ -196,15 +210,7 @@ def _create_app(self) -> web.Application:
196210
settings["websocket_max_message_size"] = self.websocket_max_message_size
197211

198212
# Create the proxy class with out arguments
199-
proxy_handler, proxy_kwargs = make_proxy(
200-
self.command,
201-
self.server_port,
202-
self.unix_socket,
203-
self.environment,
204-
self.mappath,
205-
self.timeout,
206-
self.skip_authentication,
207-
)
213+
proxy_handler, proxy_kwargs = self.make_proxy_handler()
208214

209215
base_url = re.escape(self.base_url)
210216
return web.Application(
@@ -253,7 +259,7 @@ def start(self):
253259
if self.skip_authentication:
254260
self.log.warn("Disabling Authentication with JuypterHub Server!")
255261

256-
app = self._create_app()
262+
app = self.create_app()
257263

258264
ssl_options = self._configure_ssl()
259265
http_server = httpserver.HTTPServer(app, ssl_options=ssl_options, xheaders=True)

jupyter_server_proxy/standalone/proxy.py

+63-77
Original file line numberDiff line numberDiff line change
@@ -13,83 +13,69 @@
1313
from ..handlers import SuperviseAndProxyHandler
1414

1515

16-
class StandaloneHubProxyHandler(HubOAuthenticated, SuperviseAndProxyHandler):
17-
"""
18-
Base class for standalone proxies.
19-
Will restrict access to the application by authentication with the JupyterHub API.
20-
"""
21-
22-
def __init__(self, *args, **kwargs):
23-
super().__init__(*args, **kwargs)
24-
self.environment = {}
25-
self.timeout = 60
26-
self.skip_authentication = False
27-
28-
@property
29-
def log(self) -> Logger:
30-
return app_log
31-
32-
@property
33-
def hub_users(self):
34-
if "hub_user" in self.settings:
35-
return {self.settings["hub_user"]}
36-
return set()
37-
38-
@property
39-
def hub_groups(self):
40-
if "hub_group" in self.settings:
41-
return {self.settings["hub_group"]}
42-
return set()
43-
44-
def set_default_headers(self):
45-
self.set_header("X-JupyterHub-Version", __jh_version__)
46-
47-
def prepare(self, *args, **kwargs):
48-
pass
49-
50-
def check_origin(self, origin: str = None):
51-
# Skip JupyterHandler.check_origin
52-
return WebSocketHandler.check_origin(self, origin)
53-
54-
def check_xsrf_cookie(self):
55-
# Skip HubAuthenticated.check_xsrf_cookie
56-
pass
57-
58-
def write_error(self, status_code: int, **kwargs):
59-
# ToDo: Return proper error page, like in jupyter-server/JupyterHub
60-
return RequestHandler.write_error(self, status_code, **kwargs)
61-
62-
async def proxy(self, port, path):
63-
if self.skip_authentication:
64-
return await super().proxy(port, path)
65-
else:
66-
return await ensure_async(self.oauth_proxy(port, path))
67-
68-
@web.authenticated
69-
async def oauth_proxy(self, port, path):
70-
return await super().proxy(port, path)
71-
72-
def get_env(self):
73-
return self._render_template(self.environment)
74-
75-
def get_timeout(self):
76-
return self.timeout
16+
def make_standalone_proxy(
17+
base_proxy_class: type, proxy_kwargs: dict
18+
) -> tuple[type | None, dict]:
19+
if not issubclass(base_proxy_class, SuperviseAndProxyHandler):
20+
app_log.error(
21+
"Cannot create a 'StandaloneHubProxyHandler' from a class not inheriting from 'SuperviseAndProxyHandler'"
22+
)
23+
return None, dict()
24+
25+
class StandaloneHubProxyHandler(HubOAuthenticated, base_proxy_class):
26+
"""
27+
Base class for standalone proxies.
28+
Will restrict access to the application by authentication with the JupyterHub API.
29+
"""
7730

78-
79-
def make_proxy(
80-
command, port, unix_socket, environment, mappath, timeout, skip_authentication
81-
) -> tuple[type, dict]:
82-
class Proxy(StandaloneHubProxyHandler):
8331
def __init__(self, *args, **kwargs):
8432
super().__init__(*args, **kwargs)
85-
self.name = f"{command[0]!r} Process"
86-
self.proxy_base = command[0]
87-
self.requested_port = port
88-
self.requested_unix_socket = unix_socket
89-
self.mappath = mappath
90-
self.command = command
91-
self.environment = environment
92-
self.timeout = timeout
93-
self.skip_authentication = skip_authentication
94-
95-
return Proxy, dict(state={})
33+
self.environment = {}
34+
self.timeout = 60
35+
self.skip_authentication = False
36+
37+
@property
38+
def log(self) -> Logger:
39+
return app_log
40+
41+
@property
42+
def hub_users(self):
43+
if "hub_user" in self.settings:
44+
return {self.settings["hub_user"]}
45+
return set()
46+
47+
@property
48+
def hub_groups(self):
49+
if "hub_group" in self.settings:
50+
return {self.settings["hub_group"]}
51+
return set()
52+
53+
def set_default_headers(self):
54+
self.set_header("X-JupyterHub-Version", __jh_version__)
55+
56+
def prepare(self, *args, **kwargs):
57+
pass
58+
59+
def check_origin(self, origin: str = None):
60+
# Skip JupyterHandler.check_origin
61+
return WebSocketHandler.check_origin(self, origin)
62+
63+
def check_xsrf_cookie(self):
64+
# Skip HubAuthenticated.check_xsrf_cookie
65+
pass
66+
67+
def write_error(self, status_code: int, **kwargs):
68+
# ToDo: Return proper error page, like in jupyter-server/JupyterHub
69+
return RequestHandler.write_error(self, status_code, **kwargs)
70+
71+
async def proxy(self, port, path):
72+
if self.skip_authentication:
73+
return await super().proxy(port, path)
74+
else:
75+
return await ensure_async(self.oauth_proxy(port, path))
76+
77+
@web.authenticated
78+
async def oauth_proxy(self, port, path):
79+
return await super().proxy(port, path)
80+
81+
return StandaloneHubProxyHandler, proxy_kwargs

0 commit comments

Comments
 (0)