@le-space/rootfs 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ import ipaddress
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import threading
7
+ import time
8
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
+ from urllib.parse import urlsplit
10
+
11
+
12
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
13
+ READY_FILE = os.environ.get("READY_FILE", "/etc/default/uc-go-peer.ready")
14
+ CONFIGURE_SCRIPT = "/usr/local/sbin/uc-go-peer-configure.sh"
15
+ DESCRIBE_SCRIPT = "/usr/local/sbin/uc-go-peer-describe.py"
16
+ BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "uc-go-peer-bootstrap.service")
17
+ METADATA_FILE = os.environ.get("METADATA_FILE", "/run/uc-go-peer-setup-metadata.json")
18
+ METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/uc-go-peer-setup-metadata.error")
19
+
20
+
21
+ def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
22
+ handler.send_header("Access-Control-Allow-Origin", "*")
23
+ handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
24
+ handler.send_header("Access-Control-Allow-Headers", "content-type")
25
+
26
+
27
+ def _validate_port(value: object, field_name: str) -> str:
28
+ if not isinstance(value, int) or value < 1 or value > 65535:
29
+ raise ValueError(f"{field_name} must be an integer TCP/UDP port between 1 and 65535")
30
+ return str(value)
31
+
32
+
33
+ def _validate_proxy_hostname(value: object) -> str | None:
34
+ if value is None:
35
+ return None
36
+ if not isinstance(value, str):
37
+ raise ValueError("proxy_url must be a string when provided")
38
+
39
+ candidate = value.strip()
40
+ if not candidate:
41
+ return None
42
+
43
+ parsed = urlsplit(candidate if "://" in candidate else f"https://{candidate}")
44
+ if not parsed.hostname:
45
+ raise ValueError("proxy_url must include a valid hostname")
46
+ return parsed.hostname
47
+
48
+
49
+ class Handler(BaseHTTPRequestHandler):
50
+ server_version = "UcGoPeerSetup/1.0"
51
+
52
+ def _request_path(self) -> str:
53
+ return urlsplit(self.path).path
54
+
55
+ def _send_json(self, status: int, payload: dict) -> None:
56
+ body = json.dumps(payload).encode("utf-8")
57
+ self.send_response(status)
58
+ _cors_headers(self)
59
+ self.send_header("Content-Type", "application/json")
60
+ self.send_header("Content-Length", str(len(body)))
61
+ self.end_headers()
62
+ self.wfile.write(body)
63
+
64
+ def log_message(self, format: str, *args) -> None: # noqa: A003
65
+ return
66
+
67
+ def do_OPTIONS(self) -> None: # noqa: N802
68
+ self.send_response(204)
69
+ _cors_headers(self)
70
+ self.end_headers()
71
+
72
+ def do_GET(self) -> None: # noqa: N802
73
+ if self._request_path() == "/metadata":
74
+ self._handle_metadata()
75
+ return
76
+
77
+ if self._request_path() not in ("/", "/health"):
78
+ self._send_json(404, {"status": "not-found"})
79
+ return
80
+
81
+ self._send_json(
82
+ 200,
83
+ {
84
+ "status": "waiting-for-port-mapping",
85
+ "ready": os.path.exists(READY_FILE),
86
+ "env_file": ENV_FILE,
87
+ "metadata_ready": os.path.exists(METADATA_FILE),
88
+ },
89
+ )
90
+
91
+ def _handle_metadata(self) -> None:
92
+ if os.path.exists(METADATA_FILE):
93
+ with open(METADATA_FILE, encoding="utf-8") as handle:
94
+ metadata = json.load(handle)
95
+ self._send_json(200, {"status": "ready", "metadata": metadata})
96
+ threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
97
+ threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
98
+ return
99
+
100
+ if os.path.exists(METADATA_ERROR_FILE):
101
+ with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
102
+ error_message = handle.read().strip() or "metadata generation failed"
103
+ self._send_json(500, {"status": "error", "error": error_message})
104
+ threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
105
+ threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
106
+ return
107
+
108
+ self._send_json(202, {"status": "pending"})
109
+
110
+ def do_POST(self) -> None: # noqa: N802
111
+ if self._request_path() != "/configure":
112
+ self._send_json(404, {"status": "not-found"})
113
+ return
114
+
115
+ try:
116
+ content_length = int(self.headers.get("Content-Length", "0"))
117
+ except ValueError:
118
+ self._send_json(400, {"status": "bad-request", "error": "Invalid Content-Length"})
119
+ return
120
+
121
+ try:
122
+ payload = json.loads(self.rfile.read(content_length).decode("utf-8") or "{}")
123
+ except json.JSONDecodeError as error:
124
+ self._send_json(400, {"status": "bad-request", "error": f"Invalid JSON body: {error}"})
125
+ return
126
+
127
+ try:
128
+ public_ipv4 = str(ipaddress.ip_address(payload.get("public_ipv4")))
129
+ public_ipv6 = payload.get("public_ipv6")
130
+ if public_ipv6 is not None:
131
+ public_ipv6 = str(ipaddress.ip_address(public_ipv6))
132
+ proxy_hostname = _validate_proxy_hostname(payload.get("proxy_url"))
133
+ tcp_port = payload.get("tcp_port")
134
+ ws_port = payload.get("ws_port")
135
+ udp_port = payload.get("udp_port")
136
+ quic_port = payload.get("quic_port")
137
+ webrtc_port = payload.get("webrtc_port")
138
+ args = [
139
+ CONFIGURE_SCRIPT,
140
+ "--public-ipv4",
141
+ public_ipv4,
142
+ ]
143
+ if tcp_port is not None:
144
+ args.extend(["--tcp-port", _validate_port(tcp_port, "tcp_port")])
145
+ if ws_port is not None:
146
+ args.extend(["--ws-port", _validate_port(ws_port, "ws_port")])
147
+ if proxy_hostname is not None:
148
+ args.extend(["--proxy-hostname", proxy_hostname])
149
+ if public_ipv6 is not None:
150
+ args.extend(["--public-ipv6", public_ipv6])
151
+ udp_candidate = udp_port if udp_port is not None else quic_port
152
+ if udp_candidate is None:
153
+ udp_candidate = webrtc_port
154
+ if udp_candidate is not None:
155
+ args.extend(["--udp-port", _validate_port(udp_candidate, "udp_port")])
156
+ except ValueError as error:
157
+ self._send_json(400, {"status": "bad-request", "error": str(error)})
158
+ return
159
+
160
+ try:
161
+ result = subprocess.run(args, check=True, capture_output=True, text=True)
162
+ except subprocess.CalledProcessError as error:
163
+ self._send_json(
164
+ 500,
165
+ {
166
+ "status": "error",
167
+ "error": error.stderr.strip() or error.stdout.strip() or str(error),
168
+ },
169
+ )
170
+ return
171
+
172
+ _clear_metadata_state()
173
+ threading.Thread(target=_generate_metadata_files, daemon=True).start()
174
+
175
+ self._send_json(
176
+ 200,
177
+ {
178
+ "status": "configured",
179
+ "stdout": result.stdout.strip(),
180
+ "metadata_pending": True,
181
+ },
182
+ )
183
+
184
+
185
+ def _stop_bootstrap_service() -> None:
186
+ # Give the HTTP response a brief head start, then stop the temporary setup service.
187
+ time.sleep(1)
188
+ subprocess.run(["systemctl", "stop", BOOTSTRAP_SERVICE], check=False)
189
+
190
+
191
+ def _clear_metadata_state() -> None:
192
+ for path in (METADATA_FILE, METADATA_ERROR_FILE):
193
+ try:
194
+ os.remove(path)
195
+ except FileNotFoundError:
196
+ pass
197
+
198
+
199
+ def _generate_metadata_files() -> None:
200
+ try:
201
+ describe = subprocess.run(
202
+ [DESCRIBE_SCRIPT],
203
+ check=True,
204
+ capture_output=True,
205
+ text=True,
206
+ )
207
+ payload = json.loads(describe.stdout.strip() or "{}")
208
+ with open(METADATA_FILE, "w", encoding="utf-8") as handle:
209
+ json.dump(payload, handle)
210
+ except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
211
+ with open(METADATA_ERROR_FILE, "w", encoding="utf-8") as handle:
212
+ handle.write(str(error))
213
+
214
+
215
+ def main() -> None:
216
+ server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
217
+ server.serve_forever()
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=universal-connectivity go-peer relay node
3
+ Wants=network-online.target
4
+ After=network-online.target
5
+ ConditionPathExists=/etc/default/uc-go-peer.ready
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=uc-go-peer
10
+ Group=uc-go-peer
11
+ WorkingDirectory=/var/lib/uc-go-peer
12
+ EnvironmentFile=-/etc/default/uc-go-peer
13
+ ExecStart=/bin/sh -lc 'exec /usr/local/bin/universal-chat-go -headless -identity "${GO_PEER_IDENTITY_PATH:-/var/lib/uc-go-peer/identity.key}" -port "${GO_PEER_TCP_PORT:-9095}" -ws-port "${GO_PEER_WS_PORT:-9096}" -wss-port "${GO_PEER_WSS_PORT:-9097}"'
14
+ Restart=always
15
+ RestartSec=5
16
+ NoNewPrivileges=true
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target