迁移为自部署 Rust 后端

This commit is contained in:
imengying committed 2026-09-04 00:37:21 +08:00
1 parent 384dcddc01
commit dae8f665db
87 files changed
+10789 -12428

No files matched your search

+1 -2
View File
@@ -4,8 +4,7 @@ set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
root_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
NODEFLARE_VERSION=$(sh "$script_dir/resolve-version.sh")
NODEFLARE_NATIVE_RUST_ONLY=1
export NODEFLARE_NATIVE_RUST_ONLY NODEFLARE_VERSION
export NODEFLARE_VERSION
cd "$root_dir"
. "$script_dir/ensure-rust.sh"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
root_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
NODEFLARE_VERSION=$(sh "$script_dir/resolve-version.sh")
export NODEFLARE_VERSION
cd "$root_dir"
. "$script_dir/ensure-rust.sh"
cargo build --manifest-path backend/Cargo.toml --release --locked
+1
View File
@@ -8,6 +8,7 @@ VITE_NODEFLARE_VERSION=$NODEFLARE_VERSION
export VITE_NODEFLARE_VERSION
cd "$root_dir"
[ -d node_modules ] || bun install --frozen-lockfile
bun run --cwd frontend build
if build_key=$(sh scripts/build-key.sh frontend 2>/dev/null); then
+3 -3
View File
@@ -5,11 +5,11 @@ case "${1:-}" in
frontend)
paths="frontend package.json bun.lock scripts/build-frontend.sh scripts/build-key.sh scripts/resolve-version.sh"
;;
worker)
paths="src frontend Cargo.toml Cargo.lock build.rs scripts/build-worker.sh scripts/build-key.sh scripts/resolve-version.sh scripts/worker-shim.js"
backend)
paths="backend/Cargo.toml backend/Cargo.lock backend/src backend/migrations scripts/build-backend.sh scripts/build-key.sh scripts/resolve-version.sh"
;;
*)
echo "usage: $0 frontend|worker" >&2
echo "usage: $0 frontend|backend" >&2
exit 2
;;
esac
-40
View File
@@ -1,40 +0,0 @@
#!/bin/sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
root_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
NODEFLARE_VERSION=$(sh "$script_dir/resolve-version.sh")
export NODEFLARE_VERSION
cd "$root_dir"
current_build_key=$(sh scripts/build-key.sh worker 2>/dev/null || true)
if [ "${NODEFLARE_WORKER_READY:-0}" = "1" ] && [ -n "$current_build_key" ]; then
built_build_key=$(sed -n '1p' build/.nodeflare-revision 2>/dev/null || true)
if [ "$built_build_key" = "$current_build_key" ] \
&& [ -f build/index.js ] \
&& [ -f build/worker/index.wasm ]; then
echo "Worker build output is ready; skipping rebuild."
exit 0
fi
fi
. "$script_dir/ensure-rust.sh"
if [ "${NODEFLARE_ADMIN_READY:-0}" != "1" ]; then
bun run build:frontend
fi
if command -v worker-build >/dev/null 2>&1; then
worker_build="$(command -v worker-build)"
elif [ -x "${CARGO_HOME:-$HOME/.cargo}/bin/worker-build" ]; then
worker_build="${CARGO_HOME:-$HOME/.cargo}/bin/worker-build"
else
cargo install -q "worker-build@^0.8"
worker_build="${CARGO_HOME:-$HOME/.cargo}/bin/worker-build"
fi
CUSTOM_SHIM="$script_dir/worker-shim.js" "$worker_build" --release --no-panic-recovery
if [ -n "$current_build_key" ]; then
printf '%s\n' "$current_build_key" > build/.nodeflare-revision
fi
-100
View File
@@ -1,100 +0,0 @@
#!/bin/sh
set -eu
runtime_secrets_file=""
cleanup() {
if [ -n "$runtime_secrets_file" ]; then
rm -f "$runtime_secrets_file"
fi
}
trap cleanup EXIT HUP INT TERM
validate_admin_username() {
if [ -z "${ADMIN_USERNAME:-}" ]; then
echo "ADMIN_USERNAME is required" >&2
exit 1
fi
if [ "${#ADMIN_USERNAME}" -gt 64 ]; then
echo "ADMIN_USERNAME must not exceed 64 characters" >&2
exit 1
fi
case "$ADMIN_USERNAME" in
*[[:space:]]*)
echo "ADMIN_USERNAME must not contain whitespace" >&2
exit 1
;;
esac
}
frontend_ready() {
build_key=$(sh scripts/build-key.sh frontend 2>/dev/null || true)
built_build_key=$(sed -n '1p' frontend/dist/.nodeflare-revision 2>/dev/null || true)
[ -n "$build_key" ] \
&& [ "$built_build_key" = "$build_key" ] \
&& [ -f frontend/dist/index.html ] \
&& [ -f frontend/admin-dist/admin.js ]
}
worker_ready() {
build_key=$(sh scripts/build-key.sh worker 2>/dev/null || true)
built_build_key=$(sed -n '1p' build/.nodeflare-revision 2>/dev/null || true)
[ -n "$build_key" ] \
&& [ "$built_build_key" = "$build_key" ] \
&& [ -f build/index.js ] \
&& [ -f build/worker/index.wasm ]
}
prepare_runtime_secrets() {
runtime_secrets_file=$(mktemp)
RUNTIME_SECRETS_FILE="$runtime_secrets_file" bun -e '
const names = ["ADMIN_PASSWORD", "TURNSTILE_SECRET_KEY", "CF_USAGE_API_TOKEN"];
const secrets = Object.fromEntries(
names.flatMap((name) => process.env[name] ? [[name, process.env[name]]] : []),
);
if (Object.keys(secrets).length > 0) {
await Bun.write(process.env.RUNTIME_SECRETS_FILE, JSON.stringify(secrets));
}
'
if [ ! -s "$runtime_secrets_file" ]; then
rm -f "$runtime_secrets_file"
runtime_secrets_file=""
fi
}
deploy_worker() {
set -- bunx wrangler deploy
set -- "$@" --var "ADMIN_USERNAME:${ADMIN_USERNAME}"
[ -z "${SITE_NAME:-}" ] || set -- "$@" --var "SITE_NAME:${SITE_NAME}"
[ -z "${TURNSTILE_SITE_KEY:-}" ] || set -- "$@" --var "TURNSTILE_SITE_KEY:${TURNSTILE_SITE_KEY}"
[ -z "${OFFLINE_THRESHOLD_SECONDS:-}" ] || set -- "$@" --var "OFFLINE_THRESHOLD_SECONDS:${OFFLINE_THRESHOLD_SECONDS}"
[ -z "${HISTORY_RETENTION_DAYS:-}" ] || set -- "$@" --var "HISTORY_RETENTION_DAYS:${HISTORY_RETENTION_DAYS}"
[ -z "${CF_USAGE_ACCOUNT_ID:-}" ] || set -- "$@" --var "CF_USAGE_ACCOUNT_ID:${CF_USAGE_ACCOUNT_ID}"
[ -z "$runtime_secrets_file" ] || set -- "$@" --secrets-file "$runtime_secrets_file"
if worker_ready; then
NODEFLARE_ADMIN_READY=1 NODEFLARE_WORKER_READY=1 "$@"
else
NODEFLARE_ADMIN_READY=1 "$@"
fi
}
validate_admin_username
if ! frontend_ready; then
bun run build:frontend
fi
prepare_runtime_secrets
# Workers Builds provisions this binding before running the deploy command. A
# direct first deployment does not, so provision the Worker first when the
# default database is not present. The first branch uploads only once.
if bunx wrangler d1 info nodeflare --json >/dev/null 2>&1; then
bunx wrangler d1 migrations apply DB --remote
deploy_worker
else
deploy_worker
bunx wrangler d1 migrations apply DB --remote
fi
+1 -10
View File
@@ -7,15 +7,9 @@ case ":$PATH:" in
*) PATH="$rust_bin_dir:$PATH"; export PATH ;;
esac
wasm_target_ready() {
command -v rustc >/dev/null 2>&1 || return 1
target_libdir=$(rustc --print target-libdir --target wasm32-unknown-unknown 2>/dev/null) || return 1
test -d "$target_libdir"
}
rust_ready() {
command -v cargo >/dev/null 2>&1 || return 1
[ "${NODEFLARE_NATIVE_RUST_ONLY:-0}" = "1" ] || wasm_target_ready
command -v rustc >/dev/null 2>&1
}
if ! rust_ready; then
@@ -41,9 +35,6 @@ if ! rust_ready; then
rustup toolchain install stable --profile minimal
rustup default stable
fi
if [ "${NODEFLARE_NATIVE_RUST_ONLY:-0}" != "1" ]; then
rustup target add wasm32-unknown-unknown
fi
fi
if ! rust_ready; then
-11
View File
@@ -11,17 +11,6 @@ fi
if [ -z "$raw" ]; then
raw=$(git -C "$root_dir" describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || true)
fi
if [ -z "$raw" ]; then
remote_version=$(
git -C "$root_dir" ls-remote --tags --refs origin \
'refs/tags/v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null |
awk '{ sub(/^refs\/tags\//, "", $2); print $2 }' |
grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' |
sort -V |
tail -n 1
) || remote_version=
raw=$remote_version
fi
if [ -z "$raw" ]; then
raw=$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",*$/\1/p' "$root_dir/package.json" | sed -n '1p')
fi
+27 -12
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
MONITOR_BASE_URL=${MONITOR_BASE_URL:-http://127.0.0.1:8787}
MONITOR_BASE_URL=${MONITOR_BASE_URL:-http://127.0.0.1:8080}
MONITOR_TURNSTILE_TOKEN=${MONITOR_TURNSTILE_TOKEN:-XXXX.DUMMY.TOKEN.XXXX}
: "${MONITOR_ADMIN_USERNAME:?Set MONITOR_ADMIN_USERNAME before running the smoke test}"
: "${MONITOR_ADMIN_PASSWORD:?Set MONITOR_ADMIN_PASSWORD before running the smoke test}"
@@ -17,6 +17,11 @@ request() {
monitor_curl --fail --location --silent --show-error "$@"
}
step() {
printf 'smoke: %s\n' "$1" >&2
}
step "bootstrap"
bootstrap_json=$(request "$MONITOR_BASE_URL/api/bootstrap")
printf '%s' "$bootstrap_json" | jq -e '.access == "ok" and (.servers | type == "array")' >/dev/null
config_json=$(printf '%s' "$bootstrap_json" | jq -c '.config')
@@ -28,28 +33,30 @@ const key = await crypto.subtle.importKey("raw", encoder.encode(process.env.NODE
const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 600000, salt: encoder.encode(`nodeflare:${process.env.NODEFLARE_SALT}`) }, key, 256);
console.log(Array.from(new Uint8Array(bits), byte => byte.toString(16).padStart(2, "0")).join(""));
')
cross_origin_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
-H 'Origin: https://not-nodeflare.invalid' "$MONITOR_BASE_URL/api/bootstrap")
[ "$cross_origin_status" = "403" ]
step "static assets"
security_headers=$(monitor_curl --silent --show-error --dump-header - --output /dev/null "$MONITOR_BASE_URL/")
printf '%s' "$security_headers" | grep -qi '^x-content-type-options: nosniff'
printf '%s' "$security_headers" | grep -qi '^x-frame-options: DENY'
printf '%s' "$security_headers" | grep -qi "^content-security-policy:.*frame-ancestors 'none'"
admin_html=$(request "$MONITOR_BASE_URL/admin")
case "$admin_html" in
*"/admin-assets/admin.js"*) ;;
*)
echo "Embedded admin script is missing" >&2
echo "Admin script is missing" >&2
exit 1
;;
esac
case "$admin_html" in
*"/admin-assets/admin.css"*) ;;
*)
echo "Embedded admin stylesheet is missing" >&2
echo "Admin stylesheet is missing" >&2
exit 1
;;
esac
request "$MONITOR_BASE_URL/admin-assets/admin.js" | grep -q '管理面板'
request "$MONITOR_BASE_URL/admin-assets/admin.css" | grep -q 'admin-shell'
step "login and settings"
login_json=$(request -H 'Content-Type: application/json' \
--data "$(jq -nc --arg username "$MONITOR_ADMIN_USERNAME" --arg password "$MONITOR_ADMIN_PASSWORD" --arg password_derived "$password_derived" --arg turnstile_token "$MONITOR_TURNSTILE_TOKEN" '{username:$username,password:$password,password_derived:$password_derived,turnstile_token:$turnstile_token}')" \
"$MONITOR_BASE_URL/api/admin/login")
@@ -78,6 +85,9 @@ server_id=
latency_task_id=
alert_rule_id=
cleanup() {
if [ "${MONITOR_KEEP_RESOURCES:-0}" = "1" ]; then
return
fi
if [ -n "$alert_rule_id" ]; then
monitor_curl --silent --show-error -H "Authorization: Bearer $admin_token" \
-X DELETE "$MONITOR_BASE_URL/api/admin/alert-rules/$alert_rule_id" >/dev/null || true
@@ -93,6 +103,7 @@ cleanup() {
}
trap cleanup EXIT
step "admin resources"
request -H "Authorization: Bearer $admin_token" \
"$MONITOR_BASE_URL/api/admin/telegram" | \
jq -e '.telegram == null or (.telegram.bot_token == "********" and (.telegram.chat_id | length > 0))' >/dev/null
@@ -113,6 +124,7 @@ server_json=$(request -H "Authorization: Bearer $admin_token" \
"$MONITOR_BASE_URL/api/admin/servers")
server_id=$(printf '%s' "$server_json" | jq -er '.id')
agent_token=$(printf '%s' "$server_json" | jq -er '.agent_token')
step "websocket report"
MONITOR_BASE_URL="$MONITOR_BASE_URL" MONITOR_ADMIN_TOKEN="$admin_token" \
MONITOR_AGENT_TOKEN="$agent_token" MONITOR_SERVER_ID="$server_id" \
MONITOR_LATENCY_TASK_ID="$latency_task_id" \
@@ -124,15 +136,16 @@ request -H "Authorization: Bearer $admin_token" \
"$MONITOR_BASE_URL/api/admin/servers/$server_id/token" | \
jq -e --arg token "$agent_token" '.agent_token == $token' >/dev/null
step "persisted metrics"
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/admin/servers" | \
jq -e --arg id "$server_id" '.servers | any(.id == $id and .last_ip == "8.8.8.8")' >/dev/null
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/bootstrap" | jq -e --arg id "$server_id" --arg task_id "$latency_task_id" '.servers | any(.id == $id and .cpu == 18.5 and .gpu_usage == 32.5 and .disk_await_ms == 1.4 and (.gpus | length) == 1 and (.disks | length) == 1 and .disk_used == 21474836480 and .traffic_limit == 107374182400 and .net_rx_total == 2684354560 and .net_tx_total == 1342177280 and .price == 9.9 and (has("last_ip") | not) and (.latency | any(.task_id == $task_id and .latency_ms == 48.4 and .packet_loss == 75)))' >/dev/null
post_report_bootstrap=$(request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/bootstrap")
if ! printf '%s' "$post_report_bootstrap" | jq -e --arg id "$server_id" --arg task_id "$latency_task_id" '.servers | any(.id == $id and .cpu == 18.5 and .gpu_usage == 32.5 and .disk_await_ms == 1.4 and (.gpus | length) == 1 and (.disks | length) == 1 and .disk_used == 21474836480 and .traffic_limit == 107374182400 and .net_rx_total == 2684354560 and .net_tx_total == 1342177280 and .price == 9.9 and (has("last_ip") | not) and (.latency | any(.task_id == $task_id and .latency_ms == 48.4 and .packet_loss == 75)))' >/dev/null; then
printf '%s' "$post_report_bootstrap" | jq --arg id "$server_id" '.servers[] | select(.id == $id)' >&2
exit 1
fi
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/history/$server_id?hours=1" | jq -e '.points | length >= 1 and any(.gpu_usage == 32.5)' >/dev/null
history_cache_header=$(monitor_curl --silent --dump-header - --output /dev/null \
-H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/history/$server_id?hours=1" | \
tr -d '\r' | awk -F ': ' 'tolower($1) == "x-cache" { print $2 }' | tail -1)
[ "$history_cache_header" = "HIT" ]
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/latency/$server_id?hours=1" | jq -e --arg task_id "$latency_task_id" '(.tasks | any(.id == $task_id)) and (.points | any(.task_id == $task_id and .latency_ms > 38.399 and .latency_ms < 38.401 and .packet_loss > 49.999 and .packet_loss < 50.001))' >/dev/null
# An Agent may have already measured a task when the administrator removes its
@@ -141,11 +154,13 @@ request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/js
--data '{"name":"Smoke TCP","task_type":"tcp","target":"example.com","port":443,"interval_seconds":60,"default_enabled":true,"server_ids":[]}' \
"$MONITOR_BASE_URL/api/admin/latency-tasks/$latency_task_id" >/dev/null
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/latency/$server_id?hours=1" | jq -e --arg task_id "$latency_task_id" '(.tasks | all(.id != $task_id)) and (.points | all(.task_id != $task_id))' >/dev/null
step "agent reconfiguration"
MONITOR_BASE_URL="$MONITOR_BASE_URL" MONITOR_ADMIN_TOKEN="$admin_token" \
MONITOR_AGENT_TOKEN="$agent_token" MONITOR_SERVER_ID="$server_id" \
MONITOR_LATENCY_TASK_ID="$latency_task_id" MONITOR_EXPECT_TASK_ASSIGNED=0 \
MONITOR_CONFIG_ONLY=1 node scripts/websocket-smoke.mjs
step "alerts and visibility"
alert_rule_json=$(request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' \
--data "$(jq -nc --arg server_id "$server_id" '{name:"Smoke CPU",metric:"cpu",threshold:80,duration_minutes:5,aggregation:"average",enabled:true,server_ids:[$server_id]}')" \
"$MONITOR_BASE_URL/api/admin/alert-rules")
+2 -1
View File
@@ -60,7 +60,8 @@ function expectSocketStatus(path, token, statusCode) {
});
socket.once("unexpected-response", (_request, response) => {
clearTimeout(timer);
response.resume();
response.destroy();
socket.terminate();
if (response.statusCode === statusCode) {
resolve();
} else {
-48
View File
@@ -1,48 +0,0 @@
import * as imports from "./index_bg.js";
export * from "./index_bg.js";
import wasmModule from "./index.wasm";
import { WorkerEntrypoint } from "cloudflare:workers";
$SNIPPET_JS_IMPORTS
const instance = new WebAssembly.Instance(wasmModule, {
"./index_bg.js": imports,
$SNIPPET_WASM_IMPORTS
});
imports.__wbg_set_wasm(instance.exports);
instance.exports.__wbindgen_start?.();
export { wasmModule };
class Entrypoint extends WorkerEntrypoint {
async fetch(request) {
let response = imports.fetch(request, this.env, this.ctx);
$WAIT_UNTIL_RESPONSE;
return await response;
}
async scheduled(event) {
return await imports.scheduled(event, this.env, this.ctx);
}
}
const EXCLUDE_EXPORT = [
"IntoUnderlyingByteSource",
"IntoUnderlyingSink",
"IntoUnderlyingSource",
"MinifyConfig",
"PolishConfig",
"R2Range",
"RequestRedirect",
"fetch",
"scheduled",
"getMemory",
];
Object.keys(imports).map((key) => {
if (!(EXCLUDE_EXPORT.includes(key) | key.startsWith("__"))) {
Entrypoint.prototype[key] = imports[key];
}
});
export default Entrypoint;