@le-space/rootfs 0.6.1 → 0.6.2
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/README.md +4 -0
- package/package.json +1 -1
- package/reference/ucan-store/README.md +89 -0
- package/reference/ucan-store/contract.json +46 -0
- package/reference/ucan-store/rootfs/Dockerfile.rootfs +24 -0
- package/reference/ucan-store/rootfs/README.md +84 -0
- package/reference/ucan-store/rootfs/build-rootfs-image.sh +127 -0
- package/reference/ucan-store/rootfs/build-rootfs.sh +242 -0
- package/reference/ucan-store/rootfs/read-rootfs-contract.py +72 -0
- package/reference/ucan-store/rootfs/ucan-store-autotls-refresh.service +12 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap-verify.mjs +409 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap.service +21 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap.sh +155 -0
- package/reference/ucan-store/rootfs/ucan-store-configure.sh +261 -0
- package/reference/ucan-store/rootfs/ucan-store-describe.py +150 -0
- package/reference/ucan-store/rootfs/ucan-store-request-guard.mjs +740 -0
- package/reference/ucan-store/rootfs/ucan-store-service-start.sh +158 -0
- package/reference/ucan-store/rootfs/ucan-store-setup-server.py +229 -0
- package/reference/ucan-store/rootfs/ucan-store-upload-api-context.js +108 -0
- package/reference/ucan-store/rootfs/ucan-store-upload-api-service-identity.js +116 -0
- package/reference/ucan-store/rootfs/ucan-store.service +19 -0
- package/reference/ucan-store/rootfs/ucan_store_bootstrap_validate.py +226 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ENV_FILE="${ENV_FILE:-/etc/default/ucan-store}"
|
|
5
|
+
APP_BINARY="${APP_BINARY:-/usr/local/bin/ucan-store}"
|
|
6
|
+
BOOTSTRAP_VALIDATOR="${BOOTSTRAP_VALIDATOR:-/usr/local/sbin/ucan_store_bootstrap_validate.py}"
|
|
7
|
+
BOOTSTRAP_CRYPTO_VERIFIER="${BOOTSTRAP_CRYPTO_VERIFIER:-/usr/local/sbin/ucan-store-bootstrap-verify.mjs}"
|
|
8
|
+
REQUEST_GUARD="${REQUEST_GUARD:-/usr/local/sbin/ucan-store-request-guard.mjs}"
|
|
9
|
+
BOOTSTRAP_PACKAGE_FILE_DEFAULT="${BOOTSTRAP_PACKAGE_FILE:-/etc/ucan-store/bootstrap-package.json}"
|
|
10
|
+
BOOTSTRAP_VERIFICATION_FILE_DEFAULT="${BOOTSTRAP_VERIFICATION_FILE:-/etc/ucan-store/bootstrap-verification.json}"
|
|
11
|
+
VERIFY_TIMEOUT_DEFAULT="${BOOTSTRAP_VERIFICATION_TIMEOUT_SECONDS:-60}"
|
|
12
|
+
VERIFY_INTERVAL_DEFAULT="${BOOTSTRAP_VERIFICATION_INTERVAL_SECONDS:-2}"
|
|
13
|
+
APP_PID=""
|
|
14
|
+
GUARD_PID=""
|
|
15
|
+
|
|
16
|
+
if [ -f "${ENV_FILE}" ]; then
|
|
17
|
+
set -a
|
|
18
|
+
# shellcheck disable=SC1090
|
|
19
|
+
. "${ENV_FILE}"
|
|
20
|
+
set +a
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
SERVICE_PORT="${STORACHA_LOCAL_PORT:-8787}"
|
|
24
|
+
BOOTSTRAP_PACKAGE_FILE="${UCAN_STORE_BOOTSTRAP_PACKAGE_FILE:-${BOOTSTRAP_PACKAGE_FILE_DEFAULT}}"
|
|
25
|
+
BOOTSTRAP_VERIFICATION_FILE="${UCAN_STORE_BOOTSTRAP_VERIFICATION_FILE:-${BOOTSTRAP_VERIFICATION_FILE_DEFAULT}}"
|
|
26
|
+
PUBLIC_UPLOAD_SERVICE_URL="${PUBLIC_UPLOAD_SERVICE_URL:-}"
|
|
27
|
+
ADMIN_DID="${UCAN_STORE_ADMIN_DID:-}"
|
|
28
|
+
VERIFY_TIMEOUT_SECONDS="${BOOTSTRAP_VERIFICATION_TIMEOUT_SECONDS:-${VERIFY_TIMEOUT_DEFAULT}}"
|
|
29
|
+
VERIFY_INTERVAL_SECONDS="${BOOTSTRAP_VERIFICATION_INTERVAL_SECONDS:-${VERIFY_INTERVAL_DEFAULT}}"
|
|
30
|
+
REQUIRE_BOOTSTRAP_PACKAGE="${UCAN_STORE_REQUIRE_BOOTSTRAP_PACKAGE:-1}"
|
|
31
|
+
BOOTSTRAP_VALIDATION_MODE="enforce"
|
|
32
|
+
|
|
33
|
+
is_truthy() {
|
|
34
|
+
case "${1:-}" in
|
|
35
|
+
1|true|TRUE|yes|YES|on|ON)
|
|
36
|
+
return 0
|
|
37
|
+
;;
|
|
38
|
+
*)
|
|
39
|
+
return 1
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
terminate_child() {
|
|
45
|
+
if [ -n "${APP_PID}" ] && kill -0 "${APP_PID}" 2>/dev/null; then
|
|
46
|
+
kill "${APP_PID}" 2>/dev/null || true
|
|
47
|
+
wait "${APP_PID}" 2>/dev/null || true
|
|
48
|
+
fi
|
|
49
|
+
if [ -n "${GUARD_PID}" ] && kill -0 "${GUARD_PID}" 2>/dev/null; then
|
|
50
|
+
kill "${GUARD_PID}" 2>/dev/null || true
|
|
51
|
+
wait "${GUARD_PID}" 2>/dev/null || true
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
validate_bootstrap_preflight() {
|
|
56
|
+
if [ ! -f "${BOOTSTRAP_PACKAGE_FILE}" ]; then
|
|
57
|
+
if is_truthy "${REQUIRE_BOOTSTRAP_PACKAGE}"; then
|
|
58
|
+
echo "Missing required bootstrap package: ${BOOTSTRAP_PACKAGE_FILE}" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
BOOTSTRAP_VALIDATION_MODE="skip"
|
|
62
|
+
echo "Bootstrap package not configured; skipping startup validation." >&2
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if ! python3 "${BOOTSTRAP_VALIDATOR}" \
|
|
67
|
+
--package-file "${BOOTSTRAP_PACKAGE_FILE}" \
|
|
68
|
+
--runtime-service-origin "${PUBLIC_UPLOAD_SERVICE_URL}" \
|
|
69
|
+
--admin-did "${ADMIN_DID}" >/dev/null; then
|
|
70
|
+
echo "Bootstrap package preflight validation failed." >&2
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if ! node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
|
|
75
|
+
--package-file "${BOOTSTRAP_PACKAGE_FILE}" \
|
|
76
|
+
--runtime-service-origin "${PUBLIC_UPLOAD_SERVICE_URL}" \
|
|
77
|
+
--admin-did "${ADMIN_DID}" \
|
|
78
|
+
--summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null; then
|
|
79
|
+
echo "Bootstrap delegation cryptographic preflight validation failed." >&2
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
probe_service_did() {
|
|
85
|
+
python3 - "${SERVICE_PORT}" "${VERIFY_TIMEOUT_SECONDS}" "${VERIFY_INTERVAL_SECONDS}" <<'PY'
|
|
86
|
+
import json
|
|
87
|
+
import sys
|
|
88
|
+
import time
|
|
89
|
+
import urllib.error
|
|
90
|
+
import urllib.request
|
|
91
|
+
|
|
92
|
+
port = int(sys.argv[1])
|
|
93
|
+
timeout_seconds = float(sys.argv[2])
|
|
94
|
+
interval_seconds = float(sys.argv[3])
|
|
95
|
+
url = f"http://127.0.0.1:{port}/.well-known/did.json"
|
|
96
|
+
deadline = time.monotonic() + timeout_seconds
|
|
97
|
+
last_error = None
|
|
98
|
+
|
|
99
|
+
while time.monotonic() < deadline:
|
|
100
|
+
try:
|
|
101
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
102
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
103
|
+
did = payload.get("id")
|
|
104
|
+
if isinstance(did, str) and did.strip():
|
|
105
|
+
print(did.strip())
|
|
106
|
+
raise SystemExit(0)
|
|
107
|
+
last_error = "did.json missing id"
|
|
108
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
|
|
109
|
+
last_error = str(error)
|
|
110
|
+
time.sleep(interval_seconds)
|
|
111
|
+
|
|
112
|
+
raise SystemExit(last_error or "service DID probe timed out")
|
|
113
|
+
PY
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
trap terminate_child EXIT INT TERM
|
|
117
|
+
validate_bootstrap_preflight
|
|
118
|
+
|
|
119
|
+
"${APP_BINARY}" "$@" &
|
|
120
|
+
APP_PID="$!"
|
|
121
|
+
|
|
122
|
+
if [ "${BOOTSTRAP_VALIDATION_MODE}" = "enforce" ]; then
|
|
123
|
+
SERVICE_DID="$(probe_service_did)"
|
|
124
|
+
RUNTIME_SERVICE_ORIGIN=""
|
|
125
|
+
if [ -n "${PUBLIC_UPLOAD_SERVICE_URL}" ]; then
|
|
126
|
+
RUNTIME_SERVICE_ORIGIN="$(python3 - <<'PY'
|
|
127
|
+
import os
|
|
128
|
+
from urllib.parse import urlsplit
|
|
129
|
+
|
|
130
|
+
value = os.environ.get("PUBLIC_UPLOAD_SERVICE_URL", "").strip()
|
|
131
|
+
parts = urlsplit(value)
|
|
132
|
+
if parts.scheme and parts.netloc:
|
|
133
|
+
print(f"{parts.scheme}://{parts.netloc}")
|
|
134
|
+
PY
|
|
135
|
+
)"
|
|
136
|
+
fi
|
|
137
|
+
python3 "${BOOTSTRAP_VALIDATOR}" \
|
|
138
|
+
--package-file "${BOOTSTRAP_PACKAGE_FILE}" \
|
|
139
|
+
--runtime-service-did "${SERVICE_DID}" \
|
|
140
|
+
--runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
|
|
141
|
+
--admin-did "${ADMIN_DID}" >/dev/null
|
|
142
|
+
node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
|
|
143
|
+
--package-file "${BOOTSTRAP_PACKAGE_FILE}" \
|
|
144
|
+
--runtime-service-did "${SERVICE_DID}" \
|
|
145
|
+
--runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
|
|
146
|
+
--admin-did "${ADMIN_DID}" \
|
|
147
|
+
--summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
node "${REQUEST_GUARD}" &
|
|
151
|
+
GUARD_PID="$!"
|
|
152
|
+
|
|
153
|
+
set +e
|
|
154
|
+
wait -n "${APP_PID}" "${GUARD_PID}"
|
|
155
|
+
STATUS="$?"
|
|
156
|
+
set -e
|
|
157
|
+
terminate_child
|
|
158
|
+
exit "${STATUS}"
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import threading
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/ucan-store")
|
|
12
|
+
READY_FILE = os.environ.get("READY_FILE", "/etc/default/ucan-store.ready")
|
|
13
|
+
SERVICE_NAME = os.environ.get("SERVICE_NAME", "ucan-store.service")
|
|
14
|
+
BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "ucan-store-bootstrap.service")
|
|
15
|
+
CONFIGURE_SCRIPT = "/usr/local/sbin/ucan-store-configure.sh"
|
|
16
|
+
DESCRIBE_SCRIPT = "/usr/local/sbin/ucan-store-describe.py"
|
|
17
|
+
METADATA_FILE = os.environ.get("METADATA_FILE", "/run/ucan-store-setup-metadata.json")
|
|
18
|
+
METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/ucan-store-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 _clear_metadata_state() -> None:
|
|
28
|
+
for path in (METADATA_FILE, METADATA_ERROR_FILE):
|
|
29
|
+
try:
|
|
30
|
+
os.remove(path)
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _generate_metadata_files() -> None:
|
|
36
|
+
try:
|
|
37
|
+
describe = subprocess.run(
|
|
38
|
+
[DESCRIBE_SCRIPT],
|
|
39
|
+
check=True,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
payload = json.loads(describe.stdout.strip() or "{}")
|
|
44
|
+
with open(METADATA_FILE, "w", encoding="utf-8") as handle:
|
|
45
|
+
json.dump(payload, handle)
|
|
46
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
|
|
47
|
+
with open(METADATA_ERROR_FILE, "w", encoding="utf-8") as handle:
|
|
48
|
+
handle.write(str(error))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _stop_bootstrap_service() -> None:
|
|
52
|
+
subprocess.run(["systemctl", "stop", BOOTSTRAP_SERVICE], check=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Handler(BaseHTTPRequestHandler):
|
|
56
|
+
server_version = "UcanStoreSetup/1.0"
|
|
57
|
+
|
|
58
|
+
def _request_path(self) -> str:
|
|
59
|
+
return urlsplit(self.path).path
|
|
60
|
+
|
|
61
|
+
def _send_json(self, status: int, payload: dict) -> None:
|
|
62
|
+
body = json.dumps(payload).encode("utf-8")
|
|
63
|
+
self.send_response(status)
|
|
64
|
+
_cors_headers(self)
|
|
65
|
+
self.send_header("Content-Type", "application/json")
|
|
66
|
+
self.send_header("Content-Length", str(len(body)))
|
|
67
|
+
self.end_headers()
|
|
68
|
+
self.wfile.write(body)
|
|
69
|
+
|
|
70
|
+
def log_message(self, format: str, *args) -> None: # noqa: A003
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def do_OPTIONS(self) -> None: # noqa: N802
|
|
74
|
+
self.send_response(204)
|
|
75
|
+
_cors_headers(self)
|
|
76
|
+
self.end_headers()
|
|
77
|
+
|
|
78
|
+
def do_GET(self) -> None: # noqa: N802
|
|
79
|
+
if self._request_path() == "/metadata":
|
|
80
|
+
self._handle_metadata()
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if self._request_path() not in ("/", "/health"):
|
|
84
|
+
self._send_json(404, {"status": "not-found"})
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self._send_json(
|
|
88
|
+
200,
|
|
89
|
+
{
|
|
90
|
+
"status": "waiting-for-service-config",
|
|
91
|
+
"ready": os.path.exists(READY_FILE),
|
|
92
|
+
"env_file": ENV_FILE,
|
|
93
|
+
"metadata_ready": os.path.exists(METADATA_FILE),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _handle_metadata(self) -> None:
|
|
98
|
+
if os.path.exists(METADATA_FILE):
|
|
99
|
+
with open(METADATA_FILE, encoding="utf-8") as handle:
|
|
100
|
+
metadata = json.load(handle)
|
|
101
|
+
self._send_json(200, {"status": "ready", "metadata": metadata})
|
|
102
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
|
|
103
|
+
threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if os.path.exists(METADATA_ERROR_FILE):
|
|
107
|
+
with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
|
|
108
|
+
error_message = handle.read().strip() or "metadata generation failed"
|
|
109
|
+
self._send_json(500, {"status": "error", "error": error_message})
|
|
110
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
|
|
111
|
+
threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self._send_json(202, {"status": "pending"})
|
|
115
|
+
|
|
116
|
+
def do_POST(self) -> None: # noqa: N802
|
|
117
|
+
if self._request_path() != "/configure":
|
|
118
|
+
self._send_json(404, {"status": "not-found"})
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
content_length = int(self.headers.get("Content-Length", "0"))
|
|
123
|
+
except ValueError:
|
|
124
|
+
self._send_json(400, {"status": "bad-request", "error": "Invalid Content-Length"})
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
payload = json.loads(self.rfile.read(content_length).decode("utf-8") or "{}")
|
|
129
|
+
except json.JSONDecodeError as error:
|
|
130
|
+
self._send_json(400, {"status": "bad-request", "error": f"Invalid JSON body: {error}"})
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
public_ipv4 = str(payload.get("public_ipv4") or "").strip()
|
|
134
|
+
public_ipv6 = str(payload.get("public_ipv6") or "").strip()
|
|
135
|
+
proxy_candidate = str(payload.get("proxy_url") or "").strip()
|
|
136
|
+
parsed_proxy = urlsplit(
|
|
137
|
+
proxy_candidate if "://" in proxy_candidate else f"https://{proxy_candidate}"
|
|
138
|
+
) if proxy_candidate else None
|
|
139
|
+
proxy_hostname = parsed_proxy.hostname if parsed_proxy and parsed_proxy.hostname else proxy_candidate
|
|
140
|
+
admin_did = str(payload.get("admin_did") or "").strip()
|
|
141
|
+
webauthn_origin = str(payload.get("webauthn_origin") or "").strip()
|
|
142
|
+
webauthn_origin_fallbacks = str(payload.get("webauthn_origin_fallbacks") or "").strip()
|
|
143
|
+
no_start = bool(payload.get("no_start"))
|
|
144
|
+
bootstrap_package = payload.get("bootstrap_package")
|
|
145
|
+
|
|
146
|
+
if not public_ipv4:
|
|
147
|
+
self._send_json(400, {"status": "bad-request", "error": "public_ipv4 is required"})
|
|
148
|
+
return
|
|
149
|
+
if bootstrap_package not in (None, "") and not isinstance(bootstrap_package, dict):
|
|
150
|
+
self._send_json(
|
|
151
|
+
400,
|
|
152
|
+
{"status": "bad-request", "error": "bootstrap_package must be a JSON object when provided"},
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
args = [
|
|
157
|
+
CONFIGURE_SCRIPT,
|
|
158
|
+
"--public-ipv4",
|
|
159
|
+
public_ipv4,
|
|
160
|
+
]
|
|
161
|
+
bootstrap_package_file = None
|
|
162
|
+
if public_ipv6:
|
|
163
|
+
args.extend(["--public-ipv6", public_ipv6])
|
|
164
|
+
if proxy_hostname:
|
|
165
|
+
args.extend(["--proxy-hostname", proxy_hostname])
|
|
166
|
+
if admin_did:
|
|
167
|
+
args.extend(["--admin-did", admin_did])
|
|
168
|
+
if webauthn_origin:
|
|
169
|
+
args.extend(["--webauthn-origin", webauthn_origin])
|
|
170
|
+
if webauthn_origin_fallbacks:
|
|
171
|
+
args.extend(["--webauthn-origin-fallbacks", webauthn_origin_fallbacks])
|
|
172
|
+
if isinstance(bootstrap_package, dict):
|
|
173
|
+
with tempfile.NamedTemporaryFile(
|
|
174
|
+
mode="w",
|
|
175
|
+
encoding="utf-8",
|
|
176
|
+
prefix="ucan-store-bootstrap-",
|
|
177
|
+
suffix=".json",
|
|
178
|
+
dir="/run",
|
|
179
|
+
delete=False,
|
|
180
|
+
) as handle:
|
|
181
|
+
json.dump(bootstrap_package, handle)
|
|
182
|
+
bootstrap_package_file = handle.name
|
|
183
|
+
args.extend(["--bootstrap-package-file", bootstrap_package_file])
|
|
184
|
+
if no_start:
|
|
185
|
+
args.append("--no-start")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
result = subprocess.run(
|
|
189
|
+
args,
|
|
190
|
+
check=True,
|
|
191
|
+
capture_output=True,
|
|
192
|
+
text=True,
|
|
193
|
+
)
|
|
194
|
+
except subprocess.CalledProcessError as error:
|
|
195
|
+
self._send_json(
|
|
196
|
+
500,
|
|
197
|
+
{
|
|
198
|
+
"status": "error",
|
|
199
|
+
"error": error.stderr.strip() or error.stdout.strip() or str(error),
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
finally:
|
|
204
|
+
if bootstrap_package_file:
|
|
205
|
+
try:
|
|
206
|
+
os.remove(bootstrap_package_file)
|
|
207
|
+
except FileNotFoundError:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
_clear_metadata_state()
|
|
211
|
+
threading.Thread(target=_generate_metadata_files, daemon=True).start()
|
|
212
|
+
|
|
213
|
+
self._send_json(
|
|
214
|
+
200,
|
|
215
|
+
{
|
|
216
|
+
"status": "configured",
|
|
217
|
+
"stdout": result.stdout.strip(),
|
|
218
|
+
"metadata_pending": True,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main() -> None:
|
|
224
|
+
server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
|
|
225
|
+
server.serve_forever()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as Signer from '@ucanto/principal/ed25519';
|
|
2
|
+
import * as AggregatorCaps from '@storacha/capabilities/filecoin/aggregator';
|
|
3
|
+
import {
|
|
4
|
+
getConnection,
|
|
5
|
+
getMockService,
|
|
6
|
+
getStoreImplementations as getFilecoinStoreImplementations,
|
|
7
|
+
getQueueImplementations as getFilecoinQueueImplementations,
|
|
8
|
+
} from '@storacha/filecoin-api/test/context/service';
|
|
9
|
+
import * as Email from '../../utils/email.js';
|
|
10
|
+
import { create as createRevocationChecker } from '../../utils/revocation.js';
|
|
11
|
+
import { createServer, connect } from '../../lib.js';
|
|
12
|
+
import { confirmConfirmationUrl } from './utils.js';
|
|
13
|
+
import { getServiceStorageImplementations } from '../storage/index.js';
|
|
14
|
+
import { getExternalServiceImplementations } from '../external-service/index.js';
|
|
15
|
+
import { createServiceIdentity } from './ucan-store-service-identity.js';
|
|
16
|
+
|
|
17
|
+
export const createContext = async (options = { requirePaymentPlan: false }) => {
|
|
18
|
+
const requirePaymentPlan = options.requirePaymentPlan;
|
|
19
|
+
const { id: serviceSigner } = await createServiceIdentity(process.env);
|
|
20
|
+
const aggregatorSigner = await Signer.generate();
|
|
21
|
+
const dealTrackerSigner = await Signer.generate();
|
|
22
|
+
const service = getMockService();
|
|
23
|
+
const aggregatorServiceProof = await AggregatorCaps.pieceOffer.delegate({
|
|
24
|
+
issuer: aggregatorSigner,
|
|
25
|
+
audience: serviceSigner,
|
|
26
|
+
with: aggregatorSigner.did(),
|
|
27
|
+
expiration: Infinity,
|
|
28
|
+
});
|
|
29
|
+
const dealTrackerConnection = getConnection(dealTrackerSigner, service).connection;
|
|
30
|
+
const serviceStores = await getServiceStorageImplementations(options);
|
|
31
|
+
const queuedMessages = new Map();
|
|
32
|
+
const {
|
|
33
|
+
storefront: { filecoinSubmitQueue, pieceOfferQueue },
|
|
34
|
+
} = getFilecoinQueueImplementations(queuedMessages);
|
|
35
|
+
const {
|
|
36
|
+
storefront: { pieceStore, receiptStore, taskStore },
|
|
37
|
+
} = getFilecoinStoreImplementations();
|
|
38
|
+
const email = Email.debug();
|
|
39
|
+
const externalServices = await getExternalServiceImplementations({
|
|
40
|
+
...options,
|
|
41
|
+
serviceID: serviceSigner,
|
|
42
|
+
});
|
|
43
|
+
const serviceContext = {
|
|
44
|
+
id: serviceSigner,
|
|
45
|
+
signer: serviceSigner,
|
|
46
|
+
email,
|
|
47
|
+
requirePaymentPlan,
|
|
48
|
+
url: new URL('http://localhost:8787'),
|
|
49
|
+
...serviceStores,
|
|
50
|
+
...externalServices,
|
|
51
|
+
getServiceConnection: () => connection,
|
|
52
|
+
...createRevocationChecker({
|
|
53
|
+
revocationsStorage: serviceStores.revocationsStorage,
|
|
54
|
+
}),
|
|
55
|
+
errorReporter: {
|
|
56
|
+
catch(error) {
|
|
57
|
+
if (options.assert) {
|
|
58
|
+
options.assert.fail(error);
|
|
59
|
+
} else {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
filecoinSubmitQueue,
|
|
65
|
+
pieceOfferQueue,
|
|
66
|
+
pieceStore,
|
|
67
|
+
receiptStore,
|
|
68
|
+
taskStore,
|
|
69
|
+
aggregatorInvocationConfig: {
|
|
70
|
+
issuer: serviceSigner,
|
|
71
|
+
with: serviceSigner.did(),
|
|
72
|
+
audience: aggregatorSigner,
|
|
73
|
+
proofs: [aggregatorServiceProof],
|
|
74
|
+
},
|
|
75
|
+
dealTrackerService: {
|
|
76
|
+
connection: dealTrackerConnection,
|
|
77
|
+
invocationConfig: {
|
|
78
|
+
issuer: serviceSigner,
|
|
79
|
+
with: serviceSigner.did(),
|
|
80
|
+
audience: dealTrackerSigner,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
maxUploadSize: 5_000_000_000,
|
|
84
|
+
};
|
|
85
|
+
const connection = connect({
|
|
86
|
+
id: serviceContext.id,
|
|
87
|
+
channel: createServer(serviceContext),
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
...serviceContext,
|
|
91
|
+
...serviceStores,
|
|
92
|
+
...externalServices,
|
|
93
|
+
mail: serviceContext.email,
|
|
94
|
+
service: serviceContext.id,
|
|
95
|
+
connection,
|
|
96
|
+
grantAccess: (mail) => confirmConfirmationUrl(connection, mail),
|
|
97
|
+
fetch,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const cleanupContext = (context) =>
|
|
102
|
+
Promise.all([
|
|
103
|
+
context.carStoreBucket.deactivate(),
|
|
104
|
+
context.blobsStorage.deactivate(),
|
|
105
|
+
context.indexingService.deactivate(),
|
|
106
|
+
context.claimsService.deactivate(),
|
|
107
|
+
...context.storageProviders.map((provider) => provider.deactivate()),
|
|
108
|
+
]);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as Signer from '@ucanto/principal/ed25519';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SIGNER_FILE = '/var/lib/ucan-store/service-ed25519.key';
|
|
6
|
+
const DEFAULT_ALGORITHM = 'ed25519';
|
|
7
|
+
|
|
8
|
+
const normalizeString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
9
|
+
|
|
10
|
+
const parseUrlLike = (value) => {
|
|
11
|
+
const normalized = normalizeString(value);
|
|
12
|
+
if (!normalized) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return new URL(normalized);
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
return new URL(`https://${normalized}`);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const didWebFromUrlLike = (value) => {
|
|
28
|
+
const parsed = parseUrlLike(value);
|
|
29
|
+
if (!parsed || !parsed.hostname) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const host = parsed.hostname.toLowerCase();
|
|
34
|
+
const port = parsed.port ? `%3A${parsed.port}` : '';
|
|
35
|
+
return `did:web:${host}${port}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveServiceDid = (env, signer) => {
|
|
39
|
+
const explicitDid =
|
|
40
|
+
normalizeString(env.UCAN_STORE_SERVICE_DID) ||
|
|
41
|
+
normalizeString(env.PUBLIC_UPLOAD_SERVICE_DID);
|
|
42
|
+
if (explicitDid) {
|
|
43
|
+
return explicitDid;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hostnameDid = didWebFromUrlLike(env.PROXY_HOSTNAME);
|
|
47
|
+
if (hostnameDid) {
|
|
48
|
+
return hostnameDid;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const publicUrlDid = didWebFromUrlLike(env.PUBLIC_UPLOAD_SERVICE_URL);
|
|
52
|
+
if (publicUrlDid) {
|
|
53
|
+
return publicUrlDid;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return signer.did();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const writeSignerFile = async (signerFile, signer) => {
|
|
60
|
+
const parentDir = path.dirname(signerFile);
|
|
61
|
+
const encoded = `${Signer.format(signer)}\n`;
|
|
62
|
+
const tempFile = `${signerFile}.tmp-${process.pid}-${Date.now()}`;
|
|
63
|
+
|
|
64
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
65
|
+
await fs.writeFile(tempFile, encoded, { mode: 0o600 });
|
|
66
|
+
await fs.rename(tempFile, signerFile);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const loadOrCreateServiceSigner = async (env = process.env) => {
|
|
70
|
+
const algorithm = normalizeString(env.UCAN_STORE_SERVICE_KEY_ALGORITHM) || DEFAULT_ALGORITHM;
|
|
71
|
+
if (algorithm !== DEFAULT_ALGORITHM) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unsupported UCAN store service signer algorithm: ${algorithm}. Only ${DEFAULT_ALGORITHM} is currently supported.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signerFile = normalizeString(env.UCAN_STORE_SERVICE_SIGNER_FILE) || DEFAULT_SIGNER_FILE;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const existing = normalizeString(await fs.readFile(signerFile, 'utf8'));
|
|
81
|
+
if (!existing) {
|
|
82
|
+
throw new Error(`Service signer file is empty: ${signerFile}`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
signer: Signer.parse(existing),
|
|
86
|
+
signerFile,
|
|
87
|
+
algorithm,
|
|
88
|
+
created: false,
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code !== 'ENOENT') {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const signer = await Signer.generate();
|
|
97
|
+
await writeSignerFile(signerFile, signer);
|
|
98
|
+
return {
|
|
99
|
+
signer,
|
|
100
|
+
signerFile,
|
|
101
|
+
algorithm,
|
|
102
|
+
created: true,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const createServiceIdentity = async (env = process.env) => {
|
|
107
|
+
const { signer, signerFile, algorithm, created } = await loadOrCreateServiceSigner(env);
|
|
108
|
+
const serviceDid = resolveServiceDid(env, signer);
|
|
109
|
+
return {
|
|
110
|
+
signer,
|
|
111
|
+
id: serviceDid === signer.did() ? signer : signer.withDID(serviceDid),
|
|
112
|
+
signerFile,
|
|
113
|
+
algorithm,
|
|
114
|
+
created,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=ucan-store upload service
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target
|
|
5
|
+
ConditionPathExists=/etc/default/ucan-store.ready
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=ucan-store
|
|
10
|
+
Group=ucan-store
|
|
11
|
+
WorkingDirectory=/opt/ucan-store
|
|
12
|
+
EnvironmentFile=-/etc/default/ucan-store
|
|
13
|
+
ExecStart=/usr/local/sbin/ucan-store-service-start.sh
|
|
14
|
+
Restart=always
|
|
15
|
+
RestartSec=5
|
|
16
|
+
NoNewPrivileges=true
|
|
17
|
+
|
|
18
|
+
[Install]
|
|
19
|
+
WantedBy=multi-user.target
|