@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.
- package/index.js +17 -3
- package/package.json +1 -1
- package/reference/uc-go-peer/contract.json +58 -0
- package/reference/uc-go-peer/rootfs/Dockerfile.rootfs +24 -0
- package/reference/uc-go-peer/rootfs/build-rootfs-image.sh +94 -0
- package/reference/uc-go-peer/rootfs/build-rootfs.sh +489 -0
- package/reference/uc-go-peer/rootfs/read-rootfs-contract.py +72 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-autotls-refresh.py +144 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-autotls-refresh.service +23 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap.service +17 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap.sh +118 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-configure.sh +204 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-describe.py +195 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-setup-server.py +221 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer.service +19 -0
|
@@ -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
|