Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 5e10d3c

Browse files
committed
Allow serving alternative endpoints on exporter
1 parent d39950e commit 5e10d3c

2 files changed

Lines changed: 85 additions & 5 deletions

File tree

packages/jumpstarter/jumpstarter/exporter/session.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616

1717
from .logging import LogHandler
18+
from .tls import with_alternative_endpoints
1819
from jumpstarter.common import Metadata, TemporarySocket
1920
from jumpstarter.common.streams import StreamRequestMetadata
2021
from jumpstarter.driver import Driver
@@ -53,12 +54,16 @@ def __init__(self, *args, root_device, **kwargs):
5354

5455
self._logging_queue = deque(maxlen=32)
5556
self._logging_handler = LogHandler(self._logging_queue)
57+
self._alternative_endpoints = []
5658

5759
@asynccontextmanager
58-
async def serve_port_async(self, port):
60+
async def serve_ports_async(self, port, alternative_endpoints: list[str] | None = None):
5961
server = grpc.aio.server()
6062
server.add_insecure_port(port)
6163

64+
if alternative_endpoints is not None:
65+
self._alternative_endpoints = with_alternative_endpoints(server, alternative_endpoints)
66+
6267
jumpstarter_pb2_grpc.add_ExporterServiceServicer_to_server(self, server)
6368
router_pb2_grpc.add_RouterServiceServicer_to_server(self, server)
6469

@@ -69,15 +74,15 @@ async def serve_port_async(self, port):
6974
await server.stop(grace=None)
7075

7176
@asynccontextmanager
72-
async def serve_unix_async(self):
77+
async def serve_unix_async(self, alternative_endpoints: list[str] | None = None):
7378
with TemporarySocket() as path:
74-
async with self.serve_port_async(f"unix://{path}"):
79+
async with self.serve_ports_async(f"unix://{path}", alternative_endpoints):
7580
yield path
7681

7782
@contextmanager
78-
def serve_unix(self):
83+
def serve_unix(self, alternative_endpoints: list[str] | None = None):
7984
with start_blocking_portal() as portal:
80-
with portal.wrap_async_context_manager(self.serve_unix_async()) as path:
85+
with portal.wrap_async_context_manager(self.serve_unix_async(alternative_endpoints)) as path:
8186
yield path
8287

8388
def __getitem__(self, key: UUID):
@@ -92,6 +97,7 @@ async def GetReport(self, request, context):
9297
instance.report(parent=parent, name=name)
9398
for (_, parent, name, instance) in self.root_device.enumerate()
9499
],
100+
alternative_endpoints=self._alternative_endpoints,
95101
)
96102

97103
async def DriverCall(self, request, context):
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from datetime import datetime, timedelta
2+
from ipaddress import IPv4Address, IPv6Address, ip_address
3+
4+
import grpc
5+
from cryptography import x509
6+
from cryptography.hazmat.backends import default_backend
7+
from cryptography.hazmat.primitives import hashes, serialization
8+
from cryptography.hazmat.primitives.asymmetric import rsa
9+
from jumpstarter_protocol import jumpstarter_pb2
10+
11+
12+
def parse_endpoint(endpoint):
13+
host, sep, port = endpoint.rpartition(":")
14+
15+
if sep == "":
16+
raise ValueError("port not specified in endpoint {}".format(endpoint))
17+
18+
host = host.strip("[]") # strip brackets from ipv6 addresses
19+
20+
try:
21+
port = int(port)
22+
if port < 0 or port > 65535:
23+
raise ValueError("port number {} out of range".format(port))
24+
except ValueError as e:
25+
raise ValueError("invalid port {} in endpoint {}".format(port, endpoint)) from e
26+
27+
try:
28+
return ip_address(host), port
29+
except ValueError:
30+
return host, port
31+
32+
33+
def with_alternative_endpoints(server, endpoints: list[str]):
34+
sans = []
35+
for endpoint in endpoints:
36+
host, port = parse_endpoint(endpoint)
37+
match host:
38+
case str():
39+
sans.append(x509.DNSName(host))
40+
case IPv4Address() | IPv6Address():
41+
sans.append(x509.IPAddress(host))
42+
43+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
44+
45+
crt = (
46+
x509.CertificateBuilder()
47+
.subject_name(x509.Name([]))
48+
.issuer_name(x509.Name([]))
49+
.public_key(key.public_key())
50+
.serial_number(x509.random_serial_number())
51+
.not_valid_before(datetime.now())
52+
.not_valid_after(datetime.now() + timedelta(days=365))
53+
.add_extension(x509.SubjectAlternativeName(sans), critical=False)
54+
.sign(private_key=key, algorithm=hashes.SHA256(), backend=default_backend())
55+
)
56+
57+
pem_crt = crt.public_bytes(serialization.Encoding.PEM)
58+
pem_key = key.private_bytes(
59+
encoding=serialization.Encoding.PEM,
60+
format=serialization.PrivateFormat.TraditionalOpenSSL,
61+
encryption_algorithm=serialization.NoEncryption(),
62+
)
63+
64+
server_credentials = grpc.ssl_server_credentials([(pem_key, pem_crt)])
65+
66+
endpoints_pb = []
67+
for endpoint in endpoints:
68+
server.add_secure_port(endpoint, server_credentials)
69+
# FIXME: generate and check token
70+
endpoints_pb.append(
71+
jumpstarter_pb2.Endpoint(endpoint=endpoint, token="", certificate=pem_crt),
72+
)
73+
74+
return endpoints_pb

0 commit comments

Comments
 (0)