Skip to content

Commit 6b9336d

Browse files
committed
Merge StandaloneProxyHandler into StandaloneHubProxyHandler, extract address from JUPYTERHUB_SERVICE_URL
1 parent 5cae144 commit 6b9336d

File tree

2 files changed

+104
-122
lines changed

2 files changed

+104
-122
lines changed

jupyter_server_proxy/standalone/__init__.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
11
import argparse
22
import logging
33
import os
4+
from urllib.parse import urlparse
45

56
from tornado import ioloop
67
from tornado.httpserver import HTTPServer
7-
from tornado.log import app_log as log, enable_pretty_logging
8+
from tornado.log import app_log as log
9+
from tornado.log import enable_pretty_logging, gen_log
810

911
from .activity import start_activity_update
10-
from .proxy import configure_http_client, get_port_from_env, get_ssl_options, make_proxy_app
12+
from .proxy import configure_ssl, make_proxy_app
13+
14+
15+
def _default_address_and_port() -> tuple[str, int]:
16+
"""
17+
Get the Address and Port for the Proxy, either from JUPYTERHUB_SERVICE_URL or default values.
18+
See https://github.com/jupyterhub/jupyterhub/blob/4.x/jupyterhub/singleuser/mixins.py#L266-L284.
19+
"""
20+
address = "127.0.0.1"
21+
port = 8888
22+
23+
if os.environ.get("JUPYTERHUB_SERVICE_URL"):
24+
url = urlparse(os.environ["JUPYTERHUB_SERVICE_URL"])
25+
26+
if url.hostname:
27+
address = url.hostname
28+
29+
if url.port:
30+
port = url.port
31+
elif url.scheme == "http":
32+
port = 80
33+
elif url.scheme == "https":
34+
port = 443
35+
36+
return address, port
1137

1238

1339
def run(
1440
command: list[str],
15-
port: int,
16-
address: str,
41+
port: int | None,
42+
address: str | None,
1743
server_port: int,
1844
socket_path: str | None,
1945
socket_auto: bool,
2046
environment: list[tuple[str, str]] | None,
2147
mappath: list[tuple[str, str]] | None,
2248
debug: bool,
2349
# logs: bool,
24-
overwrite_authentication: bool | None,
50+
skip_authentication: bool,
2551
timeout: int,
2652
activity_interval: int,
2753
# progressive: bool,
@@ -31,12 +57,14 @@ def run(
3157
enable_pretty_logging(logger=log)
3258
if debug:
3359
log.setLevel(logging.DEBUG)
60+
gen_log.setLevel(logging.DEBUG)
3461

35-
if not port:
36-
port = get_port_from_env()
62+
address_port_default = _default_address_and_port()
63+
address = address or address_port_default[0]
64+
port = port or address_port_default[1]
3765

38-
if overwrite_authentication is True:
39-
log.info("Enabling Authentication with JupyterHub")
66+
if skip_authentication:
67+
log.warn("Disabling Authentication with JuypterHub Server!")
4068

4169
prefix = os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/")
4270

@@ -48,21 +76,25 @@ def run(
4876
dict(environment),
4977
dict(mappath),
5078
timeout,
51-
overwrite_authentication is True,
79+
skip_authentication,
5280
debug,
81+
# progressive,
5382
websocket_max_message_size,
5483
)
5584

56-
ssl_options = get_ssl_options()
57-
85+
ssl_options = configure_ssl()
5886
http_server = HTTPServer(app, ssl_options=ssl_options, xheaders=True)
5987
http_server.listen(port, address)
6088

6189
log.info(f"Starting standaloneproxy on '{address}:{port}'")
6290
log.info(f"URL Prefix: {prefix!r}")
6391
log.info(f"Command: {' '.join(command)!r}")
6492

93+
# Periodically send JupyterHub Notifications, that we are still running
6594
if activity_interval > 0:
95+
log.info(
96+
f"Sending Acitivity Notivication to JupyterHub with interval={activity_interval}s"
97+
)
6698
start_activity_update(activity_interval)
6799

68100
ioloop.IOLoop.current().start()
@@ -78,18 +110,16 @@ def main():
78110
parser.add_argument(
79111
"-p",
80112
"--port",
81-
default=0,
82113
type=int,
83114
dest="port",
84-
help="Port for the proxy server to listen on (0 for JupyterHub default).",
115+
help="Set port for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '127.0.0.1' by default.",
85116
)
86117
parser.add_argument(
87118
"-a",
88119
"--address",
89-
default="localhost",
90120
type=str,
91121
dest="address",
92-
help="Address for the proxy server to listen on.",
122+
help="Set address for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '8888' by default.",
93123
)
94124
parser.add_argument(
95125
"-s",
@@ -135,17 +165,17 @@ def main():
135165
dest="debug",
136166
help="Display debug level logs.",
137167
)
168+
# ToDo: Split Server and Application Logger
138169
# parser.add_argument(
139170
# "--logs",
140171
# action="store_true",
141172
# default=True,
142173
# help="Display logs generated by the subprocess.",
143174
# )
144175
parser.add_argument(
145-
"--overwrite-authentication",
146-
default=None,
147-
type=lambda v: None if v is None else bool(v),
148-
help="Forcefully enable/disable authentication with JupyterHub.",
176+
"--skip-authentication",
177+
action="store_true",
178+
help="Do not enforce authentication with the JupyterHub Server.",
149179
)
150180
parser.add_argument(
151181
"--timeout",
@@ -159,6 +189,7 @@ def main():
159189
type=int,
160190
help="Frequency to notify Hub that the WebApp is still running (In seconds, 0 for never).",
161191
)
192+
# ToDo: Progressive Proxy
162193
# parser.add_argument(
163194
# "--progressive",
164195
# action="store_true",
@@ -176,8 +207,6 @@ def main():
176207
)
177208

178209
args = parser.parse_args()
179-
log.debug(args)
180-
181210
run(**vars(args))
182211

183212

Lines changed: 53 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,62 @@
11
import os
22
import re
3-
from urllib.parse import urlparse
3+
from logging import Logger
44

55
from jupyterhub import __version__ as __jh_version__
66
from jupyterhub.services.auth import HubOAuthCallbackHandler, HubOAuthenticated
77
from jupyterhub.utils import make_ssl_context
88
from tornado import httpclient, web
99
from tornado.log import app_log
10-
from tornado.web import Application, RedirectHandler
10+
from tornado.web import Application
1111
from tornado.websocket import WebSocketHandler
1212

1313
from ..handlers import SuperviseAndProxyHandler
1414

1515

16-
def configure_http_client():
17-
keyfile = os.environ.get("JUPYTERHUB_SSL_KEYFILE", "")
18-
certfile = os.environ.get("JUPYTERHUB_SSL_CERTFILE", "")
19-
client_ca = os.environ.get("JUPYTERHUB_SSL_CLIENT_CA", "")
20-
21-
if keyfile == "" and certfile == "" and client_ca == "":
22-
return
23-
24-
ssl_context = make_ssl_context(keyfile, certfile, cafile=client_ca)
25-
httpclient.AsyncHTTPClient.configure(None, defaults={"ssl_options": ssl_context})
26-
27-
28-
class StandaloneProxyHandler(SuperviseAndProxyHandler):
16+
class StandaloneHubProxyHandler(HubOAuthenticated, SuperviseAndProxyHandler):
2917
"""
30-
Base class for standalone proxies. Will not ensure any authentication!
18+
Base class for standalone proxies.
19+
Will restrict access to the application by authentication with the JupyterHub API.
3120
"""
3221

3322
def __init__(self, *args, **kwargs):
3423
super().__init__(*args, **kwargs)
3524
self.environment = {}
3625
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__)
3746

3847
def prepare(self, *args, **kwargs):
3948
pass
4049

50+
async def proxy(self, port, path):
51+
if self.skip_authentication:
52+
return await super().proxy(port, path)
53+
else:
54+
return await self.oauth_proxy(port, path)
55+
56+
@web.authenticated
57+
async def oauth_proxy(self, port, path):
58+
return await super().proxy(port, path)
59+
4160
def check_origin(self, origin: str = None):
4261
# Skip JupyterHandler.check_origin
4362
return WebSocketHandler.check_origin(self, origin)
@@ -49,34 +68,21 @@ def get_timeout(self):
4968
return self.timeout
5069

5170

52-
class StandaloneHubProxyHandler(HubOAuthenticated, StandaloneProxyHandler):
53-
"""
54-
Standalone Proxy used when spawned by a JupyterHub.
55-
Will restrict access to the application by authentication with the JupyterHub API.
56-
"""
57-
58-
@property
59-
def hub_users(self):
60-
return {self.settings["user"]}
71+
def configure_ssl():
72+
keyfile = os.environ.get("JUPYTERHUB_SSL_KEYFILE")
73+
certfile = os.environ.get("JUPYTERHUB_SSL_CERTFILE")
74+
cafile = os.environ.get("JUPYTERHUB_SSL_CLIENT_CA")
6175

62-
@property
63-
def hub_groups(self):
64-
if self.settings["group"]:
65-
return {self.settings["group"]}
66-
return set()
76+
if not (keyfile and certfile and cafile):
77+
app_log.warn("Could not configure SSL")
78+
return None
6779

68-
@property
69-
def allow_all(self):
70-
if "anyone" in self.settings:
71-
return self.settings["anyone"] == "1"
72-
return super().allow_all
80+
ssl_context = make_ssl_context(keyfile, certfile, cafile)
7381

74-
@web.authenticated
75-
async def proxy(self, port, path):
76-
return await super().proxy(port, path)
82+
# Configure HTTPClient to use SSL for Proxy Requests
83+
httpclient.AsyncHTTPClient.configure(None, defaults={"ssl_options": ssl_context})
7784

78-
def set_default_headers(self):
79-
self.set_header("X-JupyterHub-Version", __jh_version__)
85+
return ssl_context
8086

8187

8288
def make_proxy_app(
@@ -92,16 +98,12 @@ def make_proxy_app(
9298
# progressive: bool,
9399
websocket_max_message_size: int,
94100
):
95-
"""
96-
Create a StandaloneHubProxyHandler subclass with given parameters
97-
"""
98101
app_log.debug(f"Process will use {port = }")
99102
app_log.debug(f"Process will use {unix_socket = }")
100103
app_log.debug(f"Process environment: {environment}")
101104
app_log.debug(f"Proxy mappath: {mappath}")
102105

103-
base = StandaloneProxyHandler if skip_authentication else StandaloneHubProxyHandler
104-
class Proxy(base):
106+
class Proxy(StandaloneHubProxyHandler):
105107
def __init__(self, *args, **kwargs):
106108
super().__init__(*args, **kwargs)
107109
self.name = f"{command[0]!r} Process"
@@ -112,10 +114,11 @@ def __init__(self, *args, **kwargs):
112114
self.command = command
113115
self.environment = environment
114116
self.timeout = timeout
117+
self.skip_authentication = skip_authentication
115118

116119
settings = dict(
117120
debug=debug,
118-
# Required for JupyterHub Authentication
121+
# Required for JupyterHub
119122
hub_user=os.environ.get("JUPYTERHUB_USER", ""),
120123
hub_group=os.environ.get("JUPYTERHUB_GROUP", ""),
121124
cookie_secret=os.urandom(32),
@@ -125,12 +128,8 @@ def __init__(self, *args, **kwargs):
125128
app_log.debug(f"Restricting WebSocket Messages to {websocket_max_message_size}")
126129
settings["websocket_max_message_size"] = websocket_max_message_size
127130

128-
return Application(
131+
app = Application(
129132
[
130-
(
131-
r"^" + re.escape(prefix) + r"/oauth_callback",
132-
HubOAuthCallbackHandler,
133-
),
134133
(
135134
r"^" + re.escape(prefix) + r"/(.*)",
136135
Proxy,
@@ -140,57 +139,11 @@ def __init__(self, *args, **kwargs):
140139
),
141140
),
142141
(
143-
r"^" + re.escape(prefix.replace("@", "%40")) + r"/(.*)",
144-
RedirectHandler,
145-
dict(url=prefix + "/{0}"),
142+
r"^" + re.escape(prefix) + r"/oauth_callback",
143+
HubOAuthCallbackHandler,
146144
),
147145
],
148146
**settings,
149147
)
150148

151-
152-
def get_ssl_options():
153-
ssl_options = {}
154-
keyfile = os.environ.get("JUPYTERHUB_SSL_KEYFILE") or ""
155-
certfile = os.environ.get("JUPYTERHUB_SSL_CERTFILE") or ""
156-
client_ca = os.environ.get("JUPYTERHUB_SSL_CLIENT_CA") or ""
157-
158-
if keyfile:
159-
ssl_options["keyfile"] = keyfile
160-
161-
if certfile:
162-
ssl_options["certfile"] = certfile
163-
164-
if client_ca:
165-
ssl_options["ca_certs"] = client_ca
166-
167-
if not ssl_options:
168-
# None indicates no SSL config
169-
ssl_options = None
170-
else:
171-
# SSL may be missing, so only import it if it"s to be used
172-
import ssl
173-
174-
# PROTOCOL_TLS selects the highest ssl/tls protocol version that both the client and
175-
# server support. When PROTOCOL_TLS is not available use PROTOCOL_SSLv23.
176-
# PROTOCOL_TLS is new in version 2.7.13, 3.5.3 and 3.6
177-
ssl_options.setdefault(
178-
"ssl_version", getattr(ssl, "PROTOCOL_TLS", ssl.PROTOCOL_SSLv23)
179-
)
180-
if ssl_options.get("ca_certs", False):
181-
ssl_options.setdefault("cert_reqs", ssl.CERT_REQUIRED)
182-
183-
return ssl_options
184-
185-
186-
# https://github.com/jupyterhub/jupyterhub/blob/2.0.0rc3/jupyterhub/singleuser/mixins.py#L340-L349
187-
def get_port_from_env():
188-
if os.environ.get("JUPYTERHUB_SERVICE_URL"):
189-
url = urlparse(os.environ["JUPYTERHUB_SERVICE_URL"])
190-
if url.port:
191-
return url.port
192-
elif url.scheme == "http":
193-
return 80
194-
elif url.scheme == "https":
195-
return 443
196-
return 8888
149+
return app

0 commit comments

Comments
 (0)