@le-space/rootfs 0.6.36 → 0.6.37
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.d.ts +2 -0
- package/index.js +3 -0
- package/package.json +1 -1
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-configure.sh +42 -0
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-describe.py +24 -0
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-setup-server.py +354 -4
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-describe.cpython-312.pyc +0 -0
package/index.d.ts
CHANGED
|
@@ -115,6 +115,8 @@ interface RootfsManifest {
|
|
|
115
115
|
version: string;
|
|
116
116
|
rootfsInstallStrategy: string;
|
|
117
117
|
requiresBootstrapNetwork: boolean;
|
|
118
|
+
/** Guest fetches its bootstrap config from the Aleph aggregate itself. */
|
|
119
|
+
supportsBootstrapConfigAggregate?: boolean;
|
|
118
120
|
bootstrapSummary: string;
|
|
119
121
|
rootfsSourceSizeBytes?: number;
|
|
120
122
|
requiredPortForwards: RootfsContract["ports"];
|
package/index.js
CHANGED
|
@@ -351,6 +351,9 @@ function createRootfsManifest(plan, contract, options = {}) {
|
|
|
351
351
|
if (contract.rootfs.profile === "playwright-runner") {
|
|
352
352
|
manifest.playwrightVersion = "1.61.1";
|
|
353
353
|
}
|
|
354
|
+
if (contract.rootfs.profile === "uc-go-peer" || contract.rootfs.profile === "orbitdb-relay") {
|
|
355
|
+
manifest.supportsBootstrapConfigAggregate = true;
|
|
356
|
+
}
|
|
354
357
|
if (typeof options.rootfsSourceSizeBytes === "number" && Number.isFinite(options.rootfsSourceSizeBytes) && options.rootfsSourceSizeBytes > 0) {
|
|
355
358
|
manifest.rootfsSourceSizeBytes = options.rootfsSourceSizeBytes;
|
|
356
359
|
}
|
package/package.json
CHANGED
|
@@ -40,6 +40,7 @@ QUIC_PORT=""
|
|
|
40
40
|
BOOTSTRAP_PUBLISHER_PRIVATE_KEY=""
|
|
41
41
|
BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_HEX=""
|
|
42
42
|
BOOTSTRAP_OWNER_PRIVATE_KEY=""
|
|
43
|
+
BOOTSTRAP_OWNER_ADDRESS=""
|
|
43
44
|
BOOTSTRAP_OWNER_AUTHORIZATION_B64=""
|
|
44
45
|
BOOTSTRAP_REGISTRATION_ID=""
|
|
45
46
|
START_SERVICE=1
|
|
@@ -60,6 +61,7 @@ Usage:
|
|
|
60
61
|
[--bootstrap-publisher-private-key <hex>] \
|
|
61
62
|
[--bootstrap-publisher-libp2p-identity-hex <hex>] \
|
|
62
63
|
[--bootstrap-owner-private-key <hex>] \
|
|
64
|
+
[--bootstrap-owner-address <0x…>] \
|
|
63
65
|
[--bootstrap-owner-authorization-b64 <base64>] \
|
|
64
66
|
[--bootstrap-registration-id <id>] \
|
|
65
67
|
[--no-start]
|
|
@@ -69,6 +71,33 @@ marks the relay as ready, and optionally starts the relay plus Caddy services.
|
|
|
69
71
|
EOF
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
current_bootstrap_publisher_private_key() {
|
|
75
|
+
local existing
|
|
76
|
+
existing="$(grep -E '^[#[:space:]]*ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY=' "${ENV_FILE}" 2>/dev/null | tail -n1 | cut -d= -f2- || true)"
|
|
77
|
+
if [ -n "${existing}" ]; then
|
|
78
|
+
printf '%s\n' "${existing}"
|
|
79
|
+
return
|
|
80
|
+
fi
|
|
81
|
+
printf '\n'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# The bootstrap publisher key signs this relay's own Aleph registrations, so
|
|
85
|
+
# it never has to leave the guest. Generating it here means the deployer does
|
|
86
|
+
# not have to send a secret to the plain-HTTP setup endpoint, which runs
|
|
87
|
+
# before Caddy/TLS exists and would otherwise put the key on the wire in the
|
|
88
|
+
# clear. An already-provisioned key is reused so the relay keeps its identity
|
|
89
|
+
# across reconfigurations.
|
|
90
|
+
generate_bootstrap_publisher_private_key() {
|
|
91
|
+
while true; do
|
|
92
|
+
local candidate
|
|
93
|
+
candidate="$(openssl rand -hex 32)"
|
|
94
|
+
if [ -n "${candidate}" ] && [ "${candidate}" != "0000000000000000000000000000000000000000000000000000000000000000" ]; then
|
|
95
|
+
printf '0x%s\n' "${candidate}"
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
write_env_var() {
|
|
73
102
|
local key="$1"
|
|
74
103
|
local value="$2"
|
|
@@ -176,6 +205,10 @@ while [ "$#" -gt 0 ]; do
|
|
|
176
205
|
BOOTSTRAP_OWNER_PRIVATE_KEY="${2:-}"
|
|
177
206
|
shift 2
|
|
178
207
|
;;
|
|
208
|
+
--bootstrap-owner-address)
|
|
209
|
+
BOOTSTRAP_OWNER_ADDRESS="${2:-}"
|
|
210
|
+
shift 2
|
|
211
|
+
;;
|
|
179
212
|
--bootstrap-owner-authorization-b64)
|
|
180
213
|
BOOTSTRAP_OWNER_AUTHORIZATION_B64="${2:-}"
|
|
181
214
|
shift 2
|
|
@@ -267,6 +300,12 @@ fi
|
|
|
267
300
|
if [ -n "${QUIC_PORT}" ]; then
|
|
268
301
|
write_env_var "EXTERNAL_RELAY_QUIC_PORT" "${QUIC_PORT}"
|
|
269
302
|
fi
|
|
303
|
+
if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
|
|
304
|
+
BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(current_bootstrap_publisher_private_key)"
|
|
305
|
+
fi
|
|
306
|
+
if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
|
|
307
|
+
BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(generate_bootstrap_publisher_private_key)"
|
|
308
|
+
fi
|
|
270
309
|
if [ -n "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
|
|
271
310
|
write_env_var "ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
|
|
272
311
|
fi
|
|
@@ -276,6 +315,9 @@ fi
|
|
|
276
315
|
if [ -n "${BOOTSTRAP_OWNER_PRIVATE_KEY}" ]; then
|
|
277
316
|
write_env_var "ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY" "${BOOTSTRAP_OWNER_PRIVATE_KEY}"
|
|
278
317
|
fi
|
|
318
|
+
if [ -n "${BOOTSTRAP_OWNER_ADDRESS}" ]; then
|
|
319
|
+
write_env_var "ALEPH_BOOTSTRAP_OWNER_ADDRESS" "${BOOTSTRAP_OWNER_ADDRESS}"
|
|
320
|
+
fi
|
|
279
321
|
if [ -n "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}" ]; then
|
|
280
322
|
write_env_var "ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64" "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}"
|
|
281
323
|
fi
|
|
@@ -51,6 +51,29 @@ def normalize_multiaddrs(payload: dict) -> list[str]:
|
|
|
51
51
|
return [entry for entry in values if isinstance(entry, str) and entry.strip()]
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def bootstrap_publisher_address(env_values: dict[str, str]) -> str | None:
|
|
55
|
+
"""Address of the key this relay signs its own Aleph registrations with.
|
|
56
|
+
|
|
57
|
+
The guest generates that key itself, so the deployer cannot know the
|
|
58
|
+
address up front. Reporting it here lets the deployer bind an owner
|
|
59
|
+
authorization to the relay's real publisher identity instead of having to
|
|
60
|
+
send the key to the guest over the plain-HTTP setup endpoint.
|
|
61
|
+
"""
|
|
62
|
+
private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
|
|
63
|
+
if not private_key:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
from eth_account import Account
|
|
68
|
+
except ImportError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return Account.from_key(private_key).address
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
54
77
|
def build_grouped_multiaddrs(env_values: dict[str, str], all_multiaddrs: list[str], peer_id: str) -> dict[str, list[str]]:
|
|
55
78
|
proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip().lower()
|
|
56
79
|
direct_tcp_multiaddrs = [addr for addr in all_multiaddrs if "/tcp/" in addr and "/ws" not in addr]
|
|
@@ -148,6 +171,7 @@ def main() -> None:
|
|
|
148
171
|
env_values = parse_env_file(ENV_FILE)
|
|
149
172
|
payload = {
|
|
150
173
|
"peer_id": peer_id,
|
|
174
|
+
"bootstrap_publisher_address": bootstrap_publisher_address(env_values),
|
|
151
175
|
"announce_addrs": [
|
|
152
176
|
entry.strip()
|
|
153
177
|
for entry in env_values.get("VITE_APPEND_ANNOUNCE", "").split(",")
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
2
4
|
import ipaddress
|
|
3
5
|
import json
|
|
4
6
|
import os
|
|
@@ -6,7 +8,9 @@ import subprocess
|
|
|
6
8
|
import threading
|
|
7
9
|
import time
|
|
8
10
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
-
from urllib.
|
|
11
|
+
from urllib.error import HTTPError, URLError
|
|
12
|
+
from urllib.parse import urlencode, urlsplit
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay")
|
|
@@ -18,6 +22,26 @@ DESCRIBE_SCRIPT = "/usr/local/sbin/orbitdb-relay-describe.py"
|
|
|
18
22
|
METADATA_FILE = os.environ.get("METADATA_FILE", "/run/orbitdb-relay-setup-metadata.json")
|
|
19
23
|
METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/orbitdb-relay-setup-metadata.error")
|
|
20
24
|
|
|
25
|
+
# Guest-side bootstrap config handoff.
|
|
26
|
+
#
|
|
27
|
+
# A browser served over HTTPS cannot POST to this server: it listens on plain
|
|
28
|
+
# HTTP (it has to exist before Caddy/TLS does), so the request is blocked as
|
|
29
|
+
# mixed content. Instead the deployer publishes the configuration into a
|
|
30
|
+
# public Aleph aggregate and puts a locator into the VM's SSH key; the guest
|
|
31
|
+
# reads that locator, fetches its own configuration over HTTPS, applies it,
|
|
32
|
+
# and publishes a signed acknowledgement the deployer waits for.
|
|
33
|
+
AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/authorized_keys")
|
|
34
|
+
ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api.aleph.im")
|
|
35
|
+
BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
|
|
36
|
+
BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
|
|
37
|
+
BOOTSTRAP_CONFIG_SIGNAL_REF = os.environ.get("BOOTSTRAP_CONFIG_SIGNAL_REF", "vm-bootstrap-config")
|
|
38
|
+
BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = os.environ.get(
|
|
39
|
+
"BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE", "vm-bootstrap-config-signal"
|
|
40
|
+
)
|
|
41
|
+
BOOTSTRAP_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
|
|
42
|
+
|
|
43
|
+
CONFIGURE_LOCK = threading.Lock()
|
|
44
|
+
|
|
21
45
|
|
|
22
46
|
def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
|
|
23
47
|
handler.send_header("Access-Control-Allow-Origin", "*")
|
|
@@ -49,6 +73,323 @@ def _validate_proxy_hostname(value: object) -> str | None:
|
|
|
49
73
|
return parsed.hostname
|
|
50
74
|
|
|
51
75
|
|
|
76
|
+
def _json_dumps(payload: object) -> str:
|
|
77
|
+
return json.dumps(payload, separators=(",", ":"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_env_file(path: str) -> dict[str, str]:
|
|
81
|
+
values: dict[str, str] = {}
|
|
82
|
+
if not os.path.exists(path):
|
|
83
|
+
return values
|
|
84
|
+
|
|
85
|
+
with open(path, encoding="utf-8") as handle:
|
|
86
|
+
for line in handle:
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
89
|
+
continue
|
|
90
|
+
key, value = stripped.split("=", 1)
|
|
91
|
+
values[key.strip()] = value.strip()
|
|
92
|
+
return values
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
|
|
96
|
+
"""Read the deployment locator the deployer left in our SSH key.
|
|
97
|
+
|
|
98
|
+
The Aleph INSTANCE message carries an SSH public key with an extra
|
|
99
|
+
`aleph-bootstrap-config:<owner>:<token>` part, so it lands in the guest's
|
|
100
|
+
authorized_keys and gives us everything needed to find our own record.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
with open(AUTHORIZED_KEYS_FILE, encoding="utf-8") as handle:
|
|
104
|
+
for raw_line in handle:
|
|
105
|
+
line = raw_line.strip()
|
|
106
|
+
if not line or line.startswith("#"):
|
|
107
|
+
continue
|
|
108
|
+
for part in line.split():
|
|
109
|
+
if part.startswith("aleph-bootstrap-config:"):
|
|
110
|
+
_, owner_address, deployment_token = part.split(":", 2)
|
|
111
|
+
owner_address = owner_address.strip()
|
|
112
|
+
deployment_token = deployment_token.strip()
|
|
113
|
+
if owner_address and deployment_token:
|
|
114
|
+
return owner_address, deployment_token
|
|
115
|
+
except (FileNotFoundError, ValueError):
|
|
116
|
+
return None, None
|
|
117
|
+
|
|
118
|
+
return None, None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _address_from_private_key(private_key: str) -> str:
|
|
122
|
+
from eth_account import Account
|
|
123
|
+
|
|
124
|
+
return Account.from_key(private_key).address
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _sign_personal_message(private_key: str, payload: str) -> str:
|
|
128
|
+
from eth_account import Account
|
|
129
|
+
from eth_account.messages import encode_defunct
|
|
130
|
+
|
|
131
|
+
signed = Account.sign_message(encode_defunct(text=payload), private_key=private_key)
|
|
132
|
+
signature = signed.signature.hex()
|
|
133
|
+
return signature if signature.startswith("0x") else f"0x{signature}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
|
|
137
|
+
return "\n".join([chain, sender, message_type, item_hash])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _broadcast_aleph_message(api_host: str, message: dict[str, object]) -> None:
|
|
141
|
+
body = json.dumps({"message": message, "sync": True}).encode("utf-8")
|
|
142
|
+
request = Request(
|
|
143
|
+
f"{api_host.rstrip('/')}/api/v0/messages",
|
|
144
|
+
data=body,
|
|
145
|
+
headers={"content-type": "application/json"},
|
|
146
|
+
method="POST",
|
|
147
|
+
)
|
|
148
|
+
with urlopen(request, timeout=30) as response:
|
|
149
|
+
response.read()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _fresh_metadata_payload() -> tuple[dict | None, str | None]:
|
|
153
|
+
if os.path.exists(METADATA_FILE):
|
|
154
|
+
try:
|
|
155
|
+
with open(METADATA_FILE, encoding="utf-8") as handle:
|
|
156
|
+
return json.load(handle), None
|
|
157
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
158
|
+
return None, str(error)
|
|
159
|
+
if os.path.exists(METADATA_ERROR_FILE):
|
|
160
|
+
with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
|
|
161
|
+
return None, handle.read().strip() or "metadata generation failed"
|
|
162
|
+
return None, None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_configure_args_from_record(record: dict) -> list[str]:
|
|
166
|
+
"""Map an Aleph bootstrap config record onto configure.sh arguments.
|
|
167
|
+
|
|
168
|
+
Deliberately mirrors the /configure payload mapping, minus every secret:
|
|
169
|
+
the record lives in a *public* aggregate, so it may only carry network
|
|
170
|
+
facts, the registration id, the owner address and a signed authorization.
|
|
171
|
+
"""
|
|
172
|
+
runtime = record.get("runtime") if isinstance(record.get("runtime"), dict) else {}
|
|
173
|
+
bootstrap = record.get("bootstrap") if isinstance(record.get("bootstrap"), dict) else {}
|
|
174
|
+
|
|
175
|
+
public_ipv4 = str(runtime.get("publicIpv4") or "").strip()
|
|
176
|
+
if not public_ipv4:
|
|
177
|
+
raise ValueError("bootstrap config is missing runtime.publicIpv4")
|
|
178
|
+
|
|
179
|
+
mapped = runtime.get("mappedPorts") if isinstance(runtime.get("mappedPorts"), dict) else {}
|
|
180
|
+
|
|
181
|
+
def host_port(*container_ports: str) -> int | None:
|
|
182
|
+
for container_port in container_ports:
|
|
183
|
+
entry = mapped.get(container_port)
|
|
184
|
+
if isinstance(entry, dict):
|
|
185
|
+
host = entry.get("host")
|
|
186
|
+
if isinstance(host, int) and host > 0:
|
|
187
|
+
return host
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
tcp_port = host_port("9091")
|
|
191
|
+
ws_port = host_port("9092", "443")
|
|
192
|
+
if not tcp_port or not ws_port:
|
|
193
|
+
raise ValueError("bootstrap config is missing the mapped TCP/WS ports")
|
|
194
|
+
|
|
195
|
+
args = [
|
|
196
|
+
CONFIGURE_SCRIPT,
|
|
197
|
+
"--public-ipv4",
|
|
198
|
+
public_ipv4,
|
|
199
|
+
"--tcp-port",
|
|
200
|
+
str(tcp_port),
|
|
201
|
+
"--ws-port",
|
|
202
|
+
str(ws_port),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
public_ipv6 = str(runtime.get("publicIpv6") or "").strip()
|
|
206
|
+
if public_ipv6:
|
|
207
|
+
args.extend(["--public-ipv6", public_ipv6])
|
|
208
|
+
|
|
209
|
+
proxy_hostname = _validate_proxy_hostname(runtime.get("proxyUrl"))
|
|
210
|
+
if proxy_hostname:
|
|
211
|
+
args.extend(["--proxy-hostname", proxy_hostname])
|
|
212
|
+
|
|
213
|
+
for flag, container_ports in (
|
|
214
|
+
("--metrics-port", ("9090",)),
|
|
215
|
+
("--metrics-https-port", ("443",)),
|
|
216
|
+
("--webrtc-port", ("9093",)),
|
|
217
|
+
("--quic-port", ("9094",)),
|
|
218
|
+
):
|
|
219
|
+
value = host_port(*container_ports)
|
|
220
|
+
if value:
|
|
221
|
+
args.extend([flag, str(value)])
|
|
222
|
+
|
|
223
|
+
registration_id = str(bootstrap.get("registrationId") or "").strip()
|
|
224
|
+
if registration_id:
|
|
225
|
+
args.extend(["--bootstrap-registration-id", registration_id])
|
|
226
|
+
|
|
227
|
+
owner_address = str(record.get("ownerAddress") or "").strip()
|
|
228
|
+
if owner_address:
|
|
229
|
+
args.extend(["--bootstrap-owner-address", owner_address])
|
|
230
|
+
|
|
231
|
+
owner_authorization = str(bootstrap.get("ownerAuthorizationBase64") or "").strip()
|
|
232
|
+
if owner_authorization:
|
|
233
|
+
args.extend(["--bootstrap-owner-authorization-b64", owner_authorization])
|
|
234
|
+
|
|
235
|
+
return args
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _run_configure_process(args: list[str], bootstrap_record: dict | None = None) -> tuple[bool, str]:
|
|
239
|
+
with CONFIGURE_LOCK:
|
|
240
|
+
if os.path.exists(READY_FILE):
|
|
241
|
+
return True, "ready"
|
|
242
|
+
try:
|
|
243
|
+
result = subprocess.run(args, check=True, capture_output=True, text=True)
|
|
244
|
+
except subprocess.CalledProcessError as error:
|
|
245
|
+
return False, error.stderr.strip() or error.stdout.strip() or str(error)
|
|
246
|
+
|
|
247
|
+
_clear_metadata_state()
|
|
248
|
+
_generate_metadata_files()
|
|
249
|
+
|
|
250
|
+
if isinstance(bootstrap_record, dict):
|
|
251
|
+
try:
|
|
252
|
+
_publish_vm_bootstrap_config_signal(bootstrap_record)
|
|
253
|
+
except Exception as error: # pragma: no cover - runtime error path
|
|
254
|
+
return False, f"configured relay but failed to publish bootstrap signal: {error}"
|
|
255
|
+
return True, result.stdout.strip()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _publish_vm_bootstrap_config_signal(record: dict) -> None:
|
|
259
|
+
"""Tell the deployer we applied the config; it blocks until this appears."""
|
|
260
|
+
env_values = _parse_env_file(ENV_FILE)
|
|
261
|
+
publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
|
|
262
|
+
if not publisher_private_key:
|
|
263
|
+
raise RuntimeError("missing ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY")
|
|
264
|
+
|
|
265
|
+
owner_authorization = None
|
|
266
|
+
owner_authorization_b64 = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
|
|
267
|
+
if owner_authorization_b64:
|
|
268
|
+
try:
|
|
269
|
+
parsed = json.loads(base64.b64decode(owner_authorization_b64).decode("utf-8"))
|
|
270
|
+
if isinstance(parsed, dict):
|
|
271
|
+
owner_authorization = parsed
|
|
272
|
+
except (ValueError, json.JSONDecodeError):
|
|
273
|
+
owner_authorization = None
|
|
274
|
+
|
|
275
|
+
owner_address = str(record.get("ownerAddress") or "").strip()
|
|
276
|
+
deployment_token = str(record.get("deploymentToken") or "").strip()
|
|
277
|
+
profile = str(record.get("profile") or "").strip()
|
|
278
|
+
instance_item_hash = str(record.get("instanceItemHash") or "").strip()
|
|
279
|
+
if not deployment_token or not profile or not instance_item_hash:
|
|
280
|
+
raise RuntimeError("bootstrap config signal is missing deployment metadata")
|
|
281
|
+
|
|
282
|
+
metadata, metadata_error = _fresh_metadata_payload()
|
|
283
|
+
if not metadata:
|
|
284
|
+
raise RuntimeError(
|
|
285
|
+
f"bootstrap config signal metadata is not ready: {metadata_error or 'unavailable'}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
peer_id = metadata.get("peer_id")
|
|
289
|
+
probe_multiaddrs = metadata.get("probe_multiaddrs")
|
|
290
|
+
browser_bootstrap_multiaddrs = metadata.get("browser_bootstrap_multiaddrs")
|
|
291
|
+
if not isinstance(peer_id, str) or not peer_id.strip():
|
|
292
|
+
raise RuntimeError("bootstrap config signal is missing peer_id")
|
|
293
|
+
if not isinstance(probe_multiaddrs, list) or not any(
|
|
294
|
+
isinstance(entry, str) and entry.strip() for entry in probe_multiaddrs
|
|
295
|
+
):
|
|
296
|
+
raise RuntimeError("bootstrap config signal is missing probe_multiaddrs")
|
|
297
|
+
if not isinstance(browser_bootstrap_multiaddrs, list):
|
|
298
|
+
browser_bootstrap_multiaddrs = []
|
|
299
|
+
|
|
300
|
+
publisher_address = _address_from_private_key(publisher_private_key)
|
|
301
|
+
now_ms = int(time.time() * 1000)
|
|
302
|
+
content = {
|
|
303
|
+
"type": BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE,
|
|
304
|
+
"address": publisher_address,
|
|
305
|
+
"ref": BOOTSTRAP_CONFIG_SIGNAL_REF,
|
|
306
|
+
"content": {
|
|
307
|
+
"deploymentToken": deployment_token,
|
|
308
|
+
"status": "applied",
|
|
309
|
+
"profile": profile,
|
|
310
|
+
"ownerAddress": owner_address,
|
|
311
|
+
"instanceItemHash": instance_item_hash,
|
|
312
|
+
"updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)),
|
|
313
|
+
"publisherAddress": publisher_address,
|
|
314
|
+
"authorization": owner_authorization,
|
|
315
|
+
"peerId": peer_id.strip(),
|
|
316
|
+
"probeMultiaddrs": [
|
|
317
|
+
entry.strip()
|
|
318
|
+
for entry in probe_multiaddrs
|
|
319
|
+
if isinstance(entry, str) and entry.strip()
|
|
320
|
+
],
|
|
321
|
+
"browserBootstrapMultiaddrs": [
|
|
322
|
+
entry.strip()
|
|
323
|
+
for entry in browser_bootstrap_multiaddrs
|
|
324
|
+
if isinstance(entry, str) and entry.strip()
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
"time": now_ms,
|
|
328
|
+
}
|
|
329
|
+
item_content = _json_dumps(content)
|
|
330
|
+
message = {
|
|
331
|
+
"channel": BOOTSTRAP_CHANNEL,
|
|
332
|
+
"sender": publisher_address,
|
|
333
|
+
"chain": "ETH",
|
|
334
|
+
"type": "POST",
|
|
335
|
+
"time": now_ms / 1000,
|
|
336
|
+
"item_type": "inline",
|
|
337
|
+
"item_content": item_content,
|
|
338
|
+
"item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
|
|
339
|
+
}
|
|
340
|
+
message["signature"] = _sign_personal_message(
|
|
341
|
+
publisher_private_key,
|
|
342
|
+
_signature_payload(
|
|
343
|
+
str(message["chain"]),
|
|
344
|
+
str(message["sender"]),
|
|
345
|
+
str(message["type"]),
|
|
346
|
+
str(message["item_hash"]),
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
_broadcast_aleph_message(ALEPH_API_HOST, message)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
|
|
353
|
+
owner_address, deployment_token = _extract_guest_bootstrap_locator()
|
|
354
|
+
if not owner_address or not deployment_token:
|
|
355
|
+
return owner_address, deployment_token, None
|
|
356
|
+
|
|
357
|
+
request_url = (
|
|
358
|
+
f"{ALEPH_API_HOST.rstrip('/')}/api/v0/aggregates/{owner_address}.json?"
|
|
359
|
+
f"{urlencode({'keys': BOOTSTRAP_CONFIG_KEY})}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
with urlopen(request_url, timeout=10) as response:
|
|
364
|
+
payload = json.loads(response.read().decode("utf-8") or "{}")
|
|
365
|
+
except (HTTPError, URLError, json.JSONDecodeError, TimeoutError):
|
|
366
|
+
return owner_address, deployment_token, None
|
|
367
|
+
|
|
368
|
+
aggregate = payload.get("data", {}).get(BOOTSTRAP_CONFIG_KEY, {})
|
|
369
|
+
if not isinstance(aggregate, dict):
|
|
370
|
+
return owner_address, deployment_token, None
|
|
371
|
+
|
|
372
|
+
record = aggregate.get(deployment_token)
|
|
373
|
+
return owner_address, deployment_token, record if isinstance(record, dict) else None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _poll_bootstrap_config_loop() -> None:
|
|
377
|
+
while not os.path.exists(READY_FILE):
|
|
378
|
+
_, deployment_token, record = _load_vm_bootstrap_record()
|
|
379
|
+
if deployment_token and isinstance(record, dict):
|
|
380
|
+
status = str(record.get("status") or "").strip().lower()
|
|
381
|
+
if status == "pending":
|
|
382
|
+
try:
|
|
383
|
+
args = _build_configure_args_from_record(record)
|
|
384
|
+
except ValueError:
|
|
385
|
+
time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
|
|
386
|
+
continue
|
|
387
|
+
ok, _ = _run_configure_process(args, record)
|
|
388
|
+
if ok:
|
|
389
|
+
return
|
|
390
|
+
time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
|
|
391
|
+
|
|
392
|
+
|
|
52
393
|
class Handler(BaseHTTPRequestHandler):
|
|
53
394
|
server_version = "OrbitdbRelaySetup/1.0"
|
|
54
395
|
|
|
@@ -143,7 +484,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
143
484
|
bootstrap_publisher_libp2p_identity_hex = payload.get(
|
|
144
485
|
"bootstrap_publisher_libp2p_identity_hex"
|
|
145
486
|
)
|
|
146
|
-
bootstrap_owner_private_key
|
|
487
|
+
# NOTE: `bootstrap_owner_private_key` is deliberately NOT accepted
|
|
488
|
+
# here. This endpoint is plain HTTP (it runs before Caddy/TLS
|
|
489
|
+
# exists), so anything sent to it crosses the network in the
|
|
490
|
+
# clear. The owner only ever needs to hand the guest a *signed
|
|
491
|
+
# authorization*, never the key itself.
|
|
147
492
|
bootstrap_owner_authorization_b64 = payload.get("bootstrap_owner_authorization_b64")
|
|
148
493
|
bootstrap_registration_id = payload.get("bootstrap_registration_id")
|
|
149
494
|
no_start = bool(payload.get("no_start"))
|
|
@@ -182,8 +527,6 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
182
527
|
str(bootstrap_publisher_libp2p_identity_hex),
|
|
183
528
|
]
|
|
184
529
|
)
|
|
185
|
-
if bootstrap_owner_private_key is not None:
|
|
186
|
-
args.extend(["--bootstrap-owner-private-key", str(bootstrap_owner_private_key)])
|
|
187
530
|
if bootstrap_owner_authorization_b64 is not None:
|
|
188
531
|
args.extend(
|
|
189
532
|
["--bootstrap-owner-authorization-b64", str(bootstrap_owner_authorization_b64)]
|
|
@@ -256,6 +599,13 @@ def _generate_metadata_files() -> None:
|
|
|
256
599
|
|
|
257
600
|
|
|
258
601
|
def main() -> None:
|
|
602
|
+
# Poll for our own Aleph bootstrap config alongside the HTTP listener, so
|
|
603
|
+
# a deployer that cannot reach this plain-HTTP endpoint (any page served
|
|
604
|
+
# over HTTPS) can hand the configuration over through Aleph instead. The
|
|
605
|
+
# first path to succeed wins; the other becomes a no-op once READY_FILE
|
|
606
|
+
# exists and CONFIGURE_LOCK serialises them.
|
|
607
|
+
threading.Thread(target=_poll_bootstrap_config_loop, daemon=True).start()
|
|
608
|
+
|
|
259
609
|
server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
|
|
260
610
|
server.serve_forever()
|
|
261
611
|
|