From d0d80966639967336a9122ff2deab8df25d41e1f Mon Sep 17 00:00:00 2001 From: imengying Date: Sat, 5 Sep 2026 09:54:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=201.0.0=20=E9=A6=96=E5=8F=91?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E4=B8=8E=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 16 +- README.md | 11 +- agent/Cargo.lock | 2 +- agent/Cargo.toml | 2 +- agent/agent.sh | 66 ++- agent/install-freebsd.sh | 53 ++- agent/install-macos.sh | 51 ++- agent/install.ps1 | 127 ++++-- agent/src/live_batch.rs | 42 +- agent/src/main.rs | 31 +- backend/Cargo.lock | 6 +- backend/Cargo.toml | 3 +- backend/config.example.toml | 3 + backend/migrations/postgres/01_initial.sql | 2 +- backend/migrations/sqlite/01_initial.sql | 2 +- backend/src/backup.rs | 456 +++++++++++++++++++-- backend/src/config.rs | 16 +- backend/src/db/queries.rs | 173 +++++++- backend/src/main.rs | 63 ++- backend/src/middleware.rs | 10 +- backend/src/models.rs | 2 + backend/src/routes/admin.rs | 66 ++- backend/src/routes/auth.rs | 123 +++++- backend/src/routes/mod.rs | 95 +++-- backend/src/routes/public.rs | 10 +- backend/src/routes/remote.rs | 6 +- backend/src/routes/site.rs | 47 ++- backend/src/security.rs | 40 ++ backend/src/theme.rs | 2 +- backend/src/websocket/agent.rs | 21 +- backend/src/websocket/dashboard.rs | 53 ++- backend/src/websocket/mod.rs | 3 + bun.lock | 2 +- docs/dashboard-light.png | Bin 201345 -> 0 bytes docs/theme-store-light.png | Bin 66107 -> 0 bytes frontend/package.json | 2 +- frontend/src/AppProvider.tsx | 15 +- frontend/src/api.ts | 62 +-- frontend/src/components/AdminPanel.tsx | 89 ++-- frontend/src/demo.ts | 2 +- frontend/src/styles/admin.css | 4 + frontend/src/transport.ts | 15 +- frontend/src/types.ts | 2 + install.ps1 | 36 +- install.sh | 68 +++ package.json | 2 +- scripts/resolve-version.sh | 3 - scripts/smoke-test.sh | 76 +++- scripts/websocket-smoke.mjs | 77 +++- 49 files changed, 1760 insertions(+), 298 deletions(-) delete mode 100644 docs/dashboard-light.png delete mode 100644 docs/theme-store-light.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2693e1..908e333 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,11 +8,6 @@ on: - "v*.*.*" pull_request: workflow_dispatch: - inputs: - version: - description: Tag version with a v prefix, for example v0.0.1 - required: true - default: v0.0.1 permissions: contents: read @@ -33,8 +28,8 @@ jobs: name: Resolve release version shell: bash env: - RAW_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || startsWith(github.ref, 'refs/tags/') && github.ref_name || 'v0.0.0' }} - RELEASE_BUILD: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }} + RAW_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'v1.0.0' }} + RELEASE_BUILD: ${{ startsWith(github.ref, 'refs/tags/') }} run: | if [ "$RELEASE_BUILD" = "true" ] && [[ ! "$RAW_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Tag must use the v-prefixed format v1.2.3" >&2 @@ -132,6 +127,8 @@ jobs: run: CARGO_TARGET_DIR=target/backend cargo test --locked --manifest-path backend/Cargo.toml - name: Test Agent run: CARGO_TARGET_DIR=target/agent cargo test --locked --manifest-path agent/Cargo.toml + - name: Build Agent for smoke test + run: CARGO_TARGET_DIR=target/agent cargo build --locked --manifest-path agent/Cargo.toml - name: Build backend run: CARGO_TARGET_DIR=target/backend cargo build --locked --manifest-path backend/Cargo.toml - name: Smoke test SQLite and PostgreSQL @@ -180,6 +177,7 @@ jobs: MONITOR_BASE_URL=http://127.0.0.1:18080 \ MONITOR_ADMIN_USERNAME=sqlite-smoke \ MONITOR_ADMIN_PASSWORD=SmokePassword123 \ + MONITOR_AGENT_BINARY="$GITHUB_WORKSPACE/target/agent/debug/nodeflare-agent" \ MONITOR_MIGRATION_URL="$POSTGRES_URL" \ MONITOR_MIGRATION_KIND=postgresql \ sh scripts/smoke-test.sh @@ -207,6 +205,7 @@ jobs: MONITOR_BASE_URL=http://127.0.0.1:18081 \ MONITOR_ADMIN_USERNAME=sqlite-smoke \ MONITOR_ADMIN_PASSWORD=SmokePassword123 \ + MONITOR_AGENT_BINARY="$GITHUB_WORKSPACE/target/agent/debug/nodeflare-agent" \ sh scripts/smoke-test.sh server: @@ -752,8 +751,9 @@ jobs: cp -a inputs/frontend/unpacked/admin-dist/. package/share/admin/ cp agent/agent.sh agent/install-macos.sh agent/install-freebsd.sh agent/install.ps1 package/share/agent/ cp deploy/nodeflare.service package/nodeflare.service + cp LICENSE package/LICENSE chmod 0755 "package/$PACKAGE_BINARY" package/share/agent/*.sh - chmod 0644 package/nodeflare.service package/share/agent/install.ps1 + chmod 0644 package/LICENSE package/nodeflare.service package/share/agent/install.ps1 if [ "$FORMAT" = zip ]; then (cd package && zip -qr "../release/$ASSET" .) unzip -tq "release/$ASSET" diff --git a/README.md b/README.md index 22a421b..b51d40c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ fetch -qo - https://raw.githubusercontent.com/imengying/NodeFlare/main/install.s Windows PowerShell(管理员): ```powershell -Invoke-WebRequest https://raw.githubusercontent.com/imengying/NodeFlare/main/install.ps1 -OutFile "$env:TEMP\nodeflare-install.ps1" +Invoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/imengying/NodeFlare/main/install.ps1 -OutFile "$env:TEMP\nodeflare-install.ps1" Unblock-File "$env:TEMP\nodeflare-install.ps1" & "$env:TEMP\nodeflare-install.ps1" ``` @@ -34,6 +34,8 @@ Unblock-File "$env:TEMP\nodeflare-install.ps1" postgres://用户:密码@127.0.0.1:5432/nodeflare?sslmode=prefer ``` +PostgreSQL 用户名或密码中的 `@`、`:`、`/`、`?` 等字符需要先进行 URL 百分号编码。 + 安装完成后访问 `http://服务器地址:8080/admin/login`。默认只监听 `127.0.0.1:8080`,对外使用时请配置 HTTPS 反向代理,例如 Caddy: ```caddy @@ -42,6 +44,8 @@ monitor.example.com { } ``` +NodeFlare 默认只信任本机反向代理写入的 `X-Forwarded-For`。反向代理位于容器或其他主机时,在 `config.toml` 的 `trusted_proxies` 中填写其 IP 或 CIDR,未受信任来源提交的转发头会被忽略。 + Linux systemd 常用命令: ```bash @@ -76,13 +80,16 @@ Windows 使用 `install.ps1 -Uninstall`,彻底删除数据时再加 `-Purge` 管理后台的“数据库”页面可查看占用空间、手动回收空间、导出或恢复 ZIP,SQLite 和 PostgreSQL 都支持。 -- 备份包含设置、节点、监控历史、通知、主题、远程任务和安全配置。 +- 备份包含设置、节点、监控历史、通知、主题记录及主题文件、远程任务和安全配置。 - 登录会话等临时数据不会导出;恢复完成后需要重新登录。 +- 导出、恢复和迁移数据库前需要提交当前 TOTP;未启用 TOTP 时提交当前管理员密码。 - 建议使用相同版本的 NodeFlare 恢复备份。 - `pg_dump` 和 SQLite `.backup` 仍可作为额外的数据库原生备份方式,但不是使用内置 ZIP 功能的前提。 “数据库迁移”可在 SQLite 和 PostgreSQL 之间复制全部持久数据。迁移会覆盖目标库中已有的 NodeFlare 数据并更新 `database_url`,完成后重启 NodeFlare。 +第三方主题会作为与管理后台同源的前端代码运行,只安装你信任的主题包或仓库。 + ## 安装 Agent 进入“服务器”,创建节点并复制页面生成的安装命令。Linux 命令格式如下: diff --git a/agent/Cargo.lock b/agent/Cargo.lock index 5abab8b..6a2f574 100644 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -436,7 +436,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "nodeflare-agent" -version = "0.0.1" +version = "1.0.0" dependencies = [ "clap", "httpdate", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index d5311c0..0630694 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nodeflare-agent" -version = "0.0.1" +version = "1.0.0" edition = "2024" license = "MIT" description = "Independent Rust telemetry agent for NodeFlare" diff --git a/agent/agent.sh b/agent/agent.sh index 0196f1c..c7da362 100644 --- a/agent/agent.sh +++ b/agent/agent.sh @@ -8,20 +8,26 @@ STATE_ROOT="/etc/nodeflare" STATE_DIR="$STATE_ROOT/agent" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" OPENRC_FILE="/etc/init.d/$SERVICE_NAME" +temporary="" +backup_agent="" +backup_service="" +rollback_agent=false +had_agent=false +had_service=false usage() { cat <<'EOF' NodeFlare Agent 安装脚本 用法: - agent.sh -e -t [-i <上报间隔>] [-m <下载加速前缀>] + agent.sh -e -t [-i <历史保存间隔>] [-m <下载加速前缀>] agent.sh --status agent.sh --uninstall 参数: -e NodeFlare 服务地址(必填) -t 后台生成的 Agent Token(必填,请勿泄露) - -i 初始上报间隔,15-3600 秒(默认 60) + -i 初始历史保存间隔,15-3600 秒(默认 60) -m GitHub 下载加速前缀(可选,仅作用于 Release 下载; 形如 https://ghproxy.net,脚本会自动拼接完整地址,摘要校验不受影响) EOF @@ -36,6 +42,39 @@ fail() { exit 1 } +cleanup_agent_install() { + if [ "$rollback_agent" = true ]; then + rollback_agent=false + log "安装未完成,正在恢复上一版本" + case "$init_system" in + systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true ;; + openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true ;; + esac + if [ "$had_agent" = true ]; then + cp -p "$backup_agent" "$AGENT_FILE" 2>/dev/null || true + else + rm -f "$AGENT_FILE" + fi + service_path=$SERVICE_FILE + [ "$init_system" != openrc ] || service_path=$OPENRC_FILE + if [ "$had_service" = true ]; then + cp -p "$backup_service" "$service_path" 2>/dev/null || true + else + rm -f "$service_path" + fi + case "$init_system" in + systemd) + systemctl daemon-reload 2>/dev/null || true + [ "$had_agent" != true ] || systemctl restart "$SERVICE_NAME" 2>/dev/null || true + ;; + openrc) [ "$had_agent" != true ] || rc-service "$SERVICE_NAME" restart 2>/dev/null || true ;; + esac + fi + [ -z "$temporary" ] || rm -f "$temporary" + [ -z "$backup_agent" ] || rm -f "$backup_agent" + [ -z "$backup_service" ] || rm -f "$backup_service" +} + detect_glibc_version() { [ -x "$glibc_loader" ] || return LC_ALL=C "$glibc_loader" --version 2>/dev/null | awk ' @@ -145,8 +184,8 @@ install_agent() { *) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;; esac case "$endpoint" in *@*) fail "服务地址不能包含用户信息" ;; esac - case "$interval" in ''|*[!0-9]*) fail "上报间隔必须是整数" ;; esac - [ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "上报间隔必须在 15-3600 秒之间" + case "$interval" in ''|*[!0-9]*) fail "历史保存间隔必须是整数" ;; esac + [ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "历史保存间隔必须在 15-3600 秒之间" mirror=${mirror%/} if [ -n "$mirror" ]; then [ ${#mirror} -le 2048 ] || fail "下载加速前缀长度超出限制" @@ -176,7 +215,7 @@ install_agent() { chmod 700 "$STATE_ROOT" chmod 750 "$STATE_DIR" temporary="$INSTALL_DIR/.agent.$$.download" - trap 'rm -f "$temporary"' EXIT HUP INT TERM + trap cleanup_agent_install EXIT HUP INT TERM artifact="agent-linux-$arch-$libc" release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest" log "正在获取 GitHub 最新正式版本($artifact)" @@ -233,12 +272,24 @@ install_agent() { [ "$installed_version" = "${release_tag#v}" ] || { fail "Release $release_tag 与 Agent 版本 $installed_version 不一致" } + backup_agent="$STATE_DIR/.agent.$$.previous" + service_path=$SERVICE_FILE + [ "$init_system" != openrc ] || service_path=$OPENRC_FILE + backup_service="$STATE_DIR/.service.$$.previous" + if [ -f "$AGENT_FILE" ]; then + cp -p "$AGENT_FILE" "$backup_agent" + had_agent=true + fi + if [ -f "$service_path" ]; then + cp -p "$service_path" "$backup_service" + had_service=true + fi + rollback_agent=true case "$init_system" in systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true ;; openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true ;; esac mv "$temporary" "$AGENT_FILE" - trap - EXIT HUP INT TERM log "正在配置并启动 $init_system 服务" if [ "$init_system" = "systemd" ]; then printf '%s\n' \ @@ -285,6 +336,9 @@ install_agent() { fail "NodeFlare 服务启动失败,请查看上方状态信息" } fi + rollback_agent=false + cleanup_agent_install + trap - EXIT HUP INT TERM printf '\nNodeFlare Agent 安装完成\n' printf ' 版本:%s\n' "$installed_version" printf ' 服务:%s(%s)\n' "$SERVICE_NAME" "$init_system" diff --git a/agent/install-freebsd.sh b/agent/install-freebsd.sh index a70ef81..6704134 100755 --- a/agent/install-freebsd.sh +++ b/agent/install-freebsd.sh @@ -8,6 +8,12 @@ AGENT_FILE="$INSTALL_DIR/agent" STATE_ROOT="/var/db/nodeflare" STATE_DIR="$STATE_ROOT/agent" SERVICE_FILE="/usr/local/etc/rc.d/$SERVICE_NAME" +temporary="" +backup_agent="" +backup_service="" +rollback_agent=false +had_agent=false +had_service=false log() { printf '[NodeFlare] %s\n' "$1" @@ -18,12 +24,34 @@ fail() { exit 1 } +cleanup_agent_install() { + if [ "$rollback_agent" = true ]; then + rollback_agent=false + log "安装未完成,正在恢复上一版本" + service "$SERVICE_NAME" stop 2>/dev/null || true + if [ "$had_agent" = true ]; then + cp -p "$backup_agent" "$AGENT_FILE" 2>/dev/null || true + else + rm -f "$AGENT_FILE" + fi + if [ "$had_service" = true ]; then + cp -p "$backup_service" "$SERVICE_FILE" 2>/dev/null || true + service "$SERVICE_NAME" start 2>/dev/null || true + else + rm -f "$SERVICE_FILE" + fi + fi + [ -z "$temporary" ] || rm -f "$temporary" + [ -z "$backup_agent" ] || rm -f "$backup_agent" + [ -z "$backup_service" ] || rm -f "$backup_service" +} + usage() { cat <<'EOF' NodeFlare Agent FreeBSD 安装脚本 用法: - install-freebsd.sh -e -t [-i <上报间隔>] [-m <下载加速前缀>] + install-freebsd.sh -e -t [-i <历史保存间隔>] [-m <下载加速前缀>] install-freebsd.sh --status install-freebsd.sh --uninstall @@ -130,8 +158,8 @@ case "$endpoint" in *) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;; esac case "$endpoint" in *@*) fail "服务地址不能包含用户信息" ;; esac -case "$interval" in ''|*[!0-9]*) fail "上报间隔必须是整数" ;; esac -[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "上报间隔必须在 15-3600 秒之间" +case "$interval" in ''|*[!0-9]*) fail "历史保存间隔必须是整数" ;; esac +[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "历史保存间隔必须在 15-3600 秒之间" mirror=${mirror%/} if [ -n "$mirror" ]; then [ ${#mirror} -le 2048 ] || fail "下载加速前缀长度超出限制" @@ -149,7 +177,7 @@ chmod 755 "$INSTALL_DIR" chmod 750 "$STATE_ROOT" chmod 750 "$STATE_DIR" temporary="$INSTALL_DIR/.agent.$$.download" -trap 'rm -f "$temporary"' EXIT HUP INT TERM +trap cleanup_agent_install EXIT HUP INT TERM artifact="agent-freebsd-$arch" release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest" log "正在获取 GitHub 最新正式版本($artifact)" @@ -192,10 +220,21 @@ installed_version=$("$temporary" --version) || fail "下载的 Agent 无法在 installed_version=${installed_version##* } [ "$installed_version" = "${release_tag#v}" ] || fail "Release $release_tag 与 Agent 版本 $installed_version 不一致" +backup_agent="$STATE_DIR/.agent.$$.previous" +backup_service="$STATE_DIR/.service.$$.previous" +if [ -f "$AGENT_FILE" ]; then + cp -p "$AGENT_FILE" "$backup_agent" + had_agent=true +fi +if [ -f "$SERVICE_FILE" ]; then + cp -p "$SERVICE_FILE" "$backup_service" + had_service=true +fi +rollback_agent=true + log "正在配置并启动 FreeBSD rc.d 服务" service "$SERVICE_NAME" stop 2>/dev/null || true mv "$temporary" "$AGENT_FILE" -trap - EXIT HUP INT TERM cat > "$SERVICE_FILE" </dev/null || { fail "NodeFlare 服务启动失败,请查看 /var/log/messages" } +rollback_agent=false +cleanup_agent_install +trap - EXIT HUP INT TERM + printf '\nNodeFlare Agent 安装完成\n' printf ' 版本:%s\n' "$installed_version" printf ' 服务:%s(FreeBSD rc.d)\n' "$SERVICE_NAME" diff --git a/agent/install-macos.sh b/agent/install-macos.sh index 7d00cd4..67b1762 100644 --- a/agent/install-macos.sh +++ b/agent/install-macos.sh @@ -7,6 +7,12 @@ AGENT_FILE="$INSTALL_DIR/agent" STATE_ROOT="/Library/Application Support/NodeFlare" STATE_DIR="$STATE_ROOT/Agent" PLIST_FILE="/Library/LaunchDaemons/$LABEL.plist" +temporary="" +backup_agent="" +backup_service="" +rollback_agent=false +had_agent=false +had_service=false log() { printf '[NodeFlare] %s\n' "$1" @@ -17,12 +23,34 @@ fail() { exit 1 } +cleanup_agent_install() { + if [ "$rollback_agent" = true ]; then + rollback_agent=false + log "安装未完成,正在恢复上一版本" + launchctl bootout system "$PLIST_FILE" 2>/dev/null || true + if [ "$had_agent" = true ]; then + cp -p "$backup_agent" "$AGENT_FILE" 2>/dev/null || true + else + rm -f "$AGENT_FILE" + fi + if [ "$had_service" = true ]; then + cp -p "$backup_service" "$PLIST_FILE" 2>/dev/null || true + launchctl bootstrap system "$PLIST_FILE" 2>/dev/null || true + else + rm -f "$PLIST_FILE" + fi + fi + [ -z "$temporary" ] || rm -f "$temporary" + [ -z "$backup_agent" ] || rm -f "$backup_agent" + [ -z "$backup_service" ] || rm -f "$backup_service" +} + usage() { cat <<'EOF' NodeFlare Agent macOS 安装脚本 用法: - install-macos.sh -e -t [-i <上报间隔>] [-m <下载加速前缀>] + install-macos.sh -e -t [-i <历史保存间隔>] [-m <下载加速前缀>] install-macos.sh --status install-macos.sh --uninstall @@ -97,8 +125,8 @@ case "$endpoint" in *) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;; esac case "$endpoint" in *@*) fail "服务地址不能包含用户信息" ;; esac -case "$interval" in ''|*[!0-9]*) fail "上报间隔必须是整数" ;; esac -[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "上报间隔必须在 15-3600 秒之间" +case "$interval" in ''|*[!0-9]*) fail "历史保存间隔必须是整数" ;; esac +[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "历史保存间隔必须在 15-3600 秒之间" mirror=${mirror%/} if [ -n "$mirror" ]; then [ ${#mirror} -le 2048 ] || fail "下载加速前缀长度超出限制" @@ -116,7 +144,7 @@ chmod 755 "$INSTALL_DIR" chmod 750 "$STATE_ROOT" chmod 750 "$STATE_DIR" temporary="$INSTALL_DIR/.agent.$$.download" -trap 'rm -f "$temporary"' EXIT HUP INT TERM +trap cleanup_agent_install EXIT HUP INT TERM artifact="agent-macos-aarch64" release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest" log "正在获取 GitHub 最新正式版本($artifact)" @@ -163,10 +191,20 @@ log "下载校验通过,正在验证可执行文件" installed_version=$("$temporary" --version) || fail "下载的 Agent 无法在当前 macOS 运行" installed_version=${installed_version##* } [ "$installed_version" = "${release_tag#v}" ] || fail "Release $release_tag 与 Agent 版本 $installed_version 不一致" +backup_agent="$STATE_DIR/.agent.$$.previous" +backup_service="$STATE_DIR/.service.$$.previous" +if [ -f "$AGENT_FILE" ]; then + cp -p "$AGENT_FILE" "$backup_agent" + had_agent=true +fi +if [ -f "$PLIST_FILE" ]; then + cp -p "$PLIST_FILE" "$backup_service" + had_service=true +fi +rollback_agent=true log "正在配置并启动 macOS LaunchDaemon 服务" launchctl bootout system "$PLIST_FILE" 2>/dev/null || true mv "$temporary" "$AGENT_FILE" -trap - EXIT HUP INT TERM cat > "$PLIST_FILE" < @@ -190,6 +228,9 @@ for _ in 1 2 3 4 5 6 7 8 9 10; do fi done [ "$started" = true ] || fail "NodeFlare 服务启动失败,请查看 /var/log/nodeflare-agent.log" +rollback_agent=false +cleanup_agent_install +trap - EXIT HUP INT TERM printf '\nNodeFlare Agent 安装完成\n' printf ' 版本:%s\n' "$installed_version" printf ' 服务:%s(LaunchDaemon)\n' "$LABEL" diff --git a/agent/install.ps1 b/agent/install.ps1 index 39a4c5b..6d2d328 100644 --- a/agent/install.ps1 +++ b/agent/install.ps1 @@ -14,6 +14,16 @@ $InstallDir = Join-Path $env:ProgramFiles "NodeFlare" $DataDir = Join-Path $env:ProgramData "NodeFlare" $StateDir = Join-Path $DataDir "Agent" $AgentFile = Join-Path $InstallDir "agent.exe" +$ConfigFile = Join-Path $StateDir "config.json" +$LauncherFile = Join-Path $InstallDir "run-agent.ps1" +$PreviousAgent = Join-Path $env:TEMP "nodeflare-agent-$PID.previous.exe" +$PreviousConfig = Join-Path $env:TEMP "nodeflare-agent-$PID.previous.json" +$PreviousLauncher = Join-Path $env:TEMP "nodeflare-agent-$PID.previous.ps1" +$HadPreviousAgent = $false +$HadPreviousConfig = $false +$HadPreviousLauncher = $false +$PreviousTaskXml = $null +$InstallChanged = $false function Write-Step([string]$Message) { Write-Host "[NodeFlare] $Message" @@ -110,8 +120,10 @@ $Endpoint = $Endpoint.TrimEnd('/') New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null New-Item -ItemType Directory -Path $DataDir -Force | Out-Null New-Item -ItemType Directory -Path $StateDir -Force | Out-Null -& icacls.exe $InstallDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null -& icacls.exe $StateDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null +& icacls.exe $InstallDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null +if ($LASTEXITCODE -ne 0) { Write-InstallError "无法限制程序目录权限" } +& icacls.exe $StateDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null +if ($LASTEXITCODE -ne 0) { Write-InstallError "无法限制 Agent 数据目录权限" } $Temporary = "$AgentFile.$PID.download.exe" $ReleaseApi = "https://api.github.com/repos/imengying/NodeFlare/releases/latest" $Artifact = "agent-windows-x64.exe" @@ -134,11 +146,12 @@ try { } else { Write-Step "正在下载 NodeFlare Agent $($Release.tag_name)" } - Invoke-WebRequest -Uri $DownloadUrl -OutFile $Temporary -TimeoutSec 120 + Invoke-WebRequest -UseBasicParsing -Uri $DownloadUrl -OutFile $Temporary -TimeoutSec 120 $ActualChecksum = (Get-FileHash -LiteralPath $Temporary -Algorithm SHA256).Hash if ($ActualChecksum -ne $ExpectedChecksum) { Write-InstallError "Agent SHA-256 校验失败,已停止安装" } + Unblock-File -LiteralPath $Temporary Write-Step "下载校验通过,正在验证可执行文件" $InstalledVersion = (& $Temporary --version | Out-String).Trim() if ($LASTEXITCODE -ne 0) { @@ -149,31 +162,93 @@ try { if ($InstalledVersion -ne $ExpectedVersion) { Write-InstallError "Release $($Release.tag_name) 与 Agent 版本 $InstalledVersion 不一致" } + $HadPreviousAgent = Test-Path -LiteralPath $AgentFile -PathType Leaf + $HadPreviousConfig = Test-Path -LiteralPath $ConfigFile -PathType Leaf + $HadPreviousLauncher = Test-Path -LiteralPath $LauncherFile -PathType Leaf + if ($HadPreviousAgent) { Copy-Item -LiteralPath $AgentFile -Destination $PreviousAgent -Force } + if ($HadPreviousConfig) { Copy-Item -LiteralPath $ConfigFile -Destination $PreviousConfig -Force } + if ($HadPreviousLauncher) { Copy-Item -LiteralPath $LauncherFile -Destination $PreviousLauncher -Force } + $ExistingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if ($null -ne $ExistingTask) { $PreviousTaskXml = Export-ScheduledTask -TaskName $TaskName } + $InstallChanged = $true Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Move-Item -LiteralPath $Temporary -Destination $AgentFile -Force + + $AgentConfig = [ordered]@{ + endpoint = $Endpoint + token = $Token + interval = $Interval + } + $AgentConfig | ConvertTo-Json -Compress | Set-Content -LiteralPath $ConfigFile -Encoding UTF8 + $Launcher = @' +$ErrorActionPreference = "Stop" +$InstallRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$AgentFile = Join-Path $InstallRoot "agent.exe" +$ConfigFile = Join-Path $env:ProgramData "NodeFlare\Agent\config.json" +$Config = Get-Content -LiteralPath $ConfigFile -Raw | ConvertFrom-Json +$env:NODEFLARE_AGENT_TOKEN = [string]$Config.token +& $AgentFile -e ([string]$Config.endpoint) -i ([int]$Config.interval) +exit $LASTEXITCODE +'@ + [IO.File]::WriteAllText($LauncherFile, $Launcher, [Text.UTF8Encoding]::new($false)) + & icacls.exe $ConfigFile /inheritance:r /grant:r '*S-1-5-18:F' '*S-1-5-32-544:F' | Out-Null + if ($LASTEXITCODE -ne 0) { Write-InstallError "无法限制 Agent 配置文件权限" } + & icacls.exe $LauncherFile /inheritance:r /grant:r '*S-1-5-18:F' '*S-1-5-32-544:F' | Out-Null + if ($LASTEXITCODE -ne 0) { Write-InstallError "无法限制 Agent 启动脚本权限" } + $Token = "" + $PowerShell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" + $TaskArguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$LauncherFile`"" + Write-Step "正在注册并启动 Windows 计划任务" + $TaskAction = New-ScheduledTaskAction -Execute $PowerShell -Argument $TaskArguments + $Trigger = New-ScheduledTaskTrigger -AtStartup + $Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 3650) + $Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $Trigger -Settings $Settings -Principal $Principal -Force | Out-Null + Start-ScheduledTask -TaskName $TaskName + $Task = $null + for ($Attempt = 0; $Attempt -lt 10; $Attempt++) { + Start-Sleep -Milliseconds 500 + $Task = Get-ScheduledTask -TaskName $TaskName + if ($Task.State -eq "Running") { break } + } + if ($Task.State -ne "Running") { + Write-InstallError "NodeFlare 服务启动失败(状态:$($Task.State))" + } + Write-Host "" + Write-Host "NodeFlare Agent 安装完成" + Write-Host " 版本:$InstalledVersion" + Write-Host " 服务:$TaskName(Windows 计划任务)" + Write-Host " 查看状态:Get-ScheduledTask -TaskName '$TaskName'" + $InstallChanged = $false +} catch { + if ($InstallChanged) { + Write-Warning "安装未完成,正在恢复上一版本" + Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + if ($HadPreviousAgent) { + Copy-Item -LiteralPath $PreviousAgent -Destination $AgentFile -Force + } else { + Remove-Item -LiteralPath $AgentFile -Force -ErrorAction SilentlyContinue + } + if ($HadPreviousConfig) { + Copy-Item -LiteralPath $PreviousConfig -Destination $ConfigFile -Force + } else { + Remove-Item -LiteralPath $ConfigFile -Force -ErrorAction SilentlyContinue + } + if ($HadPreviousLauncher) { + Copy-Item -LiteralPath $PreviousLauncher -Destination $LauncherFile -Force + } else { + Remove-Item -LiteralPath $LauncherFile -Force -ErrorAction SilentlyContinue + } + if ($null -ne $PreviousTaskXml) { + Register-ScheduledTask -TaskName $TaskName -Xml $PreviousTaskXml -Force | Out-Null + Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + } + } + throw } finally { Remove-Item -LiteralPath $Temporary -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $PreviousAgent -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $PreviousConfig -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $PreviousLauncher -Force -ErrorAction SilentlyContinue } - -$TaskArguments = "-e `"$Endpoint`" -t `"$Token`" -i $Interval" -Write-Step "正在注册并启动 Windows 计划任务" -$TaskAction = New-ScheduledTaskAction -Execute $AgentFile -Argument $TaskArguments -$Trigger = New-ScheduledTaskTrigger -AtStartup -$Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 3650) -$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest -Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $Trigger -Settings $Settings -Principal $Principal -Force | Out-Null -Start-ScheduledTask -TaskName $TaskName -$Task = $null -for ($Attempt = 0; $Attempt -lt 10; $Attempt++) { - Start-Sleep -Milliseconds 500 - $Task = Get-ScheduledTask -TaskName $TaskName - if ($Task.State -eq "Running") { break } -} -if ($Task.State -ne "Running") { - Write-InstallError "NodeFlare 服务启动失败(状态:$($Task.State))" -} -Write-Host "" -Write-Host "NodeFlare Agent 安装完成" -Write-Host " 版本:$InstalledVersion" -Write-Host " 服务:$TaskName(Windows 计划任务)" -Write-Host " 查看状态:Get-ScheduledTask -TaskName '$TaskName'" diff --git a/agent/src/live_batch.rs b/agent/src/live_batch.rs index e9bd63f..2f70a6f 100644 --- a/agent/src/live_batch.rs +++ b/agent/src/live_batch.rs @@ -26,11 +26,31 @@ pub(crate) fn batch_len(queue: &VecDeque) -> usize { count.min(super::LIVE_QUEUE_CAPACITY) } +pub(crate) fn latest(queue: &VecDeque) -> Vec { + let mut encoded_bytes = 64_usize; + let mut reports = Vec::new(); + for report in queue.iter().rev() { + let Ok(report_bytes) = serde_json::to_vec(report) else { + break; + }; + let next_bytes = encoded_bytes + .saturating_add(report_bytes.len()) + .saturating_add(usize::from(!reports.is_empty())); + if next_bytes > MAX_LIVE_BATCH_BYTES { + break; + } + encoded_bytes = next_bytes; + reports.push(report.clone()); + } + reports.reverse(); + reports +} + #[cfg(test)] mod tests { use std::collections::VecDeque; - use super::{MAX_LIVE_BATCH_BYTES, batch_len}; + use super::{MAX_LIVE_BATCH_BYTES, batch_len, latest}; use crate::Report; #[test] @@ -47,4 +67,24 @@ mod tests { .unwrap(); assert!(payload.len() <= MAX_LIVE_BATCH_BYTES); } + + #[test] + fn persistence_batch_keeps_the_newest_samples() { + let queue = (0..10) + .map(|timestamp| Report { + timestamp, + cpu_model: "x".repeat(128 * 1024), + ..Report::default() + }) + .collect::>(); + let reports = latest(&queue); + assert!(!reports.is_empty()); + assert!(reports.len() < queue.len()); + assert_eq!(reports.last().unwrap().timestamp, 9); + assert!( + reports + .windows(2) + .all(|pair| pair[0].timestamp < pair[1].timestamp) + ); + } } diff --git a/agent/src/main.rs b/agent/src/main.rs index c092d16..9b577a3 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -2567,14 +2567,13 @@ fn live_batch_after(queue: &VecDeque, timestamp: i64) -> Vec { candidates.into_iter().take(count).collect() } -fn live_persistence_probe(queue: &VecDeque, persisted_through: i64) -> Vec { - queue +fn live_persistence_batch(queue: &VecDeque, timestamp: i64) -> Vec { + let candidates = queue .iter() - .rev() - .find(|report| report.timestamp > persisted_through) + .filter(|report| report.timestamp > timestamp) .cloned() - .into_iter() - .collect() + .collect::>(); + live_batch::latest(&candidates) } fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) { @@ -2639,17 +2638,17 @@ fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) { }; let batch = loop { let now = Instant::now(); - let unsent = live_batch_after(&queue, accepted_through); - if !unsent.is_empty() && (now >= next_send_at || now >= next_probe_at) { - break unsent; - } let persisted = persisted_through.load(Ordering::Acquire); - if unsent.is_empty() && now >= next_probe_at { - let probe = live_persistence_probe(&queue, persisted); - if !probe.is_empty() { - break probe; + if now >= next_probe_at { + let persistence_batch = live_persistence_batch(&queue, persisted); + if !persistence_batch.is_empty() { + break persistence_batch; } } + let unsent = live_batch_after(&queue, accepted_through); + if !unsent.is_empty() && now >= next_send_at { + break unsent; + } let wake_at = if !unsent.is_empty() { next_send_at.min(next_probe_at) @@ -2676,6 +2675,10 @@ fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) { } else { match read_live_ack(connected) { Ok(LiveRead::Closed) => drop_socket = true, + Ok(LiveRead::Ack(ack)) if ack.realtime_hint => { + wss_interval = ack_wss_interval(&ack); + next_send_at = Instant::now() + wss_interval; + } Ok(LiveRead::Ack(ack)) => { observe_persisted_through(&persisted_through, &ack); if ack.persistence_error { diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 917d519..85a9a4e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1007,6 +1007,9 @@ name = "ipnet" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +dependencies = [ + "serde", +] [[package]] name = "is_terminal_polyfill" @@ -1180,7 +1183,7 @@ dependencies = [ [[package]] name = "nodeflare" -version = "0.0.1" +version = "1.0.0" dependencies = [ "anyhow", "argon2", @@ -1189,6 +1192,7 @@ dependencies = [ "data-encoding", "futures-util", "hex", + "ipnet", "pbkdf2", "rand 0.10.2", "reqwest", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index fb6319d..8b61b89 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nodeflare" -version = "0.0.1" +version = "1.0.0" edition = "2024" license = "MIT" description = "Server monitoring system with WebSocket support" @@ -36,6 +36,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std" data-encoding = "2" uuid = { version = "1", features = ["v4"] } url = "2.5" +ipnet = { version = "2", features = ["serde"] } zip = { version = "8.6.0", default-features = false, features = ["deflate"] } tempfile = "3" diff --git a/backend/config.example.toml b/backend/config.example.toml index 8067082..7ae15c7 100644 --- a/backend/config.example.toml +++ b/backend/config.example.toml @@ -9,6 +9,9 @@ database_url = "sqlite://nodeflare.db" # 服务器监听地址 bind_addr = "127.0.0.1:8080" +# 只有来自这些反向代理网段的 X-Forwarded-For 才会被信任 +trusted_proxies = ["127.0.0.1/32", "::1/128"] + # 管理员用户名 admin_username = "admin" diff --git a/backend/migrations/postgres/01_initial.sql b/backend/migrations/postgres/01_initial.sql index d1387c8..62a1a88 100644 --- a/backend/migrations/postgres/01_initial.sql +++ b/backend/migrations/postgres/01_initial.sql @@ -193,7 +193,7 @@ CREATE TABLE latency_results ( task_id TEXT NOT NULL REFERENCES latency_tasks(id) ON DELETE CASCADE, server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, timestamp BIGINT NOT NULL, - latency_ms DOUBLE PRECISION NOT NULL CHECK(latency_ms >= 0), + latency_ms DOUBLE PRECISION NOT NULL CHECK(latency_ms >= -1), packet_loss DOUBLE PRECISION NOT NULL CHECK(packet_loss BETWEEN 0 AND 100), PRIMARY KEY(task_id, server_id, timestamp) ); diff --git a/backend/migrations/sqlite/01_initial.sql b/backend/migrations/sqlite/01_initial.sql index 1b4adac..598cd22 100644 --- a/backend/migrations/sqlite/01_initial.sql +++ b/backend/migrations/sqlite/01_initial.sql @@ -196,7 +196,7 @@ CREATE TABLE latency_results ( task_id TEXT NOT NULL REFERENCES latency_tasks(id) ON DELETE CASCADE, server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, timestamp INTEGER NOT NULL, - latency_ms REAL NOT NULL CHECK(latency_ms >= 0), + latency_ms REAL NOT NULL CHECK(latency_ms >= -1), packet_loss REAL NOT NULL CHECK(packet_loss BETWEEN 0 AND 100), PRIMARY KEY(task_id, server_id, timestamp) ) WITHOUT ROWID; diff --git a/backend/src/backup.rs b/backend/src/backup.rs index 6136d60..808a986 100644 --- a/backend/src/backup.rs +++ b/backend/src/backup.rs @@ -6,18 +6,20 @@ use serde_json::{Number, Value}; use sqlx::any::{AnyArguments, AnyRow, AnyTypeInfoKind}; use sqlx::{Arguments, AssertSqlSafe, Column, Executor, Row, SqlSafeStr}; use std::collections::HashSet; -use std::fs::File; +use std::fs::{self, File}; use std::io::{BufRead, BufReader, Cursor, Read, Seek, SeekFrom, Write}; +use std::path::{Component, Path, PathBuf}; use zip::write::SimpleFileOptions; use zip::{CompressionMethod, ZipArchive, ZipWriter}; pub const DATABASE_BACKUP_MAX_BYTES: usize = 512 * 1024 * 1024; const DATABASE_BACKUP_MAX_EXTRACTED_BYTES: u64 = 4 * 1024 * 1024 * 1024; -const DATABASE_BACKUP_MAX_ENTRIES: usize = 64; +const DATABASE_BACKUP_MAX_ENTRIES: usize = 16_384; const DATABASE_BACKUP_FORMAT: &str = "nodeflare-database-backup"; -const DATABASE_BACKUP_VERSION: u32 = 1; +const DATABASE_BACKUP_VERSION: u32 = 2; const INSERT_BATCH_ROWS: usize = 100; const MAX_NDJSON_LINE_BYTES: usize = 16 * 1024 * 1024; +const THEME_BACKUP_FILE_MAX_BYTES: u64 = 32 * 1024 * 1024; const BACKUP_TABLES: &[&str] = &[ "settings", @@ -73,12 +75,14 @@ pub struct DatabaseArchive { } #[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] struct BackupManifest { format: String, version: u32, created_at: i64, source: String, tables: Vec, + theme_files: Vec, excluded_ephemeral_tables: Vec, } @@ -127,6 +131,103 @@ fn table_entry_name(table: &str) -> String { format!("tables/{table}.ndjson") } +fn theme_entry_name(relative: &str) -> String { + format!("themes/{relative}") +} + +fn validate_theme_relative_path(value: &str) -> Result<()> { + if value.is_empty() + || value.len() > 2048 + || value.contains('\\') + || value.chars().any(char::is_control) + { + bail!("主题备份路径无效"); + } + let path = Path::new(value); + let components = path.components().collect::>(); + if path.is_absolute() + || components.len() < 2 + || components + .iter() + .any(|component| !matches!(component, Component::Normal(_))) + { + bail!("主题备份路径无效"); + } + let Some(Component::Normal(id)) = components.first() else { + bail!("主题备份路径无效"); + }; + let id = id.to_str().context("主题目录名称不是 UTF-8")?; + crate::theme::validate_local_id(id).context("主题备份目录名称无效") +} + +fn collect_theme_files(theme_dir: Option<&Path>) -> Result> { + let Some(theme_dir) = theme_dir else { + return Ok(Vec::new()); + }; + let metadata = match fs::symlink_metadata(theme_dir) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(error.into()), + }; + if !metadata.is_dir() || metadata.file_type().is_symlink() { + bail!("主题目录不是普通目录"); + } + + let mut stack = Vec::new(); + for entry in fs::read_dir(theme_dir)? { + let entry = entry?; + let Some(id) = entry.file_name().to_str().map(str::to_string) else { + continue; + }; + if crate::theme::validate_local_id(&id).is_err() { + continue; + } + let metadata = fs::symlink_metadata(entry.path())?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + bail!("主题目录包含非法条目:{id}"); + } + stack.push(entry.path()); + } + + let mut files = Vec::new(); + let mut total = 0_u64; + while let Some(directory) = stack.pop() { + for entry in fs::read_dir(&directory)? { + let entry = entry?; + let path = entry.path(); + let metadata = fs::symlink_metadata(&path)?; + if metadata.file_type().is_symlink() { + bail!("主题目录不能包含符号链接"); + } + if metadata.is_dir() { + stack.push(path); + continue; + } + if !metadata.is_file() || metadata.len() > THEME_BACKUP_FILE_MAX_BYTES { + bail!("主题文件无效或超过 32 MiB"); + } + let relative = path + .strip_prefix(theme_dir)? + .to_str() + .context("主题文件路径不是 UTF-8")? + .replace(std::path::MAIN_SEPARATOR, "/"); + validate_theme_relative_path(&relative)?; + total = total + .checked_add(metadata.len()) + .context("主题文件大小溢出")?; + if total > DATABASE_BACKUP_MAX_EXTRACTED_BYTES { + bail!("主题文件总大小不能超过 4 GiB"); + } + files.push((relative, path, metadata.len())); + if files.len() + BACKUP_TABLES.len() + 1 > DATABASE_BACKUP_MAX_ENTRIES { + bail!("主题文件数量过多"); + } + } + } + files.sort_by(|left, right| left.0.cmp(&right.0)); + Ok(files) +} + async fn table_columns(db: &Database, table: &str) -> Result> { let sql = format!("SELECT * FROM {table} LIMIT 0"); let description = db @@ -196,8 +297,9 @@ fn row_values(row: &AnyRow, columns: &[BackupColumn]) -> Result> { .collect() } -pub async fn export_archive(db: &Database) -> Result { +async fn export(db: &Database, theme_dir: Option<&Path>) -> Result { let schema = database_schema(db).await?; + let theme_files = collect_theme_files(theme_dir)?; let created_at = crate::db::now(); let manifest = BackupManifest { format: DATABASE_BACKUP_FORMAT.to_string(), @@ -212,6 +314,10 @@ pub async fn export_archive(db: &Database) -> Result { .iter() .map(|table| (*table).to_string()) .collect(), + theme_files: theme_files + .iter() + .map(|(relative, _, _)| relative.clone()) + .collect(), excluded_ephemeral_tables: vec![ "sessions".to_string(), "dashboard_proofs".to_string(), @@ -257,6 +363,21 @@ pub async fn export_archive(db: &Database) -> Result { } transaction.commit().await?; + let theme_options = SimpleFileOptions::default() + .compression_method(CompressionMethod::Deflated) + .unix_permissions(0o644); + for (relative, path, expected_size) in theme_files { + writer.start_file(theme_entry_name(&relative), theme_options)?; + let mut source = + File::open(&path).with_context(|| format!("无法读取主题文件 {}", path.display()))?; + let copied = std::io::copy( + &mut Read::by_ref(&mut source).take(expected_size.saturating_add(1)), + &mut writer, + )?; + if copied != expected_size { + bail!("备份期间主题文件发生变化:{}", path.display()); + } + } let mut file = writer.finish()?; let size = file.metadata()?.len(); file.seek(SeekFrom::Start(0))?; @@ -267,15 +388,52 @@ pub async fn export_archive(db: &Database) -> Result { }) } +pub async fn export_archive(db: &Database, theme_dir: &Path) -> Result { + export(db, Some(theme_dir)).await +} + fn validate_archive(archive: &mut ZipArchive) -> Result { if archive.is_empty() || archive.len() > DATABASE_BACKUP_MAX_ENTRIES { bail!("备份 ZIP 文件条目数量无效"); } + let manifest = { + let mut entry = archive.by_name("manifest.json")?; + if entry.size() > 64 * 1024 { + bail!("备份清单过大"); + } + let expected = entry.size(); + let mut content = String::new(); + let read = entry + .by_ref() + .take(64 * 1024 + 1) + .read_to_string(&mut content)?; + if read as u64 != expected { + bail!("备份清单大小不匹配"); + } + serde_json::from_str::(&content).context("备份清单格式无效")? + }; + if manifest.format != DATABASE_BACKUP_FORMAT + || manifest.version != DATABASE_BACKUP_VERSION + || manifest.tables + != BACKUP_TABLES + .iter() + .map(|table| (*table).to_string()) + .collect::>() + { + bail!("不是受支持的 NodeFlare 数据库备份"); + } + let mut expected = BACKUP_TABLES .iter() .map(|table| table_entry_name(table)) .collect::>(); expected.insert("manifest.json".to_string()); + for relative in &manifest.theme_files { + validate_theme_relative_path(relative)?; + if !expected.insert(theme_entry_name(relative)) { + bail!("备份清单包含重复的主题文件"); + } + } let mut seen = HashSet::new(); let mut extracted_bytes = 0_u64; for index in 0..archive.len() { @@ -287,6 +445,9 @@ fn validate_archive(archive: &mut ZipArchive) -> Result THEME_BACKUP_FILE_MAX_BYTES { + bail!("主题备份文件超过 32 MiB"); + } extracted_bytes = extracted_bytes .checked_add(entry.size()) .context("备份 ZIP 解压大小溢出")?; @@ -297,24 +458,6 @@ fn validate_archive(archive: &mut ZipArchive) -> Result 64 * 1024 { - bail!("备份清单过大"); - } - let mut content = String::new(); - entry.read_to_string(&mut content)?; - let manifest: BackupManifest = serde_json::from_str(&content).context("备份清单格式无效")?; - if manifest.format != DATABASE_BACKUP_FORMAT - || manifest.version != DATABASE_BACKUP_VERSION - || manifest.tables - != BACKUP_TABLES - .iter() - .map(|table| (*table).to_string()) - .collect::>() - { - bail!("不是受支持的 NodeFlare 数据库备份"); - } Ok(manifest) } @@ -419,12 +562,185 @@ async fn insert_rows( fn extract_table_entry(archive: &mut ZipArchive, table: &str) -> Result { let mut entry = archive.by_name(&table_entry_name(table))?; + let expected = entry.size(); let mut extracted = tempfile::tempfile().context("无法创建恢复临时文件")?; - std::io::copy(&mut entry, &mut extracted)?; + let copied = std::io::copy( + &mut entry.by_ref().take(expected.saturating_add(1)), + &mut extracted, + )?; + if copied != expected { + bail!("{table} 备份文件大小不匹配"); + } extracted.seek(SeekFrom::Start(0))?; Ok(extracted) } +fn read_bounded_line(reader: &mut R, line: &mut String) -> Result { + line.clear(); + let read = reader + .take(MAX_NDJSON_LINE_BYTES as u64 + 1) + .read_line(line)?; + if read > MAX_NDJSON_LINE_BYTES { + bail!("备份记录过大"); + } + Ok(read) +} + +struct StagedThemeDirectory { + path: PathBuf, +} + +impl Drop for StagedThemeDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } +} + +struct ThemeDirectorySwap { + target: PathBuf, + previous: Option, + committed: bool, +} + +impl ThemeDirectorySwap { + fn commit(mut self) { + self.committed = true; + if let Some(previous) = self.previous.take() { + let _ = fs::remove_dir_all(previous); + } + } +} + +impl Drop for ThemeDirectorySwap { + fn drop(&mut self) { + if self.committed { + return; + } + let _ = fs::remove_dir_all(&self.target); + if let Some(previous) = self.previous.take() { + let _ = fs::rename(previous, &self.target); + } + } +} + +fn sibling_temporary_path(target: &Path, label: &str) -> Result { + let parent = target + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .context("主题目录缺少父目录")?; + let name = target + .file_name() + .and_then(|name| name.to_str()) + .context("主题目录名称无效")?; + Ok(parent.join(format!(".{name}.{label}.{}", uuid::Uuid::new_v4()))) +} + +fn extract_theme_files( + archive: &mut ZipArchive, + manifest: &BackupManifest, + theme_dir: &Path, +) -> Result { + let parent = theme_dir.parent().context("主题目录缺少父目录")?; + fs::create_dir_all(parent)?; + let staging = sibling_temporary_path(theme_dir, "restore")?; + fs::create_dir(&staging)?; + let staged = StagedThemeDirectory { path: staging }; + for relative in &manifest.theme_files { + validate_theme_relative_path(relative)?; + let destination = staged.path.join(relative); + let parent = destination.parent().context("主题文件缺少父目录")?; + fs::create_dir_all(parent)?; + let mut entry = archive.by_name(&theme_entry_name(relative))?; + let expected = entry.size(); + if expected > THEME_BACKUP_FILE_MAX_BYTES { + bail!("主题备份文件超过 32 MiB"); + } + let mut output = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&destination)?; + let copied = std::io::copy( + &mut entry.by_ref().take(expected.saturating_add(1)), + &mut output, + )?; + if copied != expected { + bail!("主题备份文件大小不匹配"); + } + output.sync_all()?; + } + Ok(staged) +} + +fn activate_theme_directory( + staged: &StagedThemeDirectory, + theme_dir: &Path, +) -> Result { + let previous = if theme_dir.exists() { + let metadata = fs::symlink_metadata(theme_dir)?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + bail!("当前主题目录不是普通目录"); + } + let previous = sibling_temporary_path(theme_dir, "previous")?; + fs::rename(theme_dir, &previous)?; + Some(previous) + } else { + None + }; + if let Err(error) = fs::rename(&staged.path, theme_dir) { + if let Some(previous) = previous.as_ref() { + let _ = fs::rename(previous, theme_dir); + } + return Err(error.into()); + } + Ok(ThemeDirectorySwap { + target: theme_dir.to_path_buf(), + previous, + committed: false, + }) +} + +async fn validate_restored_themes( + db: &Database, + transaction: &mut sqlx::Transaction<'_, sqlx::Any>, + staged: &StagedThemeDirectory, +) -> Result<()> { + let references = sqlx::query_scalar::<_, String>("SELECT resolved_url FROM themes") + .fetch_all(&mut **transaction) + .await?; + for reference in references { + let id = reference + .strip_prefix("local://") + .context("备份包含不受支持的主题来源")?; + crate::theme::validate_local_id(id)?; + let index = staged.path.join(id).join("index.html"); + let metadata = fs::symlink_metadata(&index) + .with_context(|| format!("备份缺少主题 {id} 的 index.html"))?; + if metadata.file_type().is_symlink() + || !metadata.is_file() + || metadata.len() == 0 + || metadata.len() > 4 * 1024 * 1024 + { + bail!("主题 {id} 的 index.html 无效"); + } + } + + let active = + sqlx::query_scalar::<_, String>("SELECT value FROM settings WHERE key='active_theme_id'") + .fetch_optional(&mut **transaction) + .await? + .unwrap_or_else(|| crate::theme::BUILTIN_THEME_ID.to_string()); + if active != crate::theme::BUILTIN_THEME_ID { + let exists = sqlx::query_scalar::<_, i64>(db.sql("SELECT COUNT(*) FROM themes WHERE id=?")) + .bind(&active) + .fetch_one(&mut **transaction) + .await?; + if exists != 1 { + bail!("备份中的活动主题不存在"); + } + } + Ok(()) +} + async fn restore_table( archive: &mut ZipArchive, db: &Database, @@ -434,7 +750,7 @@ async fn restore_table( ) -> Result { let mut reader = BufReader::new(extract_table_entry(archive, table)?); let mut line = String::new(); - if reader.read_line(&mut line)? == 0 || line.len() > MAX_NDJSON_LINE_BYTES { + if read_bounded_line(&mut reader, &mut line)? == 0 { bail!("{table} 备份表头无效"); } let header: BackupTableHeader = serde_json::from_str(line.trim_end())?; @@ -444,24 +760,23 @@ async fn restore_table( let mut restored = 0_usize; let mut batch = Vec::with_capacity(INSERT_BATCH_ROWS); + let mut batch_bytes = 0_usize; loop { - line.clear(); - let read = reader.read_line(&mut line)?; + let read = read_bounded_line(&mut reader, &mut line)?; if read == 0 { break; } - if line.len() > MAX_NDJSON_LINE_BYTES { - bail!("{table} 备份记录过大"); - } let trimmed = line.trim_end(); if trimmed.is_empty() { continue; } batch.push(serde_json::from_str::>(trimmed)?); - if batch.len() >= INSERT_BATCH_ROWS { + batch_bytes = batch_bytes.saturating_add(read); + if batch.len() >= INSERT_BATCH_ROWS || batch_bytes >= MAX_NDJSON_LINE_BYTES { insert_rows(db, transaction, table, columns, &batch).await?; restored += batch.len(); batch.clear(); + batch_bytes = 0; } } if !batch.is_empty() { @@ -471,9 +786,18 @@ async fn restore_table( Ok(restored) } -async fn restore(db: &Database, source: R) -> Result { +async fn restore( + db: &Database, + source: R, + theme_dir: Option<&Path>, +) -> Result { let mut archive = ZipArchive::new(source).context("文件不是有效的 ZIP")?; - let _manifest = validate_archive(&mut archive)?; + let manifest = validate_archive(&mut archive)?; + let staged_themes = match theme_dir { + Some(theme_dir) => Some(extract_theme_files(&mut archive, &manifest, theme_dir)?), + None if manifest.theme_files.is_empty() => None, + None => bail!("数据库迁移备份不能包含主题文件"), + }; let schema = database_schema(db).await?; let mut transaction = db.pool().begin().await?; @@ -502,6 +826,9 @@ async fn restore(db: &Database, source: R) -> Result(db: &Database, source: R) -> Result Some(activate_theme_directory(staged, theme_dir)?), + _ => None, + }; + if let Err(error) = transaction.commit().await { + drop(theme_swap); + return Err(error.into()); + } + if let Some(theme_swap) = theme_swap { + theme_swap.commit(); + } Ok(restored) } -pub async fn restore_archive(db: &Database, archive: &[u8]) -> Result { +pub async fn restore_archive(db: &Database, theme_dir: &Path, archive: &[u8]) -> Result { if archive.is_empty() || archive.len() > DATABASE_BACKUP_MAX_BYTES { bail!("数据库备份 ZIP 大小无效"); } - restore(db, Cursor::new(archive)).await + restore(db, Cursor::new(archive), Some(theme_dir)).await } pub async fn copy_database(source: &Database, target: &Database) -> Result { - let archive = export_archive(source).await?; - restore(target, archive.file).await + let archive = export(source, None).await?; + restore(target, archive.file, None).await } #[cfg(test)] @@ -561,19 +898,38 @@ mod tests { async fn sqlite_backup_round_trip_restores_data_and_clears_sessions() { let db = crate::db::connect("sqlite::memory:").await.unwrap(); db.migrate().await.unwrap(); + let directory = tempfile::tempdir().unwrap(); + let theme_dir = directory.path().join("themes"); + let installed_theme = theme_dir.join("theme-12345678"); + std::fs::create_dir_all(installed_theme.join("assets")).unwrap(); + std::fs::write(installed_theme.join("index.html"), b"
backup
").unwrap(); + std::fs::write( + installed_theme.join("assets/app.css"), + b"main { color: red; }", + ) + .unwrap(); sqlx::query( "INSERT INTO settings(key, value) VALUES \ ('site_name', 'Before backup'), \ ('admin_username', 'admin'), \ ('admin_password_hash', 'hash'), \ ('password_client_salt', 'salt'), \ - ('password_scheme', 'argon2-client-pbkdf2-v1')", + ('password_scheme', 'argon2-client-pbkdf2-v1'), \ + ('active_theme_id', 'theme-12345678')", + ) + .execute(db.pool()) + .await + .unwrap(); + sqlx::query( + "INSERT INTO themes(id, name, description, url, resolved_url, version, created_at) \ + VALUES ('theme-12345678', 'Backup theme', '', 'upload:theme.zip', \ + 'local://theme-12345678', '1', 1)", ) .execute(db.pool()) .await .unwrap(); - let mut exported = export_archive(&db).await.unwrap(); + let mut exported = export_archive(&db, &theme_dir).await.unwrap(); let mut bytes = Vec::new(); exported.file.read_to_end(&mut bytes).unwrap(); @@ -589,8 +945,11 @@ mod tests { .execute(db.pool()) .await .unwrap(); + std::fs::remove_dir_all(&theme_dir).unwrap(); + std::fs::create_dir_all(&theme_dir).unwrap(); + std::fs::write(theme_dir.join("stale.txt"), b"stale").unwrap(); - assert_eq!(restore_archive(&db, &bytes).await.unwrap(), 5); + assert_eq!(restore_archive(&db, &theme_dir, &bytes).await.unwrap(), 7); let site_name = sqlx::query_scalar::<_, String>("SELECT value FROM settings WHERE key='site_name'") .fetch_one(db.pool()) @@ -602,12 +961,21 @@ mod tests { .unwrap(); assert_eq!(site_name, "Before backup"); assert_eq!(sessions, 0); + assert_eq!( + std::fs::read_to_string(theme_dir.join("theme-12345678/index.html")).unwrap(), + "
backup
" + ); + assert!(!theme_dir.join("stale.txt").exists()); } #[tokio::test] async fn invalid_backup_rolls_back_existing_database() { let db = crate::db::connect("sqlite::memory:").await.unwrap(); db.migrate().await.unwrap(); + let directory = tempfile::tempdir().unwrap(); + let theme_dir = directory.path().join("themes"); + std::fs::create_dir_all(&theme_dir).unwrap(); + std::fs::write(theme_dir.join("current.txt"), b"current").unwrap(); sqlx::query( "INSERT INTO settings(key, value) VALUES \ ('site_name', 'Invalid backup'), \ @@ -617,7 +985,7 @@ mod tests { .await .unwrap(); - let mut exported = export_archive(&db).await.unwrap(); + let mut exported = export_archive(&db, &theme_dir).await.unwrap(); let mut bytes = Vec::new(); exported.file.read_to_end(&mut bytes).unwrap(); @@ -634,7 +1002,7 @@ mod tests { .await .unwrap(); - assert!(restore_archive(&db, &bytes).await.is_err()); + assert!(restore_archive(&db, &theme_dir, &bytes).await.is_err()); let site_name = sqlx::query_scalar::<_, String>("SELECT value FROM settings WHERE key='site_name'") .fetch_one(db.pool()) @@ -646,5 +1014,9 @@ mod tests { .unwrap(); assert_eq!(site_name, "Current database"); assert_eq!(sessions, 1); + assert_eq!( + std::fs::read_to_string(theme_dir.join("current.txt")).unwrap(), + "current" + ); } } diff --git a/backend/src/config.rs b/backend/src/config.rs index 09fdc2c..5adcdcc 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use clap::Parser; use serde::Deserialize; use std::io::Write; -use std::net::SocketAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; pub const VERSION: &str = match option_env!("NODEFLARE_VERSION") { @@ -49,6 +49,8 @@ pub struct Config { pub theme_dir: PathBuf, #[serde(default = "default_session_hours")] pub session_ttl_hours: i64, + #[serde(default = "default_trusted_proxies")] + pub trusted_proxies: Vec, } fn default_database_url() -> String { @@ -113,6 +115,15 @@ fn default_session_hours() -> i64 { 7 * 24 } +fn default_trusted_proxies() -> Vec { + vec![ + ipnet::IpNet::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 32) + .expect("valid IPv4 loopback network"), + ipnet::IpNet::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 128) + .expect("valid IPv6 loopback network"), + ] +} + impl Config { pub fn load(path: &Path) -> Result { let content = std::fs::read_to_string(path) @@ -129,6 +140,9 @@ impl Config { config.theme_dir = resolve_path(base, &config.theme_dir); config.database_url = resolve_database_url(base, &config.database_url)?; config.session_ttl_hours = config.session_ttl_hours.clamp(1, 24 * 90); + if config.trusted_proxies.len() > 64 { + anyhow::bail!("trusted_proxies cannot contain more than 64 networks"); + } if config.admin_username.trim().is_empty() || config.admin_username.chars().any(char::is_whitespace) || config.admin_username.chars().count() > 64 diff --git a/backend/src/db/queries.rs b/backend/src/db/queries.rs index 73e0de8..4e88260 100644 --- a/backend/src/db/queries.rs +++ b/backend/src/db/queries.rs @@ -33,7 +33,9 @@ pub struct AgentIdentity { #[derive(Debug, Clone)] pub struct PersistResult { pub reports: Vec, + pub persisted: bool, pub persisted_through: i64, + pub next_persist_after_ms: u64, } #[derive(Debug, Clone, Default)] @@ -390,7 +392,9 @@ pub async fn save_agent_batch( transaction.rollback().await?; return Ok(PersistResult { reports: Vec::new(), + persisted: true, persisted_through: persisted_before, + next_persist_after_ms: identity.report_interval.clamp(15, 3600) as u64 * 1000, }); } reports.retain(|report| report.timestamp > persisted_before); @@ -398,7 +402,9 @@ pub async fn save_agent_batch( transaction.rollback().await?; return Ok(PersistResult { reports: Vec::new(), + persisted: true, persisted_through: persisted_before, + next_persist_after_ms: identity.report_interval.clamp(15, 3600) as u64 * 1000, }); } @@ -406,7 +412,36 @@ pub async fn save_agent_batch( for report in &mut reports { apply_traffic(report, &mut traffic, identity); } - save_history_rows(db, &mut transaction, &identity.server_id, &reports).await?; + let latest = reports.last().context("report batch became empty")?; + let report_interval = identity.report_interval.clamp(15, 3600); + let next_persist_at = persisted_before.saturating_add(report_interval); + if persisted_before > 0 && latest.timestamp < next_persist_at { + let next_persist_after_ms = + next_persist_at.saturating_sub(latest.timestamp).max(1) as u64 * 1000; + if reports + .iter() + .any(|report| !report.latency_results.is_empty()) + { + save_latency_rows(db, &mut transaction, &identity.server_id, &reports).await?; + transaction.commit().await?; + } else { + transaction.rollback().await?; + } + return Ok(PersistResult { + reports, + persisted: false, + persisted_through: persisted_before, + next_persist_after_ms, + }); + } + + save_history_rows( + db, + &mut transaction, + &identity.server_id, + std::slice::from_ref(latest), + ) + .await?; save_latency_rows(db, &mut transaction, &identity.server_id, &reports).await?; let latest = reports.last().context("report batch became empty")?; let persisted_through = latest.timestamp; @@ -451,8 +486,10 @@ pub async fn save_agent_batch( .await?; transaction.commit().await?; Ok(PersistResult { - reports, + reports: vec![latest.clone()], + persisted: true, persisted_through, + next_persist_after_ms: report_interval as u64 * 1000, }) } @@ -538,7 +575,7 @@ fn valid_agent_report(report: &AgentReport, current: i64) -> bool { || latency.timestamp < current - AGENT_REPORT_MAX_AGE_SECONDS || latency.timestamp > current + 300 || !finite_between(latency.latency_ms, -1.0, 86_400_000.0) - || !finite_between(latency.packet_loss, -1.0, 100.0) + || !finite_between(latency.packet_loss, 0.0, 100.0) }) { return false; } @@ -1135,7 +1172,8 @@ pub async fn latency_history( CASE WHEN SUM(CASE WHEN latency_ms>=0 THEN 1 ELSE 0 END)>0 \ THEN SUM(CASE WHEN latency_ms>=0 THEN latency_ms ELSE 0 END) / \ SUM(CASE WHEN latency_ms>=0 THEN CAST(1 AS DOUBLE PRECISION) \ - ELSE CAST(0 AS DOUBLE PRECISION) END) ELSE -1 END AS latency_ms, \ + ELSE CAST(0 AS DOUBLE PRECISION) END) \ + ELSE CAST(-1 AS DOUBLE PRECISION) END AS latency_ms, \ AVG(packet_loss) AS packet_loss FROM ( \ SELECT r.task_id, r.server_id, t.name, t.task_type, t.target, t.port, \ (r.timestamp / ?) * ? AS bucket_timestamp, r.latency_ms, r.packet_loss \ @@ -1718,6 +1756,23 @@ pub async fn theme_resolved_url(db: &Database, id: &str) -> Result Result> { + let row = + sqlx::query(db.sql("SELECT resolved_url, version, created_at FROM themes WHERE id=?")) + .bind(id) + .fetch_optional(db.pool()) + .await?; + row.map(|row| { + let resolved_url = row.try_get::("resolved_url")?; + let version = row.try_get::("version")?; + let created_at = row.try_get::("created_at")?; + let cache_key = auth::token_hash(&format!("{id}\0{resolved_url}\0{version}\0{created_at}")); + Ok::<_, sqlx::Error>((resolved_url, cache_key[..16].to_string())) + }) + .transpose() + .map_err(Into::into) +} + pub async fn set_active_theme(db: &Database, id: &str) -> Result { if id != "builtin-nodeflare-glass" && !theme_exists(db, id).await? { return Ok(false); @@ -1886,6 +1941,15 @@ mod tests { report.cpu_model = "x".repeat(513); assert!(!valid_agent_report(&report, current)); report.cpu_model.clear(); + report.latency_results = vec![AgentLatencyResult { + task_id: "failed-probe".to_string(), + timestamp: current, + latency_ms: -1.0, + packet_loss: 100.0, + }]; + assert!(valid_agent_report(&report, current)); + report.latency_results[0].packet_loss = -1.0; + assert!(!valid_agent_report(&report, current)); report.latency_results = (0..=AGENT_REPORT_MAX_LATENCY_RESULTS) .map(|index| AgentLatencyResult { task_id: format!("task-{index}"), @@ -1924,7 +1988,7 @@ mod tests { } #[tokio::test] - async fn sqlite_persists_large_agent_batches() { + async fn sqlite_persists_history_at_report_interval_and_all_latency_results() { let db = super::super::connect("sqlite::memory:").await.unwrap(); db.migrate().await.unwrap(); let (id, token) = create_server(&db, &server_input(0)).await.unwrap(); @@ -1951,8 +2015,12 @@ mod tests { latency_results: vec![AgentLatencyResult { task_id: latency_task_id.clone(), timestamp: start + offset, - latency_ms: 10.0 + offset as f64 / 100.0, - packet_loss: 0.0, + latency_ms: if offset == 100 { + -1.0 + } else { + 10.0 + offset as f64 / 100.0 + }, + packet_loss: if offset == 100 { 100.0 } else { 0.0 }, }], ..AgentReport::default() }) @@ -1975,9 +2043,96 @@ mod tests { .fetch_one(db.pool()) .await .unwrap(); - assert_eq!(result.reports.len(), 600); - assert_eq!(rows, 600); + assert!(result.persisted); + assert_eq!(result.reports.len(), 1); + assert_eq!(rows, 1); assert_eq!(latency_rows, 600); + + let persisted_at = start + 599; + let skipped = save_agent_batch( + &db, + &identity, + "realtime-only", + &[AgentReport { + timestamp: persisted_at + 1, + ..AgentReport::default() + }], + "127.0.0.1", + ) + .await + .unwrap(); + assert!(!skipped.persisted); + assert_eq!(skipped.persisted_through, persisted_at); + assert_eq!(skipped.next_persist_after_ms, 59_000); + + let due = save_agent_batch( + &db, + &identity, + "report-due", + &[AgentReport { + timestamp: persisted_at + identity.report_interval, + ..AgentReport::default() + }], + "127.0.0.1", + ) + .await + .unwrap(); + assert!(due.persisted); + let rows = sqlx::query_scalar::<_, i64>( + db.sql("SELECT COUNT(*) FROM metric_history WHERE server_id=?"), + ) + .bind(&id) + .fetch_one(db.pool()) + .await + .unwrap(); + assert_eq!(rows, 2); + } + + #[tokio::test] + async fn sqlite_reads_a_bucket_containing_only_failed_latency() { + let db = super::super::connect("sqlite::memory:").await.unwrap(); + db.migrate().await.unwrap(); + let (server_id, token) = create_server(&db, &server_input(0)).await.unwrap(); + let task_id = create_latency_task( + &db, + &LatencyTaskInput { + name: "Failed latency".to_string(), + task_type: "tcp".to_string(), + target: "does-not-exist.invalid".to_string(), + port: Some(443), + interval_seconds: 60, + default_enabled: false, + server_ids: vec![server_id.clone()], + }, + ) + .await + .unwrap(); + let timestamp = now(); + let identity = agent_identity(&db, &token).await.unwrap().unwrap(); + save_agent_batch( + &db, + &identity, + "failed-latency", + &[AgentReport { + timestamp, + latency_results: vec![AgentLatencyResult { + task_id: task_id.clone(), + timestamp, + latency_ms: -1.0, + packet_loss: 100.0, + }], + ..AgentReport::default() + }], + "127.0.0.1", + ) + .await + .unwrap(); + + let (_, points) = latency_history(&db, &server_id, 1).await.unwrap(); + assert_eq!(points.len(), 1); + assert_eq!(points[0].task_id, task_id); + assert_eq!(points[0].latency_ms, -1.0); + assert_eq!(points[0].packet_loss, 100.0); } #[tokio::test] diff --git a/backend/src/main.rs b/backend/src/main.rs index d8dd438..18657af 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -23,10 +23,12 @@ use clap::Parser; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio::sync::{Mutex, RwLock, Semaphore, broadcast, watch}; use tower_http::trace::TraceLayer; +const LIVE_REPORT_DIVISOR: i64 = 15; + pub struct AppState { pub db: db::Database, pub config: config::Config, @@ -36,11 +38,13 @@ pub struct AppState { pub agents: RwLock>, pub dashboard_tx: broadcast::Sender, pub database_maintenance: Mutex<()>, + pub theme_operations: Mutex<()>, pub database_maintenance_active: AtomicBool, pub database_restart_required: AtomicBool, pub restart_tx: watch::Sender, pub login_attempts: security::AttemptLimiter, - pub remote_totp_attempts: security::AttemptLimiter, + pub sensitive_attempts: security::AttemptLimiter, + pub wake_requests: security::IntervalLimiter, pub password_verifications: Arc, } @@ -86,6 +90,9 @@ impl AppState { let Some(connection) = self.agents.read().await.get(server_id).cloned() else { return; }; + connection + .live_until + .store(db::now().saturating_add(75), Ordering::Release); let payload = serde_json::json!({ "type": "ack", "ts": db::now(), @@ -102,6 +109,24 @@ impl AppState { .try_send(websocket::AgentCommand::Text(payload)); } + pub async fn agent_wss_interval_ms(&self, server_id: &str) -> u64 { + let Some(connection) = self.agents.read().await.get(server_id).cloned() else { + return 60_000; + }; + let seconds = if connection.live_until.load(Ordering::Acquire) >= db::now() { + connection.collect_interval.clamp(1, 60) + } else { + (connection + .report_interval + .clamp(15, 3600) + .saturating_add(LIVE_REPORT_DIVISOR - 1) + / LIVE_REPORT_DIVISOR) + .clamp(1, 60) + .max(connection.collect_interval.clamp(1, 60)) + }; + seconds as u64 * 1000 + } + pub async fn send_remote_task(&self, task: &models::RemoteTaskInfo) { if let Some(connection) = self.agents.read().await.get(&task.server_id) { websocket::agent::queue_remote_task(connection, task); @@ -174,6 +199,7 @@ async fn main() -> Result<()> { agents: RwLock::new(HashMap::new()), dashboard_tx, database_maintenance: Mutex::new(()), + theme_operations: Mutex::new(()), database_maintenance_active: AtomicBool::new(false), database_restart_required: AtomicBool::new(false), restart_tx, @@ -182,11 +208,12 @@ async fn main() -> Result<()> { std::time::Duration::from_secs(5 * 60), std::time::Duration::from_secs(5 * 60), ), - remote_totp_attempts: security::AttemptLimiter::new( + sensitive_attempts: security::AttemptLimiter::new( 5, std::time::Duration::from_secs(5 * 60), std::time::Duration::from_secs(10 * 60), ), + wake_requests: security::IntervalLimiter::new(std::time::Duration::from_secs(1)), password_verifications: Arc::new(Semaphore::new(4)), }); @@ -387,10 +414,8 @@ fn spawn_maintenance(state: Arc) { async fn shutdown_signal(mut restart_rx: watch::Receiver) { tokio::select! { - result = tokio::signal::ctrl_c() => { - if result.is_ok() { - tracing::info!("shutdown signal received"); - } + () = operating_system_shutdown() => { + tracing::info!("shutdown signal received"); } result = restart_rx.wait_for(|requested| *requested) => { if result.is_ok() { @@ -399,3 +424,27 @@ async fn shutdown_signal(mut restart_rx: watch::Receiver) { } } } + +#[cfg(unix)] +async fn operating_system_shutdown() { + use tokio::signal::unix::{SignalKind, signal}; + + let terminate = signal(SignalKind::terminate()); + match terminate { + Ok(mut terminate) => { + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = terminate.recv() => {} + } + } + Err(error) => { + tracing::warn!(%error, "failed to register SIGTERM handler"); + let _ = tokio::signal::ctrl_c().await; + } + } +} + +#[cfg(not(unix))] +async fn operating_system_shutdown() { + let _ = tokio::signal::ctrl_c().await; +} diff --git a/backend/src/middleware.rs b/backend/src/middleware.rs index 9eb030c..ca3c632 100644 --- a/backend/src/middleware.rs +++ b/backend/src/middleware.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Request, State}, - http::{HeaderName, HeaderValue}, + http::{HeaderName, HeaderValue, header}, middleware::Next, response::{IntoResponse, Response}, }; @@ -37,8 +37,16 @@ pub struct AuthenticatedUser { } pub async fn security_headers(request: Request, next: Next) -> Response { + let api_response = request.uri().path().starts_with("/api/"); let mut response = next.run(request).await; let headers = response.headers_mut(); + if api_response { + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_static("no-store, max-age=0"), + ); + headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache")); + } headers.insert( HeaderName::from_static("x-content-type-options"), HeaderValue::from_static("nosniff"), diff --git a/backend/src/models.rs b/backend/src/models.rs index f586b9f..edadf72 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -71,6 +71,8 @@ pub struct SettingsInput { pub show_uptime: Option, pub admin_username: Option, pub new_password_derived: Option, + pub current_password_derived: Option, + pub current_totp_code: Option, pub turnstile_enabled: Option, pub turnstile_login_enabled: Option, pub turnstile_site_key: Option, diff --git a/backend/src/routes/admin.rs b/backend/src/routes/admin.rs index 726a7ca..5b0a9b3 100644 --- a/backend/src/routes/admin.rs +++ b/backend/src/routes/admin.rs @@ -77,7 +77,7 @@ pub async fn server_patch( { return Err(ApiResponse::not_found("节点不存在")); } - state.push_agent_config(&id).await; + state.disconnect_agent(&id).await; Ok(StatusCode::NO_CONTENT.into_response()) } @@ -170,6 +170,32 @@ pub async fn settings_patch( .map_or(current.admin_username.as_str(), str::trim) .to_string(); let username_changed = next_username != user.username; + let turnstile_changed = input + .turnstile_enabled + .is_some_and(|value| value != current.turnstile_enabled) + || input + .turnstile_login_enabled + .is_some_and(|value| value != current.turnstile_login_enabled) + || submitted_secret( + input.turnstile_site_key.as_deref(), + ¤t.turnstile_site_key, + ) != current.turnstile_site_key + || submitted_secret( + input.turnstile_secret_key.as_deref(), + ¤t.turnstile_secret_key, + ) != current.turnstile_secret_key; + if username_changed || password_hash.is_some() || turnstile_changed { + super::auth::require_sensitive_auth( + &state, + &user, + input.current_totp_code.as_deref().unwrap_or_default(), + input + .current_password_derived + .as_deref() + .unwrap_or_default(), + ) + .await?; + } if !username_changed { input.admin_username = None; } @@ -186,7 +212,7 @@ pub async fn settings_patch( &state.db, &next_username, state.config.session_ttl_hours, - &super::auth::session_device(&headers, peer), + &super::auth::session_device(&headers, peer, &state.config.trusted_proxies), ) .await .map_err(ApiResponse::internal)?, @@ -197,18 +223,14 @@ pub async fn settings_patch( let updated = crate::db::load_settings(&state.db) .await .map_err(ApiResponse::internal)?; - let mut response = Json(serde_json::json!({ - "settings": updated.admin_view(), - "token": token, - })) - .into_response(); + let mut response = Json(updated.admin_view()).into_response(); if let Some(token) = token { response.headers_mut().append( SET_COOKIE, HeaderValue::from_str(&admin_cookie( &token, state.config.session_ttl_hours * 3600, - request_is_secure(&headers), + request_is_secure(&headers, peer, &state.config.trusted_proxies), )) .map_err(ApiResponse::internal)?, ); @@ -389,6 +411,7 @@ pub async fn themes_post( State(state): State>, Json(input): Json, ) -> Result { + let _themes = state.theme_operations.lock().await; validate_theme_metadata(&input.name, &input.description)?; let source_url = crate::theme::normalize_repository_url(&input.url) .map_err(|error| ApiResponse::bad_request(error.to_string()))?; @@ -421,6 +444,7 @@ pub async fn themes_upload( Query(input): Query, request: Request, ) -> Result { + let _themes = state.theme_operations.lock().await; validate_theme_metadata(&input.name, &input.description)?; let filename = validate_theme_filename(&input.filename)?; if request @@ -468,6 +492,7 @@ pub async fn theme_activate( State(state): State>, Path(id): Path, ) -> Result { + let _themes = state.theme_operations.lock().await; validate_id(&id)?; if id != crate::theme::BUILTIN_THEME_ID { let url = crate::db::queries::theme_resolved_url(&state.db, &id) @@ -512,6 +537,7 @@ pub async fn theme_delete( State(state): State>, Path(id): Path, ) -> Result { + let _themes = state.theme_operations.lock().await; validate_id(&id)?; if id == crate::theme::BUILTIN_THEME_ID { return Err(ApiResponse::bad_request("内置主题不能删除")); @@ -649,6 +675,8 @@ pub async fn database_reclaim(State(state): State>) -> Result>, + Extension(user): Extension, + headers: HeaderMap, Json(input): Json, ) -> Result { if state.database_overridden { @@ -671,8 +699,10 @@ pub async fn database_migrate( if target_kind == state.db.kind() { return Err(ApiResponse::bad_request("目标必须使用另一种数据库类型")); } + super::auth::require_sensitive_headers(&state, &user, &headers).await?; let _maintenance = state.database_maintenance.lock().await; + let _themes = state.theme_operations.lock().await; state .database_maintenance_active .store(true, Ordering::Release); @@ -718,9 +748,15 @@ pub async fn database_restart(State(state): State>) -> Result>) -> Result { +pub async fn database_backup( + State(state): State>, + Extension(user): Extension, + headers: HeaderMap, +) -> Result { + super::auth::require_sensitive_headers(&state, &user, &headers).await?; let _maintenance = state.database_maintenance.lock().await; - let archive = crate::backup::export_archive(&state.db) + let _themes = state.theme_operations.lock().await; + let archive = crate::backup::export_archive(&state.db, &state.config.theme_dir) .await .map_err(ApiResponse::internal)?; let disposition = @@ -739,6 +775,7 @@ pub async fn database_backup(State(state): State>) -> Result>, + Extension(user): Extension, Query(input): Query, request: Request, ) -> Result { @@ -764,6 +801,7 @@ pub async fn database_restore( "数据库备份 ZIP 不能超过 512 MiB", )); } + super::auth::require_sensitive_headers(&state, &user, request.headers()).await?; let archive = to_bytes( request.into_body(), crate::backup::DATABASE_BACKUP_MAX_BYTES, @@ -777,14 +815,16 @@ pub async fn database_restore( })?; let _maintenance = state.database_maintenance.lock().await; + let _themes = state.theme_operations.lock().await; state .database_maintenance_active .store(true, Ordering::Release); let _operation = DatabaseOperationFlag(&state.database_maintenance_active); state.disconnect_agents().await; - let restored_rows = crate::backup::restore_archive(&state.db, &archive) - .await - .map_err(|error| ApiResponse::unprocessable(format!("恢复失败:{error}")))?; + let restored_rows = + crate::backup::restore_archive(&state.db, &state.config.theme_dir, &archive) + .await + .map_err(|error| ApiResponse::unprocessable(format!("恢复失败:{error}")))?; Ok(Json(serde_json::json!({"restored_rows": restored_rows})).into_response()) } diff --git a/backend/src/routes/auth.rs b/backend/src/routes/auth.rs index 16600d7..bc7238d 100644 --- a/backend/src/routes/auth.rs +++ b/backend/src/routes/auth.rs @@ -23,7 +23,7 @@ pub async fn login( headers: HeaderMap, Json(input): Json, ) -> Result { - let client_ip = client_ip(&headers, peer); + let client_ip = client_ip(&headers, peer, &state.config.trusted_proxies); if let Some(seconds) = state.login_attempts.retry_after(&client_ip) { return Err(ApiResponse::error( StatusCode::TOO_MANY_REQUESTS, @@ -98,7 +98,7 @@ pub async fn login( &state.db, &settings.admin_username, state.config.session_ttl_hours, - &session_device(&headers, peer), + &session_device(&headers, peer, &state.config.trusted_proxies), ) .await .map_err(ApiResponse::internal)?; @@ -111,7 +111,7 @@ pub async fn login( HeaderValue::from_str(&admin_cookie( &token, state.config.session_ttl_hours * 3600, - request_is_secure(&headers), + request_is_secure(&headers, peer, &state.config.trusted_proxies), )) .map_err(ApiResponse::internal)?, ); @@ -147,6 +147,7 @@ pub async fn session_delete( pub async fn logout( State(state): State>, + ConnectInfo(peer): ConnectInfo, headers: HeaderMap, ) -> Result { if let Some(token) = bearer_or_cookie(&headers) { @@ -157,8 +158,12 @@ pub async fn logout( let mut response = StatusCode::NO_CONTENT.into_response(); response.headers_mut().append( SET_COOKIE, - HeaderValue::from_str(&admin_cookie("", 0, request_is_secure(&headers))) - .map_err(ApiResponse::internal)?, + HeaderValue::from_str(&admin_cookie( + "", + 0, + request_is_secure(&headers, peer, &state.config.trusted_proxies), + )) + .map_err(ApiResponse::internal)?, ); Ok(response) } @@ -176,7 +181,7 @@ pub async fn verify_turnstile( return Err(ApiResponse::bad_request("公开仪表盘未启用人机验证")); } let host = hostname(&headers).ok_or_else(|| ApiResponse::bad_request("请求主机名无效"))?; - let client_ip = client_ip(&headers, peer); + let client_ip = client_ip(&headers, peer, &state.config.trusted_proxies); if !crate::turnstile::verify( &state.http, &input.token, @@ -193,7 +198,7 @@ pub async fn verify_turnstile( .await .map_err(ApiResponse::internal)?; let mut response = StatusCode::NO_CONTENT.into_response(); - let secure = if request_is_secure(&headers) { + let secure = if request_is_secure(&headers, peer, &state.config.trusted_proxies) { "; Secure" } else { "" @@ -224,6 +229,7 @@ pub async fn get_2fa_status( pub async fn setup_2fa( State(state): State>, Extension(user): Extension, + headers: HeaderMap, ) -> Result, ApiResponse> { if crate::db::queries::get_totp_secret(&state.db, &user.username) .await @@ -234,6 +240,7 @@ pub async fn setup_2fa( "请先使用当前验证码禁用两步验证,再重新生成密钥", )); } + require_sensitive_headers(&state, &user, &headers).await?; let secret = crate::totp::generate_secret(); crate::db::queries::save_totp_secret(&state.db, &user.username, &secret, false) .await @@ -249,13 +256,21 @@ pub async fn enable_2fa( Extension(user): Extension, Json(input): Json, ) -> Result { + if let Some(seconds) = state.sensitive_attempts.retry_after(&user.session_id) { + return Err(ApiResponse::error( + StatusCode::TOO_MANY_REQUESTS, + format!("验证码尝试过多,请在 {seconds} 秒后重试"), + )); + } let (secret, _) = crate::db::queries::get_totp_secret(&state.db, &user.username) .await .map_err(ApiResponse::internal)? .ok_or_else(|| ApiResponse::bad_request("请先生成两步验证密钥"))?; if !crate::totp::verify_totp(&secret, input.totp_code.trim()).map_err(ApiResponse::internal)? { - return Err(ApiResponse::bad_request("两步验证码错误")); + state.sensitive_attempts.record_failure(&user.session_id); + return Err(ApiResponse::unprocessable("两步验证码错误")); } + state.sensitive_attempts.clear(&user.session_id); crate::db::queries::set_totp_enabled(&state.db, &user.username, true) .await .map_err(ApiResponse::internal)?; @@ -274,21 +289,105 @@ pub async fn disable_2fa( if !enabled { return Ok(StatusCode::NO_CONTENT.into_response()); } - if !crate::totp::verify_totp(&secret, input.totp_code.trim()).map_err(ApiResponse::internal)? { - return Err(ApiResponse::bad_request("两步验证码错误")); + if let Some(seconds) = state.sensitive_attempts.retry_after(&user.session_id) { + return Err(ApiResponse::error( + StatusCode::TOO_MANY_REQUESTS, + format!("验证码尝试过多,请在 {seconds} 秒后重试"), + )); } + if !crate::totp::verify_totp(&secret, input.totp_code.trim()).map_err(ApiResponse::internal)? { + state.sensitive_attempts.record_failure(&user.session_id); + return Err(ApiResponse::unprocessable("两步验证码错误")); + } + state.sensitive_attempts.clear(&user.session_id); crate::db::queries::set_totp_enabled(&state.db, &user.username, false) .await .map_err(ApiResponse::internal)?; Ok(StatusCode::NO_CONTENT.into_response()) } +pub(crate) async fn require_sensitive_auth( + state: &AppState, + user: &AuthenticatedUser, + totp_code: &str, + password_derived: &str, +) -> Result<(), ApiResponse> { + let totp_code = totp_code.trim(); + let password_derived = password_derived.trim(); + if let Some(seconds) = state.sensitive_attempts.retry_after(&user.session_id) { + return Err(ApiResponse::error( + StatusCode::TOO_MANY_REQUESTS, + format!("验证尝试过多,请在 {seconds} 秒后重试"), + )); + } + + let settings = crate::db::load_settings(&state.db) + .await + .map_err(ApiResponse::internal)?; + let totp = crate::db::queries::get_totp_secret(&state.db, &user.username) + .await + .map_err(ApiResponse::internal)?; + let totp_enabled = totp.as_ref().is_some_and(|(_, enabled)| *enabled); + let valid = if let Some((secret, true)) = totp { + totp_code.len() == 6 + && totp_code.bytes().all(|byte| byte.is_ascii_digit()) + && crate::totp::verify_totp(&secret, totp_code).map_err(ApiResponse::internal)? + } else if crate::auth::valid_password_derived(password_derived) { + let password_derived = password_derived.to_string(); + let password_hash = settings.admin_password_hash; + let permit = Arc::clone(&state.password_verifications) + .try_acquire_owned() + .map_err(|_| { + ApiResponse::error(StatusCode::TOO_MANY_REQUESTS, "密码验证繁忙,请稍后重试") + })?; + tokio::task::spawn_blocking(move || { + let _permit = permit; + crate::auth::verify_password(&password_derived, &password_hash) + }) + .await + .map_err(ApiResponse::internal)? + } else { + false + }; + + if !valid { + state.sensitive_attempts.record_failure(&user.session_id); + return Err(ApiResponse::unprocessable(if totp_enabled { + "两步验证码错误" + } else { + "当前密码错误" + })); + } + state.sensitive_attempts.clear(&user.session_id); + Ok(()) +} + +pub(crate) async fn require_sensitive_headers( + state: &AppState, + user: &AuthenticatedUser, + headers: &HeaderMap, +) -> Result<(), ApiResponse> { + let totp_code = headers + .get("x-nodeflare-totp") + .and_then(|value| value.to_str().ok()) + .unwrap_or_default(); + let password_derived = headers + .get("x-nodeflare-password") + .and_then(|value| value.to_str().ok()) + .unwrap_or_default(); + require_sensitive_auth(state, user, totp_code, password_derived).await +} + fn secure_eq(left: &str, right: &str) -> bool { crate::auth::token_hash(left) == crate::auth::token_hash(right) } -pub(crate) fn session_device(headers: &HeaderMap, peer: SocketAddr) -> crate::db::SessionDevice { - let ip_address = client_ip(headers, peer); +pub(crate) fn session_device( + headers: &HeaderMap, + peer: SocketAddr, + trusted_proxies: &[ipnet::IpNet], +) -> crate::db::SessionDevice { + let ip_address = client_ip(headers, peer, trusted_proxies); let user_agent = headers .get(USER_AGENT) .and_then(|value| value.to_str().ok()) diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 4fb4207..1f4d2fe 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -93,32 +93,54 @@ pub fn hostname(headers: &HeaderMap) -> Option { .map(str::to_string) } -pub fn forwarded_ip(headers: &HeaderMap) -> Option { - ["x-real-ip", "cf-connecting-ip"].iter().find_map(|name| { - headers - .get(*name) - .and_then(|value| value.to_str().ok()) - .map(str::trim) - .filter(|value| value.parse::().is_ok()) - .map(str::to_string) - }) +fn trusted_proxy(address: std::net::IpAddr, trusted: &[ipnet::IpNet]) -> bool { + trusted.iter().any(|network| network.contains(&address)) } -pub fn client_ip(headers: &HeaderMap, peer: std::net::SocketAddr) -> String { - if peer.ip().is_loopback() - && let Some(forwarded) = forwarded_ip(headers) - { - return forwarded; +pub fn client_ip( + headers: &HeaderMap, + peer: std::net::SocketAddr, + trusted: &[ipnet::IpNet], +) -> String { + if !trusted_proxy(peer.ip(), trusted) { + return peer.ip().to_string(); } - peer.ip().to_string() + let mut chain = Vec::new(); + for value in headers.get_all("x-forwarded-for") { + let Ok(value) = value.to_str() else { + return peer.ip().to_string(); + }; + for address in value.split(',') { + let Ok(address) = address.trim().parse::() else { + return peer.ip().to_string(); + }; + chain.push(address); + if chain.len() > 32 { + return peer.ip().to_string(); + } + } + } + chain + .iter() + .rev() + .find(|address| !trusted_proxy(**address, trusted)) + .or_else(|| chain.first()) + .copied() + .unwrap_or_else(|| peer.ip()) + .to_string() } -pub fn request_is_secure(headers: &HeaderMap) -> bool { - headers - .get("x-forwarded-proto") - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.split(',').next()) - .is_some_and(|value| value.trim().eq_ignore_ascii_case("https")) +pub fn request_is_secure( + headers: &HeaderMap, + peer: std::net::SocketAddr, + trusted: &[ipnet::IpNet], +) -> bool { + trusted_proxy(peer.ip(), trusted) + && headers + .get("x-forwarded-proto") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split(',').next()) + .is_some_and(|value| value.trim().eq_ignore_ascii_case("https")) } pub fn admin_cookie(token: &str, max_age: i64, secure: bool) -> String { @@ -135,19 +157,38 @@ mod tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; #[test] - fn forwarded_ip_prefers_the_reverse_proxy_value() { + fn trusted_proxy_uses_the_normalized_forwarded_chain() { let mut headers = HeaderMap::new(); - headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.7")); - headers.insert("cf-connecting-ip", HeaderValue::from_static("198.51.100.9")); + headers.insert( + "x-forwarded-for", + HeaderValue::from_static("203.0.113.7, 192.0.2.10"), + ); let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345); - assert_eq!(client_ip(&headers, peer), "203.0.113.7"); + let trusted = [ + "127.0.0.1/32".parse().unwrap(), + "192.0.2.0/24".parse().unwrap(), + ]; + assert_eq!(client_ip(&headers, peer, &trusted), "203.0.113.7"); } #[test] fn public_peers_cannot_spoof_forwarded_headers() { let mut headers = HeaderMap::new(); - headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.7")); + headers.insert("x-forwarded-for", HeaderValue::from_static("203.0.113.7")); + headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.8")); + headers.insert("cf-connecting-ip", HeaderValue::from_static("203.0.113.9")); let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 20)), 12345); - assert_eq!(client_ip(&headers, peer), "198.51.100.20"); + let trusted = ["127.0.0.1/32".parse().unwrap()]; + assert_eq!(client_ip(&headers, peer, &trusted), "198.51.100.20"); + } + + #[test] + fn spoofable_vendor_headers_are_ignored() { + let mut headers = HeaderMap::new(); + headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.7")); + headers.insert("cf-connecting-ip", HeaderValue::from_static("198.51.100.9")); + let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345); + let trusted = ["127.0.0.1/32".parse().unwrap()]; + assert_eq!(client_ip(&headers, peer, &trusted), "127.0.0.1"); } } diff --git a/backend/src/routes/public.rs b/backend/src/routes/public.rs index ce442bc..dcfb36a 100644 --- a/backend/src/routes/public.rs +++ b/backend/src/routes/public.rs @@ -1,13 +1,14 @@ -use super::{ApiResponse, bearer_or_cookie, cookie}; +use super::{ApiResponse, bearer_or_cookie, client_ip, cookie}; use crate::AppState; use crate::db::Settings; use crate::models::{PublicConfig, WakeServersInput}; use axum::Json; -use axum::extract::{Path, Query, State}; +use axum::extract::{ConnectInfo, Path, Query, State}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use serde::Serialize; use std::collections::HashMap; +use std::net::SocketAddr; use std::sync::Arc; #[derive(Serialize)] @@ -182,6 +183,7 @@ pub async fn exchange_rates(State(state): State>) -> Result>, + ConnectInfo(peer): ConnectInfo, headers: HeaderMap, Json(input): Json, ) -> Result { @@ -189,6 +191,10 @@ pub async fn wake_servers( if input.server_ids.len() > 500 { return Err(ApiResponse::bad_request("服务器列表过长")); } + let client = client_ip(&headers, peer, &state.config.trusted_proxies); + if !state.wake_requests.allow(&client) { + return Ok(StatusCode::NO_CONTENT.into_response()); + } for server_id in input.server_ids { state.wake_agent(&server_id).await; } diff --git a/backend/src/routes/remote.rs b/backend/src/routes/remote.rs index 17cfcea..fe56222 100644 --- a/backend/src/routes/remote.rs +++ b/backend/src/routes/remote.rs @@ -51,7 +51,7 @@ pub async fn create_task( Extension(user): Extension, Json(input): Json, ) -> Result { - if let Some(seconds) = state.remote_totp_attempts.retry_after(&user.session_id) { + if let Some(seconds) = state.sensitive_attempts.retry_after(&user.session_id) { return Err(ApiResponse::error( StatusCode::TOO_MANY_REQUESTS, format!("验证码尝试过多,请在 {seconds} 秒后重试"), @@ -76,11 +76,11 @@ pub async fn create_task( &input.totp_code, ) { if error.status == StatusCode::UNPROCESSABLE_ENTITY { - state.remote_totp_attempts.record_failure(&user.session_id); + state.sensitive_attempts.record_failure(&user.session_id); } return Err(error); } - state.remote_totp_attempts.clear(&user.session_id); + state.sensitive_attempts.clear(&user.session_id); let known_server_ids = crate::db::queries::all_server_ids(&state.db) .await .map_err(ApiResponse::internal)? diff --git a/backend/src/routes/site.rs b/backend/src/routes/site.rs index 0bfed2b..9e30aa0 100644 --- a/backend/src/routes/site.rs +++ b/backend/src/routes/site.rs @@ -19,7 +19,7 @@ pub async fn handle(State(state): State>, OriginalUri(uri): Origin return local_file(&state.config.admin_frontend_dir, relative, false).await; } if let Some(relative) = path.strip_prefix("/agent/") { - return local_file(&state.config.agent_dir, relative, true).await; + return local_file(&state.config.agent_dir, relative, false).await; } if let Some(preview) = path.strip_prefix("/__theme-preview/") { let (token, relative) = preview.split_once('/').unwrap_or((preview, "")); @@ -33,20 +33,34 @@ pub async fn handle(State(state): State>, OriginalUri(uri): Origin &base, relative, &format!("/__theme-preview/{token}"), + !relative.is_empty() && relative != "index.html", ) .await; } - if let Some(relative) = path.strip_prefix("/__theme-active/") { + if let Some(active_path) = path.strip_prefix("/__theme-active/") { + let Some((cache_key, relative)) = active_path.split_once('/') else { + return StatusCode::NOT_FOUND.into_response(); + }; let settings = match crate::db::load_settings(&state.db).await { Ok(settings) => settings, Err(error) => return ApiResponse::internal(error).into_response(), }; - let Ok(Some(base)) = - crate::db::queries::theme_resolved_url(&state.db, &settings.active_theme_id).await + let Ok(Some((base, current_key))) = + crate::db::queries::theme_asset(&state.db, &settings.active_theme_id).await else { return StatusCode::NOT_FOUND.into_response(); }; - return theme_file(&state, &base, relative, "/__theme-active").await; + if cache_key != current_key { + return StatusCode::NOT_FOUND.into_response(); + } + return theme_file( + &state, + &base, + relative, + &format!("/__theme-active/{current_key}"), + !relative.is_empty() && relative != "index.html", + ) + .await; } let settings = match crate::db::load_settings(&state.db).await { @@ -54,11 +68,18 @@ pub async fn handle(State(state): State>, OriginalUri(uri): Origin Err(error) => return ApiResponse::internal(error).into_response(), }; if settings.active_theme_id != crate::theme::BUILTIN_THEME_ID - && let Ok(Some(base)) = - crate::db::queries::theme_resolved_url(&state.db, &settings.active_theme_id).await + && let Ok(Some((base, cache_key))) = + crate::db::queries::theme_asset(&state.db, &settings.active_theme_id).await { let relative = path.trim_start_matches('/'); - return theme_file(&state, &base, relative, "/__theme-active").await; + return theme_file( + &state, + &base, + relative, + &format!("/__theme-active/{cache_key}"), + false, + ) + .await; } let relative = path.trim_start_matches('/'); @@ -75,9 +96,15 @@ pub async fn handle(State(state): State>, OriginalUri(uri): Origin } } -async fn theme_file(state: &AppState, base: &str, relative: &str, prefix: &str) -> Response { +async fn theme_file( + state: &AppState, + base: &str, + relative: &str, + prefix: &str, + cache: bool, +) -> Response { match crate::theme::fetch_theme_path(&state.config.theme_dir, base, relative, prefix).await { - Ok((body, content_type)) => response(body, &content_type, true), + Ok((body, content_type)) => response(body, &content_type, cache), Err(error) => { tracing::warn!(%error, path = relative, "theme asset failed"); StatusCode::NOT_FOUND.into_response() diff --git a/backend/src/security.rs b/backend/src/security.rs index 294db0b..0c755ac 100644 --- a/backend/src/security.rs +++ b/backend/src/security.rs @@ -18,6 +18,38 @@ pub struct AttemptLimiter { retention: Duration, } +pub struct IntervalLimiter { + entries: Mutex>, + minimum_interval: Duration, + retention: Duration, +} + +impl IntervalLimiter { + pub fn new(minimum_interval: Duration) -> Self { + Self { + entries: Mutex::new(HashMap::new()), + minimum_interval, + retention: minimum_interval.saturating_mul(3_600), + } + } + + pub fn allow(&self, key: &str) -> bool { + let now = Instant::now(); + let Ok(mut entries) = self.entries.lock() else { + return false; + }; + entries.retain(|_, seen| now.duration_since(*seen) <= self.retention); + if entries + .get(key) + .is_some_and(|seen| now.duration_since(*seen) < self.minimum_interval) + { + return false; + } + entries.insert(key.to_string(), now); + true + } +} + impl AttemptLimiter { pub fn new(maximum_failures: u32, window: Duration, block_for: Duration) -> Self { Self { @@ -91,4 +123,12 @@ mod tests { limiter.clear("client"); assert_eq!(limiter.retry_after("client"), None); } + + #[test] + fn interval_limiter_coalesces_repeated_requests() { + let limiter = IntervalLimiter::new(Duration::from_secs(60)); + assert!(limiter.allow("client")); + assert!(!limiter.allow("client")); + assert!(limiter.allow("other")); + } } diff --git a/backend/src/theme.rs b/backend/src/theme.rs index 561919e..7d6a47b 100644 --- a/backend/src/theme.rs +++ b/backend/src/theme.rs @@ -286,7 +286,7 @@ fn sanitize_version(value: &str) -> Option { .then(|| version.to_string()) } -fn validate_local_id(id: &str) -> Result<()> { +pub(crate) fn validate_local_id(id: &str) -> Result<()> { if !(8..=80).contains(&id.len()) || !id.starts_with("theme-") || !id diff --git a/backend/src/websocket/agent.rs b/backend/src/websocket/agent.rs index f64d3fa..b10ea3c 100644 --- a/backend/src/websocket/agent.rs +++ b/backend/src/websocket/agent.rs @@ -11,7 +11,7 @@ use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; use std::net::SocketAddr; use std::sync::Arc; -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicI64, Ordering}; use tokio::sync::mpsc; const MAX_AGENT_MESSAGE_BYTES: usize = 2 * 1024 * 1024; @@ -84,7 +84,7 @@ pub async fn handle( Ok(None) => return ApiResponse::unauthorized("Agent Token 无效").into_response(), Err(error) => return ApiResponse::internal(error).into_response(), }; - let remote_ip = client_ip(&headers, peer); + let remote_ip = client_ip(&headers, peer, &state.config.trusted_proxies); let token = token.to_string(); ws.max_message_size(MAX_AGENT_MESSAGE_BYTES) .max_frame_size(MAX_AGENT_MESSAGE_BYTES) @@ -108,6 +108,7 @@ async fn run( sender: outbound_tx.clone(), report_interval: identity.report_interval, collect_interval: identity.collect_interval, + live_until: Arc::new(AtomicI64::new(0)), }, ); if let Some(previous) = previous { @@ -236,9 +237,13 @@ async fn handle_text( .await { Ok(result) => { - send_ack(outbound, identity, &result); + let next_wss_report_after_ms = + state.agent_wss_interval_ms(&identity.server_id).await; + send_ack(outbound, &result, next_wss_report_after_ms); if !result.reports.is_empty() { broadcast_reports(state, identity, &result.reports); + } + if result.persisted && !result.reports.is_empty() { let state = Arc::clone(state); let server_id = identity.server_id.clone(); tokio::spawn(async move { @@ -347,20 +352,18 @@ fn send_task(outbound: &mpsc::Sender, task: &RemoteTaskInfo) { fn send_ack( outbound: &mpsc::Sender, - identity: &AgentIdentity, result: &PersistResult, + next_wss_report_after_ms: u64, ) { - let report_interval = identity.report_interval.clamp(15, 3600) * 1000; - let live_interval = identity.collect_interval.clamp(1, 60) * 1000; let _ = outbound.try_send(AgentCommand::Text( serde_json::json!({ "type": "ack", "ts": crate::db::now(), - "persisted": true, + "persisted": result.persisted, "persistenceError": false, "persistedThroughTs": result.persisted_through, - "nextPersistAfterMs": report_interval, - "nextWssReportAfterMs": live_interval, + "nextPersistAfterMs": result.next_persist_after_ms, + "nextWssReportAfterMs": next_wss_report_after_ms, "realtimeHint": false, }) .to_string(), diff --git a/backend/src/websocket/dashboard.rs b/backend/src/websocket/dashboard.rs index 77abe55..462ca95 100644 --- a/backend/src/websocket/dashboard.rs +++ b/backend/src/websocket/dashboard.rs @@ -8,6 +8,7 @@ use axum::response::{IntoResponse, Response}; use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; use std::sync::Arc; +use std::time::Duration; const MAX_DASHBOARD_MESSAGE_BYTES: usize = 8 * 1024; @@ -36,17 +37,51 @@ pub async fn handle( } Err(error) => return error.into_response(), } - let server_id = query - .server_id - .filter(|value| !value.is_empty() && value.len() <= 80 && !value.contains('/')); + let server_id = match query.server_id { + Some(value) if value.is_empty() || value.len() > 80 || value.contains('/') => { + return ApiResponse::bad_request("节点 ID 无效").into_response(); + } + Some(value) => match crate::db::queries::public_server_exists(&state.db, &value).await { + Ok(true) => Some(value), + Ok(false) => return ApiResponse::not_found("节点不存在").into_response(), + Err(error) => return ApiResponse::internal(error).into_response(), + }, + None => None, + }; ws.max_message_size(MAX_DASHBOARD_MESSAGE_BYTES) .max_frame_size(MAX_DASHBOARD_MESSAGE_BYTES) - .on_upgrade(move |socket| run(socket, state, server_id)) + .on_upgrade(move |socket| run(socket, state, server_id, headers)) } -async fn run(socket: WebSocket, state: Arc, server_id: Option) { +async fn still_authorized(state: &AppState, headers: &HeaderMap, server_id: Option<&str>) -> bool { + let Ok(settings) = crate::db::load_settings(&state.db).await else { + return false; + }; + if !matches!( + dashboard_access(state, headers, &settings).await, + Ok(DashboardAccess::Ok) + ) { + return false; + } + match server_id { + Some(server_id) => crate::db::queries::public_server_exists(&state.db, server_id) + .await + .unwrap_or(false), + None => true, + } +} + +async fn run( + socket: WebSocket, + state: Arc, + server_id: Option, + headers: HeaderMap, +) { let (mut sender, mut receiver) = socket.split(); let mut updates = state.dashboard_tx.subscribe(); + let mut authorization_check = tokio::time::interval(Duration::from_secs(15)); + authorization_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + authorization_check.tick().await; loop { tokio::select! { update = updates.recv() => match update { @@ -71,7 +106,13 @@ async fn run(socket: WebSocket, state: Arc, server_id: Option) } Some(Ok(Message::Close(_))) | Some(Err(_)) | None => break, _ => {} - } + }, + _ = authorization_check.tick() => { + if !still_authorized(&state, &headers, server_id.as_deref()).await { + let _ = sender.send(Message::Close(None)).await; + break; + } + }, } } } diff --git a/backend/src/websocket/mod.rs b/backend/src/websocket/mod.rs index e46f768..1f9f9c0 100644 --- a/backend/src/websocket/mod.rs +++ b/backend/src/websocket/mod.rs @@ -1,6 +1,8 @@ pub mod agent; pub mod dashboard; +use std::sync::Arc; +use std::sync::atomic::AtomicI64; use tokio::sync::mpsc; #[derive(Clone)] @@ -9,6 +11,7 @@ pub struct AgentConnection { pub sender: mpsc::Sender, pub report_interval: i64, pub collect_interval: i64, + pub live_until: Arc, } #[derive(Debug)] diff --git a/bun.lock b/bun.lock index c4085cc..a6671fd 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ }, "frontend": { "name": "nodeflare-ui", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { "lucide-react": "^1.40.0", "react": "^19.2.8", diff --git a/docs/dashboard-light.png b/docs/dashboard-light.png deleted file mode 100644 index 269afbe44cf85bfd6d9394f4826004a502ba5a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201345 zcmd43WmJ{zw=YbCq@Xk`kZusATNYi?-KBJQBi%@+bSxU_MjGjE5b5q*aPDW1^Y?M@ z_x*OpIOE)3V8vDQn(>>p!amANV4xDA!oa{_NJ)Z~U|^7{U|`^9P>_IkSf)xp1Apk8 z#59~l9ZU_KKHJ)mt9-ULg<)r7=X7V|&Unk2Y>=`|mTd5rBe|ASvYKlr?c{3;Foy7- zG0L`PPOgRyrZDCZnArZ$eBY@JTvI|O0iu}`GHA}VefhbxF)D*bp!r>rk2 zUw~uRkB^TRbAy!VK7QwR&hd6G_@JBfK|8;91$d-lQ4ox%dM>tQ=S{g5sM2FQX}q3q zu9MrEhdEQ*Tt_qBU5BBD{{2{efhID-l2iQk)ZkvT9NYYlx7>xne?2Cm1f744prAsfe>RQ2~V1BGmUVt-8l<_v;m`g?{NdUS)o#$(6H8Nx*TJ%bf0yvE-Xm?-}r zZ=n|i#+vUIlc_Qr(IOGDC&|e%9Kbw2M*LVSF6Xp2N$Bly1NYaSwn$t=ayG)qJzz#N zcmlBm9|`)SvpWM-AZ8d|r%x@#N(FzDcHsK0pBUB`gi7&9l1ytt^}V>rvd_McedK@9 zNfEP?r)t+f_9r9+w-x7n@f`HmiT`VY1^Mk(k8x9Y=C~X3lc2vowIYVsNI04;(?ruY zl=*L;Fx4U}a`O57;nMSR(owP|2*V`5ls5LWzLv27DrbQyZDQ+7X*@VLkdRcho= z@CyjUs;+vUcq1Sn4JNM=* z2>v0rqI^OCL8!RwOFo<1 z%;;oqrG=h19&9Jtb+t`!?{Wn^uSrm8JmoA~FOEGM-OiLsmzKVN4$Ag@gxz7)=?TUl zR&2EE(+~X5Jq)Pz^!INj>ldxB`I=KcUR$@x-_Nz*9xC%&A!C8MAIN<2;nmBv>U%n2 z74obWYCO_oVzBCMR-#Wjo)wWB1ihXGrYrTaz&$W{3*^XHT+>fRuMf|+{R4}LS>$!2 zLzua_32a;MIDMXv)PE2DDB1ka!l71TA3wg-OTVSTB;gLt5e?~Vk;L{o+cHt9)I!8c zN2tsq6~OC_!0okF_7%G@lN+h$vRp)f&pM9DY;oC~lFHumetxt@4b%Xmdj1;RcH=0` z4OGwl7(rBp>v?_LOXe#tDo>&B*B=9tK8rnM{0|f5X2`Qw_C)ibk@_pi$&)_fb zIAv5V{Zc4Llx~`RcdyrXMLeh2_|Y(dSzCOpg<3>V(#9^P)tC~~ z@IMFGXIFMW<*_)E>4i+P8`kd0#A67HVo0wJQCS*IU=?j)80;-_nP*W@og@R!;1P8!|R+{hk(-_NQ~SX^8qej zhucB9t127AY@@u=7fMPBr404S9DScH4ia9k9xLy>V2rcPn6<8v8Qhx#k9BAMn_t7c zr)H(ni43<}MJF3aN9D=pW2u3J@1AGA8q!cv8O`j^czCPg${sA_5#zCF1xRK4`UAqe z$wNXy!nQXRb9!qW3cFDtoiq;LXXCn{FD_n+zxiM<)%f|@T zE}_mfw+h!a=r}=Zys+j*caW3yGGy+NDN8*-{hDEXuE3FY6Mv~4`fxV8bW-TX>htuJ z!hUu0I%Z&iucXb}>)}TXubXPFn>{+=Bf3`or<}QA$)rJj@2^JBSHxU<>xl?EzXJ?* z$Ghg+%vl9*Pbo=T8B|O2vr?4c^v_Qo*R)S|t(gC?%flOQtGUWG;f|vFv~u6av;Otp zhmi_2-oYVatLY~Cbv7%LR?xkvqKOJ?%h!xrvVeS)y`u(%BUlieD}f8IVWN!6Sz_h| zdPP#lZ!W`1#%Th`xcn5Pe>Q=NJh3 z7_>ucaM90N$9%Sicay#IRW=xckL%tM zJ1Z@=**;GT`3k33>iYYazwC`AXxnIUC8eJVXiT4c?(dTSp;V~KbEa*_6-{3xs6;uP zw*85r@bUgtv+GG=gY`;1vlw(`x0ud5uFB?0nkhpFU%$Oz=gkXt1S(^nLaa zalwm~5@NDX`% zgh4 zS@at-Khx_?T7KK*u~T=w77vU+_WEYCFN*ly{2X>J_h2(-Z;xr!^I7!0#UxYj^%0*- zu52p1+jv$so8FlJOo^lVCNE*WJed9;Y!YPN7G8Yc0bAudY;ONh-d1GqcvIpDTh^!Q z3^QC(g{(8YhBlr68VA8zUQ@nh0bvDr?Rg8_pbIN=v`Aw5|?VSVPLLrz3o(q;bl zp#A|ieG-*E>sP}xr0=q8MF}FZdZ>!?OSOA%{z&2s_} z?eTW(_Tnos(Rr3J4!?wi9NbmQ;&Q&gvDRqND*9l~A*KEF7%?a*DLH({L3Qrk7!vo6mQzyBy*I>=0M+_XBk`KnF?EYlnKN{L8< zaBPCoWMnMTiiZjdNT-@hJcHEMP-5?F`I2)YjWSLZk)YS7(*B{h4rQMUsBcc+y-)H1X zZHn_s($LT2gw`e{2p>-8wE3{)X`9%vyyF&hBQb=Ds(RXfhv`~%ZgZdN6RXRQ@$T?A@F%D(UN(F zmrm0!aphY-X&T@pYrWx&CgsaA>h6!H4mTd}giX0R@%5-ED?-NPTw=M<6_{Y$L)$I! zQ82pB;A!06DS}r#XQNYWZPc^8BcC)Kf6))t^Iz43vzdN_i49GWs3DuyEkpK({b6ez<)>szS)eL z)9>{UL_S+ZCfm3e6BPT}7sY%`>)7whb7whSstLc@7u_4k=Fnruq}z<1Eq{+bCcp)X zz+>&*j1dTS*!sD%8ow`FQ|=zIeY;nr?{u9=B6C@6jD)xFy-GP5n>Z<J(&zxxP9z-91u-kX9odWGIUrKeTn`-=r>aW}7v8GH}DvfnhNtfLc@*)w|s zg;a^|`_uZD)!hI@QTBlnZVe~DJ63MAd(-;7%J*ri&^i2~{q31EbSv?ZgU`h(=0-*M z$VZB&b5P$0iItT#le*6K*+<}fv;4U&F6RPpK2Uw3cf^1{VO8I5&=qj|v}8-r`o|_S zy@A?xJU#~Q$1p^hOGJ0^owSF@rPq@K=-Txdys_MLk-~XIcDX;Lel^ImG0ZCHOTM7q zJvxdBfEUo?BmC)JQTCp5w(s?ez8GT5>saRx*Kyuk1UyN*istyQiLy{~t*Aa%-Q*2f zD&$CdUO18vTrg8qE(@EH2zj7jg(sl5y6wgL9B=ID*%1ZJNmGE#w%GsFhWuUWIP0SvXJa zf3!mAaq^`ovqmEUfHaUR^e)~l(ZE8me*_yY&rjIdIrRj(oUXrVy&SS^bUwh-sR1#T=32RSz?OX|x5!z1Mj}YggA1&- zy)`kJ4gAYJp9pl`8d5!6Y||dQ(kpNr2ccrycau=r4yKg!c^N zd;5U6yVDzvFIJt~D-5Pz2ps662KQ6^9uRYzg%l^-9`nNf=Z-0IGbG{E(6z7&Sg}GK z{0L9{*Wp(lZW!w{*3)f>viS+xdg$L}V51U*8&g zffEUE@o(3mu4Ma<%Kv{pJ{CBUL6z+&W(_1wlhj(+THlKN=U5aJHQxU_>frYeFz2wG z{dF6*3G83gB{mY0#$mC~Q>noj`|p0}%>O@u6B!hozc%!LBeL@UMsSBm*2swBU&=U8 z@bU4zRJ{Mk3+o}r{X>#1@y}P@?2TehSnwK`<8L+ojZ>L4RVIiqd#L<_mPp{${^f>Q zsnzTuKRf{jKwyvqME*F)6uA%Pi*rK;cw1)wu)5KD_@CwJJPGdg2?uhQLxCs^9=gt- z<*@|_&`lI=fEa*jNeFJEQD@|zF;*jim~?Mr#hP9K!5-5Z%Fv$9*MG)ssmxy6nmKn$ z=_QLW4ebFtc}P+fp+SQD8NRD%Qeu1Q1OM@7Uv5S;EK@V<4YAYE^*3B%^FItWWV4`? zx$hJ3U*8D_w)XueQZ~Dn4id3??ZrFDzaco#H+1BPvfUr8YX*3M8VRoenh+)(`7@$n z&<~^i;V(t`*tm9CQWfb{vb52&RA!8U_<)2dAofQXpDL`>+)ri6R-QHg@?T2w1QzHi zK(^bZ3py@+;55;V6y-Z%@HCl+1J!AwOtchP40UVH4L+fT5@}&_>wYW7A1(EuS{%0AA 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

t_SATUVq{qlZ)WkF%W|<((e;UsfL~Gr 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

VSUW3V}e(7i?jR%W2f6-#wG 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

; + } + return (
{authenticated ? (error ?
@@ -1175,7 +1214,7 @@ export function AdminPanel({ {tab === "security" ? <>
账号与 Cloudflare 防护
-
+
TOTP 两步验证

启用后管理员登录和每次远程执行都必须提交验证器生成的 6 位动态码。

{twoFactorStatus?.enabled ? "已启用" : twoFactorStatus ? "未启用" : "读取中"}
{twoFactorSetup ?
@@ -1197,7 +1236,7 @@ export function AdminPanel({ : null} {tab === "data" ? <> -

数据库维护

备份包含节点、设置、历史、任务及安全配置,请妥善保管。
{ const file = event.currentTarget.files?.[0]; event.currentTarget.value = ""; if (file) void restoreDatabaseBackup(file); }} />
+

数据库维护

备份包含节点、设置、历史、主题文件、任务及安全配置,请妥善保管。
{ const file = event.currentTarget.files?.[0]; event.currentTarget.value = ""; if (file) void restoreDatabaseBackup(file); }} />
数据库大小{database ? formatBytes(database.size_bytes) : "读取中..."}{database ? {database.kind === "postgresql" ? "PostgreSQL" : "SQLite"}{database.reclaimable_bytes ? ` · 可回收 ${formatBytes(database.reclaimable_bytes)}` : ""} : null}
数据库迁移

将当前 {database?.kind === "postgresql" ? "PostgreSQL" : "SQLite"} 数据复制到 {database?.kind === "postgresql" ? "SQLite" : "PostgreSQL"},完成后自动更新配置,重启服务后生效。

@@ -1268,7 +1307,7 @@ export function AdminPanel({
-
+
updateForm("auto_renewal", value)} /> updateForm("auto_update", value)} /> updateForm("hidden", value)} /> updateForm("offline_notify_disabled", value)} />
diff --git a/frontend/src/demo.ts b/frontend/src/demo.ts index 6224b60..90acc6d 100644 --- a/frontend/src/demo.ts +++ b/frontend/src/demo.ts @@ -115,7 +115,7 @@ const baseServer: Server = { virtualization: "KVM", gpu_usage: 0, gpu_model: "", - agent_version: "0.0.1", + agent_version: "1.0.0", disk_read_bps: 6.4 * 1024 ** 2, disk_write_bps: 2.1 * 1024 ** 2, disk_read_iops: 128, diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index 164568f..633416d 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -117,6 +117,8 @@ .section-head span { color: var(--admin-muted); font-size: 12px; } .row-actions { display: flex; align-items: center; gap: 3px; } .section-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; margin: -1px; clip: rect(0, 0, 0, 0); white-space: nowrap; } +.compact-verification-input { width: 130px; min-height: 34px; padding: 6px 9px; } .batch-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; justify-content: space-between; min-height: 44px; border-block: 1px solid var(--admin-border); color: var(--admin-muted); font-size: 13px; } .select-all { display: inline-flex; align-items: center; flex-direction: row; gap: 7px; color: var(--admin-text); font-size: 13px; } @@ -432,6 +434,8 @@ .remote-confirm-row { grid-template-columns: 1fr; } .remote-submit { width: 100%; } .database-section-head .section-actions, .usage-head-actions { width: 100%; } + .database-section-head .section-actions > label { width: 100%; } + .compact-verification-input { width: 100%; } .database-section-head .section-actions > button, .usage-head-actions > button { flex: 1; } .database-storage { align-items: stretch; flex-direction: column; } .database-storage button { width: 100%; } diff --git a/frontend/src/transport.ts b/frontend/src/transport.ts index d61dca3..47bdfce 100644 --- a/frontend/src/transport.ts +++ b/frontend/src/transport.ts @@ -19,7 +19,7 @@ export interface LiveTransportHandlers { onServer: (server: Server) => void; onBatch: (updates: BatchUpdate[], cached: boolean) => void; onConnectedChange: (connected: boolean) => void; - onOverviewConnected: () => Promise; + onWakeRequested: () => Promise; } export interface LiveTransportOptions { @@ -60,11 +60,11 @@ export function connectLive( reconnectTimer = null; }; - const wakeOverviewAgents = () => { - if (serverId || cancelled || wakeInFlight) return; + const wakeAgents = () => { + if (cancelled || wakeInFlight) return; wakeInFlight = true; void Promise.resolve() - .then(handlers.onOverviewConnected) + .then(handlers.onWakeRequested) .catch(() => {}) .finally(() => { wakeInFlight = false; }); }; @@ -94,7 +94,7 @@ export function connectLive( if (socket !== current || cancelled || suspended) return; openedAt = Date.now(); setConnected(true); - wakeOverviewAgents(); + wakeAgents(); }; current.onclose = () => { if (socket !== current) return; @@ -159,7 +159,10 @@ export function connectLive( connect(); const heartbeat = window.setInterval(() => { - if (socket?.readyState === WebSocket.OPEN) socket.send("ping"); + if (socket?.readyState === WebSocket.OPEN) { + socket.send("ping"); + wakeAgents(); + } }, HEARTBEAT_INTERVAL); document.addEventListener("visibilitychange", updateSuspension); window.addEventListener("online", updateSuspension); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0e0b560..44b7b9b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -275,6 +275,8 @@ export interface Settings extends Omit { admin_password_configured: boolean; new_password?: string; new_password_derived?: string; + current_password_derived?: string; + current_totp_code?: string; turnstile_secret_key: string; notification_enabled: boolean; offline_alert_minutes: number; diff --git a/install.ps1 b/install.ps1 index bd6618b..b62ba51 100644 --- a/install.ps1 +++ b/install.ps1 @@ -54,6 +54,7 @@ function Write-Config([string]$Username, [string]$Password, [string]$DatabaseUrl $Lines = @( "database_url = `"$(Escape-Toml $DatabaseUrl)`"" 'bind_addr = "127.0.0.1:8080"' + 'trusted_proxies = ["127.0.0.1/32", "::1/128"]' "admin_username = `"$(Escape-Toml $Username)`"" "admin_password = `"$(Escape-Toml $Password)`"" 'turnstile_site_key = ""' @@ -106,6 +107,10 @@ $ReleaseApi = "https://api.github.com/repos/$Repository/releases/latest" $TemporaryDir = Join-Path $env:TEMP "nodeflare-install-$PID" $Archive = Join-Path $TemporaryDir $Asset $PackageDir = Join-Path $TemporaryDir "package" +$PreviousInstall = Join-Path $TemporaryDir "previous-install" +$InstallChanged = $false +$HadPreviousInstall = $false +$PreviousTaskXml = $null try { New-Item -ItemType Directory -Path $TemporaryDir -Force | Out-Null @@ -130,7 +135,8 @@ try { $PackageServer, (Join-Path $PackageDir "share\frontend\index.html"), (Join-Path $PackageDir "share\admin\admin.html"), - (Join-Path $PackageDir "share\agent\agent.sh") + (Join-Path $PackageDir "share\agent\agent.sh"), + (Join-Path $PackageDir "LICENSE") )) { if (-not (Test-Path -LiteralPath $Required -PathType Leaf)) { Stop-Install "Release 文件不完整" @@ -166,6 +172,15 @@ try { } Write-Step "安装 NodeFlare $Version" + $HadPreviousInstall = Test-Path -LiteralPath $InstallDir -PathType Container + if ($HadPreviousInstall) { + Copy-Item -LiteralPath $InstallDir -Destination $PreviousInstall -Recurse -Force + } + $ExistingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if ($null -ne $ExistingTask) { + $PreviousTaskXml = Export-ScheduledTask -TaskName $TaskName + } + $InstallChanged = $true Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $InstallDir, $DataDir, $ThemeDir -Force | Out-Null @@ -175,7 +190,8 @@ try { if ($NewConfig) { Write-Config $Username $Password $DatabaseUrl } - & icacls.exe $DataDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null + & icacls.exe $DataDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null + if ($LASTEXITCODE -ne 0) { Stop-Install "无法限制配置和数据目录权限" } $Action = New-ScheduledTaskAction -Execute $ServerFile -Argument "--config `"$ConfigFile`"" -WorkingDirectory $DataDir $Trigger = New-ScheduledTaskTrigger -AtStartup @@ -200,6 +216,22 @@ try { Write-Host " 配置和数据:$DataDir" Write-Host " 服务:Windows 计划任务 $TaskName" Write-Host " 默认访问:http://127.0.0.1:8080" + $InstallChanged = $false +} catch { + if ($InstallChanged) { + Write-Warning "安装未完成,正在恢复上一版本" + Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue + if ($HadPreviousInstall) { + Copy-Item -LiteralPath $PreviousInstall -Destination $InstallDir -Recurse -Force + } + if ($null -ne $PreviousTaskXml) { + Register-ScheduledTask -TaskName $TaskName -Xml $PreviousTaskXml -Force | Out-Null + Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + } + } + throw } finally { Remove-Item -LiteralPath $TemporaryDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/install.sh b/install.sh index 1ea4e6f..c76eb80 100755 --- a/install.sh +++ b/install.sh @@ -12,6 +12,11 @@ package_dir="" release_version="" config_temp="" tty_state="" +rollback_ready=false +backup_dir="" +previous_install=false +previous_share=false +previous_service=false case "$(uname -s)" in Linux) @@ -123,6 +128,9 @@ restore_tty() { cleanup() { restore_tty + if [ "$rollback_ready" = true ]; then + rollback_install + fi [ -z "$config_temp" ] || rm -f "$config_temp" [ -z "$download_dir" ] || rm -rf "$download_dir" } @@ -205,6 +213,7 @@ write_config() { { printf 'database_url = "%s"\n' "$escaped_database_url" printf 'bind_addr = "127.0.0.1:8080"\n' + printf 'trusted_proxies = ["127.0.0.1/32", "::1/128"]\n' printf 'admin_username = "%s"\n' "$escaped_username" printf 'admin_password = "%s"\n' "$escaped_password" printf 'turnstile_site_key = ""\n' @@ -328,6 +337,7 @@ download_release() { && [ -f "$package_dir/share/frontend/index.html" ] \ && [ -f "$package_dir/share/admin/admin.html" ] \ && [ -f "$package_dir/share/agent/agent.sh" ] \ + && [ -f "$package_dir/LICENSE" ] \ || fail "Release 文件不完整" chmod 0755 "$package_dir/nodeflare" binary_version=$("$package_dir/nodeflare" --version) || fail "服务端文件无法运行" @@ -396,6 +406,62 @@ stop_server() { esac } +service_definition() { + case "$init_system" in + systemd) printf '%s' "$systemd_file" ;; + openrc) printf '%s' "$openrc_file" ;; + launchd) printf '%s' "$launchd_file" ;; + freebsd) printf '%s' "$freebsd_rc_file" ;; + esac +} + +snapshot_install() { + backup_dir=$download_dir/previous + mkdir -p "$backup_dir" + if [ -d "$install_dir" ]; then + cp -Rp "$install_dir" "$backup_dir/install" + previous_install=true + fi + if [ "$share_dir" != "$install_dir/share" ] && [ -d "$share_dir" ]; then + cp -Rp "$share_dir" "$backup_dir/share" + previous_share=true + fi + service_path=$(service_definition) + if [ -f "$service_path" ]; then + cp -p "$service_path" "$backup_dir/service" + previous_service=true + fi + rollback_ready=true +} + +rollback_install() { + rollback_ready=false + log "安装未完成,正在恢复上一版本" + stop_server + rm -rf "$install_dir" + if [ "$previous_install" = true ]; then + cp -Rp "$backup_dir/install" "$install_dir" + fi + if [ "$share_dir" != "$install_dir/share" ]; then + rm -rf "$share_dir" + if [ "$previous_share" = true ]; then + cp -Rp "$backup_dir/share" "$share_dir" + fi + fi + service_path=$(service_definition) + rm -f "$service_path" + if [ "$previous_service" = true ]; then + cp -p "$backup_dir/service" "$service_path" + fi + if [ "$previous_install" = true ] && [ -f "$config_file" ]; then + if start_server; then + log "已恢复并重新启动上一版本" + else + printf '[NodeFlare] 警告:上一版本已恢复,但服务未能自动启动\n' >&2 + fi + fi +} + install_service() { case "$init_system" in systemd) install -m 0644 "$package_dir/nodeflare.service" "$systemd_file" ;; @@ -516,6 +582,7 @@ else fi log "安装 NodeFlare $release_version" +snapshot_install stop_server install -d -m 0700 "$config_dir" "$theme_dir" install -d -m 0755 "$install_dir" "$share_dir" @@ -539,6 +606,7 @@ fi if ! start_server; then fail "NodeFlare 服务启动失败" fi +rollback_ready=false log "安装完成" printf '%s\n' \ diff --git a/package.json b/package.json index 0bedb41..92b8485 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodeflare", - "version": "0.0.1", + "version": "1.0.0", "private": true, "repository": "https://github.com/imengying/NodeFlare", "packageManager": "bun@1.4.0", diff --git a/scripts/resolve-version.sh b/scripts/resolve-version.sh index 4419584..7b16eb8 100755 --- a/scripts/resolve-version.sh +++ b/scripts/resolve-version.sh @@ -8,9 +8,6 @@ raw=${NODEFLARE_VERSION:-} if [ -z "$raw" ]; then raw=$(git -C "$root_dir" describe --tags --exact-match --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || true) 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 raw=$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",*$/\1/p' "$root_dir/package.json" | sed -n '1p') fi diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 29064ef..7917d48 100644 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -24,9 +24,17 @@ step() { admin_token= server_id= latency_task_id= +agent_latency_task_id= alert_rule_id= backup_file= +agent_pid= +agent_state_dir= cleanup() { + if [ -n "$agent_pid" ]; then + kill "$agent_pid" 2>/dev/null || true + wait "$agent_pid" 2>/dev/null || true + fi + [ -z "$agent_state_dir" ] || rm -rf "$agent_state_dir" [ -z "$backup_file" ] || rm -f -- "$backup_file" if [ "${MONITOR_KEEP_RESOURCES:-0}" != "1" ] && [ -n "$admin_token" ]; then if [ -n "$alert_rule_id" ]; then @@ -37,6 +45,10 @@ cleanup() { monitor_curl --silent --show-error -H "Authorization: Bearer $admin_token" \ -X DELETE "$MONITOR_BASE_URL/api/admin/latency-tasks/$latency_task_id" >/dev/null || true fi + if [ -n "$agent_latency_task_id" ]; then + monitor_curl --silent --show-error -H "Authorization: Bearer $admin_token" \ + -X DELETE "$MONITOR_BASE_URL/api/admin/latency-tasks/$agent_latency_task_id" >/dev/null || true + fi if [ -n "$server_id" ]; then monitor_curl --silent --show-error -H "Authorization: Bearer $admin_token" \ -X DELETE "$MONITOR_BASE_URL/api/admin/servers/$server_id" >/dev/null || true @@ -66,13 +78,13 @@ invalid_login_payload=$(jq -nc --arg username "$MONITOR_ADMIN_USERNAME" --arg pa attempt=1 while [ "$attempt" -le 5 ]; do invalid_login_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \ - -H 'Content-Type: application/json' -H 'X-Real-IP: 203.0.113.10' \ + -H 'Content-Type: application/json' -H 'X-Forwarded-For: 203.0.113.10' \ --data "$invalid_login_payload" "$MONITOR_BASE_URL/api/admin/login") [ "$invalid_login_status" = "401" ] attempt=$((attempt + 1)) done throttled_login_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \ - -H 'Content-Type: application/json' -H 'X-Real-IP: 203.0.113.10' \ + -H 'Content-Type: application/json' -H 'X-Forwarded-For: 203.0.113.10' \ --data "$invalid_login_payload" "$MONITOR_BASE_URL/api/admin/login") [ "$throttled_login_status" = "429" ] step "static assets" @@ -100,6 +112,9 @@ login_admin() { } login_json=$(login_admin) admin_token=$(printf '%s' "$login_json" | jq -er '.token') +api_headers=$(monitor_curl --silent --show-error --dump-header - --output /dev/null \ + -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/admin/settings") +printf '%s' "$api_headers" | grep -qi '^cache-control:.*no-store' step "login devices" second_login_json=$(login_admin) @@ -125,7 +140,7 @@ settings_payload=$(request -H "Authorization: Bearer $admin_token" \ jq -c '.site_description = "Smoke settings" | del(.admin_password_configured, .totp_login_enabled)') request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' -X PATCH \ --data "$settings_payload" \ - "$MONITOR_BASE_URL/api/admin/settings" | jq -e '.settings.site_description == "Smoke settings"' >/dev/null + "$MONITOR_BASE_URL/api/admin/settings" | jq -e '.site_description == "Smoke settings"' >/dev/null request -H "Authorization: Bearer $admin_token" \ "$MONITOR_BASE_URL/api/bootstrap" | \ @@ -145,13 +160,14 @@ request -H "Authorization: Bearer $admin_token" -X POST \ step "database backup and restore" backup_file=$(mktemp /tmp/nodeflare-backup.XXXXXX.zip) -request -H "Authorization: Bearer $admin_token" \ +request -H "Authorization: Bearer $admin_token" -H "X-NodeFlare-Password: $password_derived" \ "$MONITOR_BASE_URL/api/admin/database/backup" > "$backup_file" [ -s "$backup_file" ] changed_settings=$(printf '%s' "$settings_payload" | jq -c '.site_description = "Changed after backup"') request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' -X PATCH \ --data "$changed_settings" "$MONITOR_BASE_URL/api/admin/settings" >/dev/null -restore_json=$(request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/zip' \ +restore_json=$(request -H "Authorization: Bearer $admin_token" \ + -H "X-NodeFlare-Password: $password_derived" -H 'Content-Type: application/zip' \ --data-binary "@$backup_file" "$MONITOR_BASE_URL/api/admin/database/restore?filename=smoke.zip") printf '%s' "$restore_json" | jq -e '.restored_rows > 0' >/dev/null restored_session_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \ @@ -188,6 +204,8 @@ 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_ADMIN_USERNAME="$MONITOR_ADMIN_USERNAME" \ + MONITOR_PASSWORD_DERIVED="$password_derived" \ MONITOR_AGENT_TOKEN="$agent_token" MONITOR_SERVER_ID="$server_id" \ MONITOR_LATENCY_TASK_ID="$latency_task_id" \ node scripts/websocket-smoke.mjs @@ -224,6 +242,51 @@ MONITOR_BASE_URL="$MONITOR_BASE_URL" MONITOR_ADMIN_TOKEN="$admin_token" \ MONITOR_LATENCY_TASK_ID="$latency_task_id" MONITOR_EXPECT_TASK_ASSIGNED=0 \ MONITOR_CONFIG_ONLY=1 node scripts/websocket-smoke.mjs +if [ -n "${MONITOR_AGENT_BINARY:-}" ]; then + step "real Agent failed-latency and persistence" + request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' -X PATCH \ + --data "$(printf '%s' "$server_input" | jq '.report_interval=15 | .collect_interval=1')" \ + "$MONITOR_BASE_URL/api/admin/servers/$server_id" >/dev/null + agent_latency_task_id=$(request -H "Authorization: Bearer $admin_token" \ + -H 'Content-Type: application/json' \ + --data "$(jq -nc --arg server_id "$server_id" '{name:"Smoke failure",task_type:"tcp",target:"does-not-exist.invalid",port:443,interval_seconds:30,default_enabled:false,server_ids:[$server_id]}')" \ + "$MONITOR_BASE_URL/api/admin/latency-tasks" | jq -er '.id') + agent_state_dir=$(mktemp -d "${TMPDIR:-/tmp}/nodeflare-agent-smoke.XXXXXX") + NODEFLARE_STATE_DIR="$agent_state_dir" "$MONITOR_AGENT_BINARY" \ + -e "$MONITOR_BASE_URL" -t "$agent_token" -i 15 > "$agent_state_dir/agent.log" 2>&1 & + agent_pid=$! + failed_latency_seen=false + attempt=1 + while [ "$attempt" -le 45 ]; do + if request -H "Authorization: Bearer $admin_token" \ + "$MONITOR_BASE_URL/api/latency/$server_id?hours=1" | \ + jq -e --arg task_id "$agent_latency_task_id" \ + '.points | any(.task_id == $task_id and .latency_ms == -1 and .packet_loss == 100)' >/dev/null; then + failed_latency_seen=true + break + fi + kill -0 "$agent_pid" 2>/dev/null || { + cat "$agent_state_dir/agent.log" >&2 + exit 1 + } + sleep 1 + attempt=$((attempt + 1)) + done + [ "$failed_latency_seen" = true ] || { + cat "$agent_state_dir/agent.log" >&2 + echo "Failed latency result was not persisted" >&2 + exit 1 + } + kill "$agent_pid" + wait "$agent_pid" 2>/dev/null || true + agent_pid= + rm -rf "$agent_state_dir" + agent_state_dir= + request -H "Authorization: Bearer $admin_token" -X DELETE \ + "$MONITOR_BASE_URL/api/admin/latency-tasks/$agent_latency_task_id" >/dev/null + agent_latency_task_id= +fi + 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]}')" \ @@ -243,7 +306,8 @@ hidden_history_status=$(monitor_curl --silent --output /dev/null --write-out '%{ if [ -n "${MONITOR_MIGRATION_URL:-}" ]; then step "database migration" migration_payload=$(jq -nc --arg database_url "$MONITOR_MIGRATION_URL" '{database_url:$database_url}') - request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' \ + request -H "Authorization: Bearer $admin_token" \ + -H "X-NodeFlare-Password: $password_derived" -H 'Content-Type: application/json' \ --data "$migration_payload" "$MONITOR_BASE_URL/api/admin/database/migrate" | \ jq -e --arg kind "${MONITOR_MIGRATION_KIND:-postgresql}" \ '.migrated_rows > 0 and .target_kind == $kind and .size_bytes > 0 and .restart_required == true' >/dev/null diff --git a/scripts/websocket-smoke.mjs b/scripts/websocket-smoke.mjs index 6d8047c..315d86b 100644 --- a/scripts/websocket-smoke.mjs +++ b/scripts/websocket-smoke.mjs @@ -2,6 +2,8 @@ import WebSocket from "ws"; const baseUrl = process.env.MONITOR_BASE_URL; const adminToken = process.env.MONITOR_ADMIN_TOKEN; +const adminUsername = process.env.MONITOR_ADMIN_USERNAME; +const passwordDerived = process.env.MONITOR_PASSWORD_DERIVED; const agentToken = process.env.MONITOR_AGENT_TOKEN; const serverId = process.env.MONITOR_SERVER_ID; const latencyTaskId = process.env.MONITOR_LATENCY_TASK_ID; @@ -253,7 +255,7 @@ function closeSocket(socket) { }); } -function waitForSocketClose(socket, expected) { +function waitForSocketClose(socket, expected, timeoutMs = 5_000) { return new Promise((resolve, reject) => { const cleanup = () => { clearTimeout(timer); @@ -271,7 +273,7 @@ function waitForSocketClose(socket, expected) { const timer = setTimeout(() => { cleanup(); reject(new Error(`WebSocket did not close after ${expected}`)); - }, 5_000); + }, timeoutMs); socket.once("close", onClose); socket.once("error", onError); }); @@ -317,7 +319,7 @@ try { await oversizedClosed; } - agent = await openSocket("/api/agent/ws", agentToken, { "CF-Connecting-IP": "8.8.8.8" }); + agent = await openSocket("/api/agent/ws", agentToken, { "X-Forwarded-For": "8.8.8.8" }); const config = await waitForJsonMessage( agent, "Agent config", @@ -380,9 +382,13 @@ try { await closeSocket(rotatedSocket); process.stdout.write(`${rotatedToken}\n`); } else if (!configOnly) { + if (!passwordDerived) throw new Error("MONITOR_PASSWORD_DERIVED is required for TOTP setup"); const setupResponse = await fetch(new URL("/api/admin/2fa/setup", baseUrl), { method: "POST", - headers: { Authorization: `Bearer ${adminToken}` }, + headers: { + Authorization: `Bearer ${adminToken}`, + "X-NodeFlare-Password": passwordDerived, + }, }); if (!setupResponse.ok) { throw new Error(`TOTP setup returned HTTP ${setupResponse.status}: ${await setupResponse.text()}`); @@ -454,6 +460,69 @@ try { await setTotpEnabled(setup.secret, false); enabledTotpSecret = ""; + if (!adminUsername || !passwordDerived) { + throw new Error("MONITOR_ADMIN_USERNAME and MONITOR_PASSWORD_DERIVED are required"); + } + const privateResponse = await fetch(new URL("/api/admin/settings", baseUrl), { + method: "PATCH", + headers: { + Authorization: `Bearer ${adminToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ public_dashboard: false }), + }); + if (!privateResponse.ok) { + throw new Error(`Private dashboard update returned HTTP ${privateResponse.status}`); + } + try { + const secondaryLogin = await fetch(new URL("/api/admin/login", baseUrl), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: adminUsername, + password_derived: passwordDerived, + turnstile_token: "", + totp_code: "", + }), + }); + if (!secondaryLogin.ok) { + throw new Error(`Secondary login returned HTTP ${secondaryLogin.status}`); + } + const secondaryToken = (await secondaryLogin.json()).token; + const revokedDashboard = await openSocket("/api/ws", secondaryToken); + const sessionsResponse = await fetch(new URL("/api/admin/sessions", baseUrl), { + headers: { Authorization: `Bearer ${secondaryToken}` }, + }); + const sessions = await sessionsResponse.json(); + const secondarySessionId = sessions.sessions?.find((session) => session.current)?.id; + if (!secondarySessionId) throw new Error("Secondary session was not listed"); + const revoked = waitForSocketClose( + revokedDashboard, + "its login session was revoked", + 20_000, + ); + const revokeResponse = await fetch( + new URL(`/api/admin/sessions/${encodeURIComponent(secondarySessionId)}`, baseUrl), + { + method: "DELETE", + headers: { Authorization: `Bearer ${adminToken}` }, + }, + ); + if (!revokeResponse.ok) { + throw new Error(`Session revocation returned HTTP ${revokeResponse.status}`); + } + await revoked; + } finally { + await fetch(new URL("/api/admin/settings", baseUrl), { + method: "PATCH", + headers: { + Authorization: `Bearer ${adminToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ public_dashboard: true }), + }); + } + const wakeHintPromise = waitForJsonMessage( agent, "batched overview wake hint",

!Hk3E~c&{DcuKqNvV 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 z z_-u(}mr1@vq+yV=)K&Jv3(LjAaP6n^!L3wa)NNW=RMwaXs1h@$;4GBg5SpV^uCwzY z4XGmwtPF()Ef9X?F~;8Dp*Fe5v2W*70JBI%Mb&OTBf@{V^X1F-hu(MIByMc?o$)`d zoDCtd!MHr)hZ7lb;wQ8^YJ6M`>kCM5&?E!AEs5Z&uwJOTE9Dmyf5*%>zKq>32jje6 z|K37l3vcXL^#P{`Z#BMwvX57+&g`VeBA1bAd{&!Gm;6y$e4Ocu zP7G!u2n3JYg)8al{)$ngA9KOf8Gb`srxaJij9(}L|7ZI=H6(VnUA*MRQN5Agabe?$ z!jZ*HrC6`TS7ku^Vp1u)?`2DRar^1@+b7c4?7Je7e-_4Sc+#7Pd6|n2JTH%Ws#Qa9EmHhOJu^W$9V?aD;x84Eb9;1+2bx%myv|`xN=w zagO9`HWP%qd4cyF!hTg_rHqWzN~G=3LQbQMv#jcMy<6=#+W#_E^{rF+E+M&BhR4Y= z29QQP%SX0ey;Cnk(`EH8A}xUO7tby3yn4(YIG;=@`&!%Xh02>o^Md9H*tg%U?1ZC} zAMr3S0? zP^9JTQ25WXNDqkH>4Q^NbVzi#SNvv8Q=KzKR5f}tf^^RC_r_NHy7&Hl{37d}Z#@*> z7E|~h_;}~zSXDp(-j5dgN0h!|Z=dWcy!4Fa5|gO7Pv#3oYiI?rQD^yP7lnzwNl+d- zDYN;wcjvTXdH)>-1~|O`u;n%((9hQxx+=pp^;w{onFF@63&+@TW4Ypt3Uc(i0DBS7 zO`2sLAKVlQeEtl@b?$NAr0xfC$nF|Sf3YbT*0ufqOxMe5I73@ zI%)1?Y%vF!r}YQd_c;*!v*rBQO31R7rV=-7I$$DNa*#T7&@Kvr+m)kl8gKHb{!Jdn z9)~;sZZS+zrC?7H1z4cdOE8@<*0>z*WL*hmpqEK#?d%O4>KwM54B%u|e>g7WPVH0q z$$;`Q0+Ou0jDHiKo#6AvWHswdoRWwQYMuMvlorI>o^4&P?V<9S=GI}Q%#^jU^aqwo z@?b-bkq^de`@!HoUmGl_7-$&%UZtJ_m=n`S`1gK@OP^oJg3Q%x?F`xIvXCu)W;+>^ zZS|-gT|iS)?~x`bO-NolR0_WG?-lS)XB?iTOmw>?D=SMw^CQ)*whBO#f^A|aoTmG0 zJr2gJegD~7Ae{$lDZGskIu?(GhnsAK9j&y4 z6wVYszI_={)LzA;E7*2?O`KwmDjt^pPxC$|FnIl<%{%2Xz6ViR+?1klMw~S@3Ru-Y zWPBK59v!CkV=rT+Iz6_bei{A3+&cpb>`sD31UBEx2v;~# z2*Uq=jW@h&rG}6HeGC1vVL<*K#4|7-OWKm!LHv{dkiEEj7Js4QC%~xBW}Z&0LegoN z6X)62u|1j=SMnWF@r%ivT;Ar-(R`|el1@f+YjaAjTIo*lqP%?HD@U4#{8dXm%WMvF z_d!CsVrHUhi5(W;Jo=G>VMpg7d8@@a<5{w5c2}U7+?Slg!w5G)pq=&l+(8MtEMG^z(ge($ zbDkicBF7gSMCpHHcZ=M@BOG`i6$1u<97K^7% z&(xc2+S;kV-rGDheO1`VadB~vHsxf>$(<3+%KQER-4-Mjl_H-b3J*Z+>yE0f zw15B5jW8dtdYAcXbtK0*Iosq-FnJpaPKG`;LoP276T^+n`&Eem2f4q$o*-#yX^C(J z1RFcGh<+9Tyj*T-vfe+I;7=|MDG>!))4n~bSFc8iowRprS=DPvaquZb&p7jd{Bo0^ zv9VbU0NPB{ft>*?RKU1TSNZCwZJju%;)ejBaFPuZpA z+ho_$cy&E9U*oN{5Wh<%Dov*B#JzW`1@T@&zqZVdL8exTM~7Ln5;vZ=gljFX)NMe? zT=^^{wDAaz`bb2yBr7f3+2A*120{lv8mlPj+4Z>2RJGjws*qyF zLj4*Sukk3fB+mA*p6sgi%QtK3x+{*jT2gQ2sPj)$f#cM_3aZBJ`>m@krEj{8);%h) zdu&=}d7e>UbTX+MZ>6v%u$&<(@#P^RK8S)j?;$ee~bjle2*$8p@o@|%OT2wy+* zH*cO^#&C$uP#$%uR8+houDTc`c_CjaNmv8orPjxb32sL+3HvX-gBzw5!rgn~0>MFe zZj4@3fA}2hTfG4Th!)T*+X7iKdJ{eIr>;L*+jMs+2jp@Pcj8%SS+k`#rs^sAtL$F` z0|Ic;J^_*581(dFH#p7F6pNe-0-w~>2YMhZs*Jq)puTzNd%rX!VdnDXJuTD=?SRj3 zUEUnhj047y;+CMOYi8$8f3d#RC?JY-aj%aSw!DtH&iH z4^GOeM^MSMfd}LLi%M&y>@7bhz5Q+0e=}%wR%9sBl8I-01Y=2^-=F8iWn}toDG^o9 zNuuz*kLg}sk;#^;PtoFXdhyt^vVs|rf zFcw_wV=OVs@g!Aq$Y=C{8=rP6avT1kt;>t-IZYwIspIjgp>JxB?m(q*VZP4v`-%Q~ z7D%PzHB+ub&a!Xcjnl_Y>j|t{e>LErUN(MyAzm5jQH46-e|)>3B7#$4=iK@7&D1x# z-#Q#KDT+L2UU71#bl8gs7rtyp=RmUfyvoI#W&__7>-0 z!>gb!{-drJ_R2$AY+m~{W|G*g4=D#(zWeN^@3)$%=^Pd znq)6c2Jc#0RuVtrfVB)wO%VcqnNNIP9zsxEe!QiPJ_#Vp_t%DmVXd-BUZ(<|pikCD z^8C{omDenA&7idz7&mScF^hl~@-9clQmOe6=|a}EW+(LN%8+qnMgBVU5&*a_fQeIp z$qZy_cSOBVPO*N;gXbRPs6N6yQ@cgAe|2%S9c&rA*opUKGve=~xL-Mhun-l+M&tH- z8ZM77quPzuQdnA&Oj1R&lsKGeBJ5hj7DBl|v*-vkp|dn%T&+_tj~eOXttvKFB5N%x zx|_SJ9&Jf&PQ56ScWlZ5>E#AhZQHh;y#zfU;Ut-%hzP6*+{^-K zWH+UHj-O5oF5&G-EjqWCP?na6oUuU51T6~hd|t$QMNhXjoAJ+zeIKM;5r$q0Yw8C> z!3Eo=)U3%c|H-+_>k$_=OY37UTl(7KH%W8+GV~L0znQcQK-CaDhns>QTknu1l~j`w z12^0pPoBp&Hsb^v%~5ZEM7;UJ^0NM6%>@ad;e>TSPexPv%kJXsmAY!9$WiKlaL)r&l zv45wdN>3!NlGOWD0IPRZX?pGNleG+{7jRu! zXY6xEEZze4TWed`o9CKXt#xN6DP#8Cv7DlrhZ{m~7eUYpbW^;=_GD+OwN1oCrIzGs z?9~I9W^G?Sq8Xz+RCKh`ms3}Fg_D0-3Qizsmo@!t#y;NNY07a%grn81kIUhBuV26V z?qfOPgdi3bTT?*_%W=Zq(v9G^uy%e|ZVbaOso|KwlK4`FhV{r_5}|M*V8K&0JtA== z4Kdf$QwS@%*DFNPMo*i<{(&)(A)CSUY;t3L<9lMPGB}O`99%#!HFRMHm}vK1+e;sh zJ_8!~c4BRka+|{Pwq}Cq;O`9+oe!9A|C}HvrVLIQuXPthV&7=!ZOd%1aJw?|*}2RT zqv$uep`od{Pm}Y3s!4R{yRV_eYV-FpJ0Oz*oyV#v=~_-sj$1vPOO3ba)p35%w58U0 zZ*2W@>)S!H5qYlxjs*b0y;dQQ=&tATlo}YssMl>E>^5?(u*HHC8-C1Wq_}TsWx1|++M{;{L75B zI+s6#nJnvfR(E*fg)~gr55?ks%w}IT_|Xe`73c9UQBHfxGZ#DG;-5mYYH4%c)7^>s zRc$F1Z*``{X4R(vde@uS=1zb9v0U3(6=u32rzVlER>umY zxVcjmw}9`i84imUbY{Vu;&JF-lV+U826vG>*KAj%?fV>-r@zm(cXpauyVcot=jK`y z%$y{Ik*itPRwd|dtHgt|OG@yLPn^6u@Btm103A^Z>SK|?x_^i@>`Z_sXvM)lkUd`tQX?w=;-5xf@qa;8~>i?a^KMENbWAB0`1^T zvmlIn5mtmk(4gaL#9ZPOrEhR?Y+bvns?vSVc>YfW%2yr?NA>tz&TV9ttb64FxTr76 zy!f?qpK`y~pP6!c`+D6ji|6&+Q)ZnT9J*KQX&Um|M@O8k}G_38ISq}I9?Q5j(v9bj@I5bVPbfSf!Rlpy)}AyU1p zmF~r!UNOx>LqqdLTHjp=n^?65ZNH0SqEunrQD=biE80yu)CbsX0DFNCQUBgK9tv!Z z0Jmz0`R8cWzihNt3Y&DXW;qPov9VKld>?9!^i!XUf7@ZkUy@d&rOx@VT+0m2)P1S+ zx)zrZ(g6B}cLwV~Y`f};Elq8i`PIrc4)oVrm7Db2MKZ8S0ww*ECTq~FE5`l0tPNXw1Ezq>y^>ZqjI1Z z*hdY+lqoXLPoj|9%ZJk88VeVQgTe4xf>(j?PZHlg0XQN$gb<+K3b6b;ITA6!S5| zyy_jc&hcu0LyNHnv`IQ^AGFLZUR;e^m)ZUz!ThoA6}x{Zvg!z<_`6|$PkwcvrU<)W zTma0Hi`tGNLB#qenP|z_|990yYga(H&>a%V~i-$LEkyr0Nlr`yo- zAlFSkzG&pyLG>qwhC~L;4G41NZ)p_Yaw9*4_%(+RO!T$lcSA;+K6$V<&2-evk1eD8Ncq-1uW+LC%UKr)~U2`nj~ zBBOLWtpre~9xM(Eh1n_q^3dIEN_%6wyKlnBwREcl-_pZB zRQ`BB=ZZ=Z7FK1qnZ=kN$#?PNS*`+MT5s!>>`|elxb!c=@w!^XqjI+21(A=(VL3Ne zM)vou#Z2gIQ{HB6>CAm!JQANNH3-XUFLu`N{S#5JOxa@ z^e75bYD$V^Na-qw+oDQ9oXa0OI?r_s<&4%^3F3cx{3h6jb!AY=xF>!je7C>$i=o91 zV3gK^)DbX;U{ykK>dxMFG;EhWo>@AvRyYXT!md@BJzT7%Q0rGIV6OF3FDNzmf!Vy> zR3V=sSdkm6w_l}D>hFMr_J9?`73;CaAn z*E4Gwrj!pqL6J{&8=5a6>ywiEAGEcycN6}G02z{e80M|4XzrWFZV3&G4B}-L;K14e z!$xT=>&%~b_q{@mXBU14T`Lch7=l6nl>fww6*TF<^kraBC;DPllR83?zHK6Wfg1u* z+Yod}abC?%2n)lI?VXg=)zIJrq1t|5jn$?Lq#M*K!>sD2H5?Ref&$Y-KL^VeR>(jS z@bM3cMkHka6xC;Cd2mU<0bwjY*gaz9&m`r@E%4pzJlq&B>0e7LA1_*Hv!UX{z&tx> z-5;|BLr`{b3eFf{{%V-9XfnN^F!+5~&WA43@Cn=WuW)85_qM7u4wn&>C05R)Yr8%G z3BDR#wumcxBi|m$kS)Sw=3h5Krl%SU@NS)IicDtAj0E7t0LGH{EpBqD5086)`faX{ z)y6U<ZApZB)6?~04IzXjrvp*LQNKKSbQ zzcTsb^00`28IVT+Gugf?gH{1n=`V*|Jg!m=1y&=$$duj=6}+`Iy#F;~{e$uNJ7I<7 z&qJ+O4(E#)_(Ab8|Ic&)fu4ujodIo0OG}mFpLqfJJ@om%W5xf^G3kGtquNQav$Kb^ z@{Ivw3lK4)M@3a5@dCE^Z8C8fe`3)=ovi--{TQw*Bl7oG==t*6*8TD!}<0~{9p>ByT43o#MoI~~8&j3x}SC*lI{seFHd zi(f{#twZl_cYC*|ykJkB;n=BN^?YIHw6K&jn1DXha6VBnc+N{~t$8f@>Gb8nO@)Di zHTbk;Sr*xBKr%U`u_wcGY^}pqi&W!wLO$oSHBfQ(T_K#Eh)%fWqx7D;@7xb?Th|>O z8(&f{*J;1c$6-nXQTll~4XpDmR_^cG(xv*cV5vN!{Ky1j<5;VDv@SR!Z$c;aaI8St z;RX%|>o)%<3f`chHBK<-#|5^PgR(p?^%c0F>?Qf?>KOmR)a2y;sm#ViLu%CY zSRcAPn$(>7RitS)IzCQRvC%W-Y_o0sjE{eEXG-?L0Xugc$B=^n@Wlj7(9);t^bZOi zTrk1kXa@!kO=PAUK4IXNpQ-=AK|~<<3J=tG*b&;GRq+^{$jSb^ProzoO2*{#CRBg* zq~p-s?)!OaU6}LDs@l2e@0KxTnW5zj$PvJ~E6WBY81 zb?b=F=ML$$rx0E+KiqT@>!D8y%Ot1RQiaW&VxKKd7|jifGmR2`aqt;wd{B#=bKjF$ z*dF@?JUY#*LHb4Cs!(t0;PouL!Tj{|Ax`{Kf&ys?f)%c`0nmc6+(^dZb^pdsSjJrm z#mW|x6!A)ykniBe`&AyhbL8orrCN)q^UGAJG3N%JL8DwpKSARj(J8gk?YRl7rr{oZ zpv^Dc7WmVvcmx_Ae0EoNyQ0hGl+~D3>rEIV^E3j_c^jrZLh`4;wFL#P*53lR z&Rtf}BWc|7+m4nLdBv$)pQ``Fl>fo6uvUb{D*VO4IBbm*xn<~&`yAeevsL3^^o~@2 z-pU=o-TNxK596y39BiNb4jHp0X43M*MLEJPXJl}$qa(RNl+r;JrTfL5d=?hRvxul) zxhDGODTgGKiSKv#(=yUm^~oMTyzvyG{^#1sD=YMsCDW@7!x^o~R#k_%e?7Q^G~~Lx zF=XE|mSu2b*t)$uFVmkna--_gG}5NbFSu6Ao{8yRHLPW@DkWm9?44;vQ=29XIcUZ^ zs`O($@Sf3{@_OZ`1ZLO$i`3-@@GWMN(%B_DtMgUKg{h&@S9V+D+%r7w?k*tshr-?9 zlTGyg=$8P$rrW)I>5OUp4M#)e5A3!`2PpXdS8IZls|mCwe&vWM7mvK6BnpC=jm%w? zYx$-M9~OP}GXjRNxuqr3$$X;&QfmhXkqcnCrFFVk%6lfI2Nd7aM!fvxag}QRe_ZY4CMdgzj39 zCZhJW<69a)5cIdz9g8QXB)@#p4}A`KGi0mJ(w5|&xO}jy1VGkiy#kN95f3fYah9EX z{iE{aW*o?&IUT3Z zzJnwq&~EHWEgDTLU>$zk5oj&U8g)9HjOD!eBanH0gW4(jA z_a#=pL#vsX73;OEV9}bD^rV#Uc!L6cSr@%39z{hyghI}d-GW))XMR8in+r6ORD zdF?Sq3beV@S^13-cC`e676QWm`a4-f-!_c2O^n8NfOvaCh?0=>zG3mQsHoN(!mCN$ zMbPqqr9XQ*<2*`y-?J5&Vv^-Z`b~*%uWq^aj3CLT8T%PyzE+?J$U6ftpZ$feUkxs- zXzL|GHcV%^E08|*Wr{hJn2a^OdI}ULevYSKy%vX0jqQ$uR|9Ma^Fg z9%W^1O6lZ0KYH`GeBqfguRUx?7DnxHyu>cRBONX*4K&t%Hi(IPU_V7yp)FZsH8hTSRS1&uJolh;PL}B@S)?-9zlh4KeV<)TZSBZt zi*2t|X8BVJM+zsn2?t3;LM>lre#j2gi{W~3uh#nfc-UC#b8A4mp(B_jIlJYKk5C25?Z~;ehsog+`&fj67drxHjPWb)t zxFmq|emUB(h3uh*G64Z9(6_}@{|2ZVK!EC{LFf5J1WzXDas|%mXf}OrT^^k-iUDht zT9i-p2h6X{Jzovu2rXc=kn$(s-wjyo%bMD*emxJ`ON;-RqF8Wc&^}ue8shg`Z+XDA zVjU&?UGV96MmB7h9d5q6yx}`_6Daq)1*Z$j1`>Ki#`ItN3rwy}NC)deg`AvL)VIev zu}zh%IMmN8=d+V-cJBfXdQuk-7befg2Y?DVvf~3AP4mYQVx9sbWcg>1+aeN{oyt~c zyZFbYZmav(T^DjSJ_QNy=UcnF?jTB0Sc6h3b@jf^WS^UX2iUu32aZ0Hu!(fl$?MV% z`0&oqOGmGkH7r|mB2Tk|XtN?KL{s`z9345u%if6{hiz|+wf}ryy7zgqHuO4Bv@dO2 zj-&m;fwBQV_2vNf9mo{nx`!V(GPtq=b-0i}9M(Roi4r9FcIuTB!|{;tBaklEAEbw|CdUE-2a7|H5;?5CZw?z zY#ak2_DS(Y-hN-ETm9i<|G4tSLsb~?l8m~xF0Us_7`feBv+!qkN9EZf$3A@@P@Clb z8{^j+Ba;2j4RamWI)HR)Kn)4Ifo`29&nHTygUrQ|A1FUe*E=x$l^0phAeYJ1nPniF zN%W_HzRYyv)%cE14{*vf7D$QeSNh};FA;F*y?g3wA%)kBUpnhkhcWy2w&q10ErJ~r zV8BdY7`P5Dw7tp~3O2@F&Ze+xm@dmIbmUOG?P5d6JcyCV`7qw~>Cu#^XLNR9LS82# zJ!m`XqftFtLx^?vn|4?m9cDZ3TfG>qk;);Zd|*AZX3Li^p06frE>p-o);_$U0O1V& zJ_Tf^-A4g?rAoB8o4%Odop~@_CQ_m~!^vHU2{^7HXTI{>xF$9sVH9fE+{MND$jVre zQ!n?Od#=m^VTrGMrFSux2d1>T<}!<2xxltiedX?N*;zo%Sh7E4zjEV{+&kz}WC81~ z&+2U+Do@?2+U4q9;o&yHzd*P~cte9R7`!3y{xGUBhL!b347WGViB*v_VtVpIcF57m z=|HzO`v)g3NwCFddvkEHap(sglGitchJFuKLG~dHn!@wzDW0=A|9pK;mr0G_v#1^;|N_RSDN@$aTDo6;pla87+83&LC`- zG&Ionq#geUac6vm5ZX|hAO3v88dUqLd3%KS!-;N*jbHQGK~lJIsgN~N`x*t~i;xva z?Tg_RZ@}ArN_dEozFw1Vrd4^Bq+o8{u-e$_+P3^}e66;hRA7DO)B_ynyte>?k=`Q> zY+>DeAh{p|A#Mfu>5;&&3i*YqpEYN$Yx!+Wl$z?*ZJ^FtWnI7_PC!kHv}CS)ZhWYu z>7W9hQBH@WxjTVQKnS(dVEJg&SIPUki(LX<$nb#fH0X2k>TM$zcJ}eM1|x#z^C4H_ z>HCA!ucRqg4!rF4EH1T>1=m$D!D3&i`WW7Tqrai&>inn?cD>Zzkaf zDBPrA%R$CQc8hNQv~mNUFT=jT^{FxFL;-p59=+F5T3c%XOMs9%I6A7YM3R~#f)D|6 z-O`w9ENjm`9!{oCakj&}o5#A)lgAr+x|hxXZ!wvu*Oa)$W&=Mr5js4kMcco)2t(?Z zVdgd@PT$UT*mw0qktD^gvY{=GR=rtDZxSNyTl}}|F>ylKXc*{43mF-QfY@FXAtAd# zJ@)zItyzJys{u})Yu}GMZQTD-co`$EqZI#GfaBjRA8Hu8B48l^TC?K)EPks0hY`o0RGd5$r zVqg5ss(~Y2c_G2ODpjn~QpWi=$xOn+h6k$9qLF9))MUvF+6hA1M&&5%TV_f-q#Sm}QRASqF zvHX%S%6F|_Zm1H6q3QD| zJq23;RJ+M`(5A&&TDD*?g>mm;$w7+!&t@N5tKWe^7_yYSJooI9Tg5KjO_73DN`k>1 zdlgIN=5+~5^SnHK6(4E}^`2y7)r5DmDwZZ`ZBhahKJwAgxd?YWFj*EbV`q@?NVh-* zzlmoSQ}V|Ph~Cns&`fxKlq)&sTbCW7D1m#dET3~4JAL@|4f5T1@(3GJQO2*|-iO)d zVw|pfWo@#sC2Z*I05Wf{e2pUQIF1G#+{pF&^V-Q8@@Ml|g)!0`McCZTcxL?70I?P# zHuh8a9+)c5Y{Rk>L_z|hF%C0c>It^_#jV$lQpQneK?OQo)|P$-7Fuptk_$}0ibkp= z2GXSP;oPVe`~^Eei3cQv_8pHFdE&z4^nM3O%@gD{jURkkooU_O0N^Y zZW@(VTPxf*bVysRsd-0BH9P*}Nu~+5;+CFO%l%bSgrTS-?iIFO8B=KmANmedM}>L3 zXBa?|XWO?u_cY?Vi~*TcOjJU*Wc)KbVChg^MHoGZLY_cD7c%WN(qc+MC+++}Q1Jxt z2a8%_%3v@I5yT||mL;DSs_6#V+?F$W&Qa`0OEp_f<6E@v*ZiW@x8-cZpyQZ{jJ;0h z#Vc=(%CQ`Wr6=7}zf8f(l?m~{(!Ku>^KOFDR)9p(O1Appm;}bL5@alqq*cqQ>mAZ; z5<0RKDZRm08(n))O;f+QcTffRyO&CoW%%&pAG?Z|I_uaB+tI%pY49yTm;(qd;N$lBC6=OzgL-fhB{8} zv=~z3k;9XT3LY;e$+{jsG}zVd#n~QAqR5^G&ZcnSTCv4RQsLz8(uF@%zr}N3K3k^< zpGO>N{UtG&_5&x^qq94QpKJ!MIs+(3V&39nmz9Cn|M&V?x6O@|NLStLtdvL2pmmuf zOlJ;pL>Bn*z8gw1SKi$~$~}wz-!@C^lMp1YI}0tbvfMIKw5Q#f+!Lh3$zPR5)J1OP z^IX~IENtJXe{kz;Goz%Z-K)gxFmBXqT2$aXFE=i9L0fgFJAj){wrNYX^z;h5U8#Qm`}Siaz;I#R0jhG6_v`*+ zgMNhKromNp;aU2J?&alGP*BoN+5QcBJu!0=i9{oun{t*=oegC)(00UW*Mf^Vu41dH ze5omx_J7_;?w>dEcC*#@_ZB=z5E1i7kyKFwuf)N$Hv?0>fl$hMOPmLKMf6Hff=G=` zPF(0^lGh?T3|rcpw1C-3JZ~ZIUinX#le{kc@b!QC|Np-Ltn&ZbAVs%6ymbZ385mmd zA@V>(`WuV>EuCB5TJNz{HWF8Q=pM!PJX!W##woge#VMlCnw&{&1t=5D;@+`}|1%C_ z5F|j!?g@HZfe$y>zkKk8!N5hx64lr0MlWkjT6;8IHQyH%myCfSi6YOa0^^HI>MK-P?w&mR;qly2OhrL$@$Mbs-a<(f4D;*9Dk{b+Umxmaw8Mt=d6lt<#wt-Y1es4 z<Be@Zv5IYqI)_hc0DP{9>ZtR4SLHLC3SIJzV8x|Wan5 z=(U)XlTSV^ODfx`$aLrEUXbg*R%N>fPIm4DiC(GscM4TrjnPwY1tzAVAuF(G6u3v; z>{KqYi(RF=NhZGTRe$kGMITWCl;*U(6M&yGuC1#p zDk)JtpxI9S9(wA4pHQj4WpFzGxv{S32w88@pd|5H}n0sCrQ~wL@>zKj(_`v zBVKau3Z3ZBaPP-}V+lFv%5eDo`cevXQgwOIhSe!Yd@!f|<*1{JU3}@E z$;WC*%ut(iLWZzUH@UFwV&C#n{@Ef|5bDS>cnbj}xGdEW=H{Ok3P0Jv?Qb+pPL4FV zSxh#DqizcWn#Emj4j~D>lf$(ty?4{@9u!ru*V5s`wtX%cY}t+OGPbFjw$mJ4C2gjR7@gh0&JVzJENPX?lI2aqTPfF;343txS8SIAS>ZYBbR# z9i22AV#a(n7_QMGp6R`SrcjUCVff_hn$6WAa|m7I%I9$7N0%eJTr+XHNL^{i##H;# zuStY{m zC=Scb&aNm;Vv$fC85!wZ>+kP(z$P-)`V&>@ENB&I!fUjzu}CNe9d5d$8BV>mG9hla z?tY6F{>GiZho9&X->QJ4lB4p4pxMUp5*@N?%u8tJs$B?dT|B4$0wLtt!lug z?+|stXuR_v{0gjctU@gS8*=$%Uo3p+r!V(}G{q9AAF#By!0%V=Uw%F+OO0ijA;@L! z&PJcuYmEmPP&lkq%MlKO@o1w3`$3ouZdTZRkB6{iYSZUZnmD{rxod4)5F8Q04Hl&|I8Je zAeHVMVFSS&aWUs9jGK)8;3>>nVkec}mw`s7 zL*`>*TlfbActX?5t7bZIK_}>JdB9?E=5Q8jJ~WUrDb&ubC%0RFwmgAKcZw%<#-)A}gi%|bv7F`Is! z&)*|0c&PcQH`X3KI8P)@_v_hP>{uTYCq;fZ$m1GRQ8rrY*5#5i=%$#eGF(?qPQ$&G z@a3qjDI}efHb3XnX0R^Nr@ny^a)7(NTan0SGLaIO|*pc zsWkY(v0t9k2wA}z3y(ZwWzY9~M&JP~eM3WJQ;85V8bBPt=RnA$Dh|iV6P94`rSZn) zRTD1Ygm}6Dwt-;-rC(cHTfF-9l6k9rX4nBj-isI7dV(!XqU$S?oosW012tTqCE<{6 z*)>F!?tqiLvtN>?x3z7{$-3d~#|=P{M-5m7YzdI`EsOeHf~4Sv(;n@(e!OK}b4V)Y8T}8VzRie^N z<0(&R=KLhR%8V!YU>nK+N10GuY{Zr>weloqTjf!!o0#CGz! zcRf;eQMPT0ws$X;OXG0*j&Omp1Rd5Vh98HJ=Z+}8Z@<#F30Hu|y-P@y-)f`1V@m6u zdKKHAY}6?CRJ<0exRx?%-D#$iUGEFAZ6=ap`6?7rjlvYOMmr(;Ca`t%n z>FoEZ#M;MzZ(}}7=O{V$49h=E8e04L-4!%!3-M%YSFbYdFC5J&C+vO}8hEf)7uqAp z))4jxC7dE??iZUg+LM^P5Pj!~;OuGY`_P2NjQ9DlaShcVWQ|;0=h__xl!aA`uXsOO zYQo#uYa%sY{_JveuRP$!hx|#-<9ZxX9A+WyyUL1^49}81gZH?N-l5J|-3Ka-zsho)!DypPjyXkJy%@K=A%@`@}34>}k!8Gut6(E$Mc}k2K3fuQ1 zR_Z<`1;c=WAr~IL_hg3p^*a5F{YLQ8j+4`x%i-BN?a89DK*e$my~4K9Q=;5KemMH~ zw>J)BWzTCj;~cyRF{yqJeett$-F=~nKy6OL+6%>GOaCiQx5iher%j*xO|{eUJHR1$)LCo^1FOQoVcL(CicL17Y96O(TnHj+xbS| zYCZ9Ajth@@W5UV03SlJ*-oJQ>3+ zgN!B*zl=0_GMU|xUVA&IYVKTF;PSlMkES1C4Mf7sOQ5gEF+tAvVDVJF8}N3ei=U{o zlkAaCp}|<+@$gdti2>2*XmX~?Y%g5!h0Wd<>8I#dqpI#%Ub!I+o?iM1DxL0!)iVUZ zuj;kCILmk?e9K${M+iOkoh?P0z+iI5)+Qxnc`g3iX&Z2q?u0tslljGh{Z~`<8jh&P z7D)dM-ie`)JatY(>81($uqd&VZTOi|&?H68NKhPCq z8cA{oA1hDR!B!h?*u*UYfUb2vA;P}KAjCRBrL35fdw?Zs8rI4d$AdRAGCOM6Yt|RS zSBBE_wxs)i%_@H%ETpuCht(<0TGpB2Y$^gy2fab~PfvGsjwW)KHyGJ=_Djpp-A884 z52NDO<@E`5-ir%5d`s(8gJ~9extztSw&2|exfJc%S`mjIWywgtB;gWftIOpWGd;P( z)*MK8;0%&yq<*AO?G1>bY_3C1tGMEb%FJQO0m&ztFHSe>a2cA;3_WOWUjCCO7gbBx z@Yn2dC7SDl^f9cJ&A;WjITP(~?Fd+rK| zr0Qf!e5=(3mwZmV-n%+nx;lq-Z_%+1TUz=MrDx2hDRyd_DC1tRoncAMUK`|~(UZtj z+;sFICurRaau&;F-~A)g`|Qu0cL%uyXFcQ~H2l2cK3x<}G2k3!gMGKVi$%k?PhzDi zUCly`10obTEDQ}>KQ->HJ|Sk_RoL>LT03Pu0vLX6Zy)a>c_STHztI=Qbtes2kBp0x zZxoBb9+~2l2bpS;9C;dc9B0#i^U}=CdK#DRn$v^eP7oI>h7z1jRny>7wg`1a>?y1S z;%ok7C*@;?b~J56zr1V?srM>w6(>qOEDc^~?hjp=Wf!q&w@C^lB606>!p{dNRt2wU z^(Xc9%P}l{cM4;};{x;Rbv)SMaBFT6)gTM{nUH>?=G~bd`<}T_@4>Ihs=}4~W4d8m zRRQs+d`79$A0nT14XfnGt^(x&=7eQNnTL!FDn8eKXB32&rSaQMD!O4E(aCmdZXN5N z*9k(Xl#XJxA6GCl3N2az#(qDJ7;p`tEoaA{)D~9)rb}8>ym_G?4RNMiZ2muTQHm$~ zYsKL1ir}ssv6roRjykhq?@pMFHts+@_lPtwyn+ay$a<|-MtC{i<6yWI;_ZW@i(FRu zpDjxFf48Xdk92+n4ML~L(AqX*2$vaIMj%*~S<<(6Qz5XwDE8?ZB+NBJn3*b%Siw()D*@1WWn;MXuB;9@?t z0^d)V05a80HjjpDyWwhp996G=;r>*b4{4>>C~)PAX&idVZA`A%56`O*R#E3XNwQ~O z>Q$vNiIT}?*dH;7&Z%Y9N;mqMDZdXm9Uxdk>{|z3%oJ@n5F>IaDaaba*ih%IE}5a@ zaP~%>J(PnVypoP9U*Oo>t=P`LC1w}4pF4bb#U($JEv_+??~U}q2c77HUUOnwm0co! z%5*mKI2aPVMG0BlZCpf*$1Q%tPe0OawCfQh1_z8gWW3Ee|BixK`- zr~$EXD%PE7tvyQEokCw?b3bO3w}c6ODUZuBIfgVFG{M#1jro-u|3gqZYka;<|I@}g zWQ%;pt>}Fz%;8PYs~M!#9VWq>SvG!RW&bji;_f3=Mk(JuJiV;ko=6KlGOU98<0rvs zQOI?#sMEN`w7ayURb#{~W2yBiAixVhJH06<>)NRixvbobq5H?fasNAf76!wepN#4F zPk2mM>K)u47whQbVuZ6~*!V_Nzbs>JpAuVtS9P6&IYm#wqR8X3l;hY9IcsYBA%JDf z1o#)oxkJ_&CK`6L-uBpgcW04GZ+!=LjN%QHzc}0V?qLd8ohq~msnK2@$mS~4qZ1%6 z#vI~%F4z`&k}@n%;c;Fm;fk8?wb+M^zImI-Wzy1!B zFYTrF==IAMnUd$CYf}it*)aU&ry)6V4)6KRz9?d<&G~FX5t=}LKHk(pX)QeqP0u+;zzigV*+i_owzh8niM53xTNPbR!|}bp*g=9BR@4Yxr0cf(<0Z7$ z)frs;ew~*`dVNdPt*bi0`xKkhUr2jy7St#d17X53$QcVV$;+2OGPccxg0(36e@4y{ z*4LxFXA6c?Kmqw>eca~IKS%$MrFHPu&1Ya^avERlt?GUIkB|I|`A)#MVF_Moq7dj_dd%Pq(= zj0$&HS~~G-Ehh}@bgL|JpiNfi1}qvh%6CHV$P0$lzWVmLi*P=z=PRX=>9sX3d^mdb z#8x~D2(y;en$x}AeE!*t1pdbSRQ;3iR$ew>TRC93`a$6gXJfC3lTQKX6cy8RMQ-g* zX;>^oFMR#>2I-i9+}UOTWU>-C&$f_1J6yob7|1c`d=@jp}Scaxupr<%GKY3(LIVZK*bV*j^!vRwFX&Lu`Sr zfpyq|fR&v=?q&$`#iw?&f^8AcPRSDg?J0pF`HRofq8%5RMj0Hiweaf;fwk6Y`dmuu zpChCpKsuIuR{6nfY2$`hvyVg^2k$m|Y5!;qdzTa1(1`xbl^VPn<1N$60H)oTknagtW1B$j zajj*g3dv|5pocgu1^WtoRs^NnW#u-mYEU$VLp5I&(=FUW`rZU>|NdqRu}f_9`v%7o zS$xhQhJzT|z7CyiQLok?t;?=|!M=F1d>;-rHXX+Y9q%&-MC$E=N zY1P6wX%}+cqy5zSXFa(prLKhsC(jUwT)6uszm~6Mp_yM`N zJmb1?V1e@3RK|N1rhHI9?mI%(5E6^u)XsTmwdFE`ZN)D4Bl;q|0dDNs-!Yb7QMMSP z_VaT&OOIK}vGqweq;nzvh3tJ8s$@ZE&ljnF5YNO%_3yKHI)H1_jv5P^~jEZ&jq@I?N%*w_M_emwhDshKJ zp3*z(gvkiSveSTCeck?b(?5Fqm*aM6zDB;a99z1F5m_|=8Ew2RP8C(F956$`+?g}% zb#ef*Ogs(DGrZ5}JqqUkRo!M zNy`#pXJ)Z7Rpwe1IQV7ena`lf@xcBYcDZqD{EnGQ!r6AFpZ6i+O_7`9`?E6~P7~r# zm4|sEVRg;lgO4^Jqar*q-Ee4(K0EpzmVy)1NwFG^cyrQh7k&0IgJ;^59gdPybJRtF zghxi-l(ao@N}Lv5kk;@DMm4^<9=FtIK7XkcUGra%Iqc7ltoaw`nj*Rl zS&x49zs}*y#+L)geZnwTet+I~rM2Oz?7V=zvU@>O3M}E8ynKB78`IO4a-nBUPg2s; zLr%e6rWN)Z2TUmwf~LAGG9ciTTgOm~H?;M1xW~ZIN`Q-5!E#V$<&}n(4m4bDr{2iO z=;N-Hzs;#OwZBRvpXsT6hQ$TQ?*37vF4IzNoM0OBOv_)h+5$|pz;`Bw z*)$#M>uw7tPK)UL>(;C)D%Xo(uzp6k?pJC5&AX!QpGIxXIXzFmyIDMH{Pb|c=j4#y z1@x%XPC@#mO;_+K{b^>-(c!QqKuyim4F57+>TdTw?zsnMKmkG=CSZ{U=e|2R_CLM1 zmXiO_P?ctNuEEV9av9Mi zp?lU8T6_zKch6X2&@3v!6h@;agPv`-R-)?Bx~VfN4g}1~seQ&yW%qIA<7FHI^;v}- zp616OzPggg)ZN+ogU_Rt2f)$EG_83&P^Wo&Ym;bgYo4LOFpX>UyEyL-ZOG2P#Nxgb zz89KddH)L&gTu~9F`*D2uJ@iIqZcSF!9@%@Og=knEjawx_#!#k*_5%g`b-ZO_AIWg*rd6vXs7>;a%TfinIQO%q{S8yIF;n(zZ^ltDCOTZ9kd}zLaq$- zu+s0OYCp^06ww}%o*~MmUA5Z352COQ&)k)jF)Mp`v61>`F+E|v-`n3G9*7-?NbTSY zn-iauj&dDGEW{uZtXXO>iGbpQT;k3!H8X#PVe}%pH=_CF%47pvoAWk0<6GO*`dsR1 zV`^1YPb8(8W|%Ir^|VV~%E>5VZ*@#H$)d5)D4njaZ0vr9+w8(=eX1@*HS%j9{>R>Z zh|4)McJD9)pIWEpKLVd{7aA96Fe0m&-o$umB1hC|n0N(~HJo&BWHmt0;*;$@8J6J# z(B&p#_th_LQBj09cwA8`2-wU_w=b~D-hEeb!&PrW()leOF6c6`{SUn9F!^4{p?^`t z4POHwoX4KqT!f1J86NY@*-*&IuP5GTKOd%KV+V8K@Z__8G??qnwS$S+21vd@ioJo6 zuYRLh%Th@u0f;I37Gs@Ock%_Sd7W~-f_|Q{4CHbxbjPPh5Kq_V;@U-Yrs!|Vjh_ez z37K9aUuD^sQ^;#n1oHWE5>F1qdyGv@(aottDWkgkoRZ`IvIl!2q$>rMTFv5~phx=E z#g+{2jIV!rQSkp=@jO2+F}z>;J*Vk&^m~q1PB7*Bye5yV(Ziw8RxSC{<$^$^A0mV5 z)&n-7`svjI=~ugDaj*EG&wG}lEN!qqjGE6knF(Lf*?V6=I2-F3{zj`YiB0h|V@$7_ z&31;DGWN8`cx?4hrbqon=$8=G*+KKZ@J04zLaLf6nrzo!t5(XpNOW<-lb4roX&_)W zRlY5_AL+;7yTQsgUh7K8bm|>0bf|Um_{7{8oYaVwvWep#|JXPPMakR~H!z{Y1(>YM00bT?aXXQDi7w)38IbSpg< zs9I(iEzK2CIoRa+OY{b{f_mPLc4zID3o^*~6&%QskSK-@lal?lDr3nX{o?Iyd_!ED zfa3Yd6epbsJAs&fD~`{;tDADBc5OT=crk9ji;$|ddH9uns-nui2)U#u(X0Y|Zii3N^A6f)jh2p$-5t<>yv-c6pf2N@-!mR-oeQw#j zaVR-%9Nmn0iQam33y;y^lqM(Iq%zIJB%Dt!=-_7uy!E`~HjB8CYQl|hTzP=Pky$?< zZ>Jl3;p}N(b;wbl>@gfaQ`U0dK;yHW!;<$@dEruBmzMo5#!Yy4s-kFb^?6_K7d)+4 z$dPloFLZFZsg@v|>%8NVnp~Yh4y2zjnp!zI%+_qEJUd|^B2AOFgvw)ZnLTIL4Y^~$ca0S8a z9Zyg_msLqOVv=uTEt>QWXsP>xSq7olaL>3*#$CuMk;4Ah@24XOuoGlH(_FY9T!9V% z9y4S3DTp1oNpwovmR%jVZPa{iB|h77&Ns9wcsMZ2-9nMpN5|zG|7+$GRrxbx9i4;_q-XMZ z*x&;NL0bY0h6K@swLc5{)0XW^#=kTdPE?b>ux*YPNip$}KT_V4W(Z8gs2qT)1j}qV#TrQip&V|L;QqVqr9|bP^b2b8Dd?FFKs06 zrFyqh|KnZdq~0NX(|~$>l%BV^1XQ12j52tkCviNsxz8#0A%)z@$MX;vUw34ruJC;4 z?*$VMc-n>RZcf2k7B?8Stkh8sP6fsYUbCp^tw{&pAh}D?ZpPro_<>*j#U-HYUSBq+G8s-WeJ=zl z_H_qp)gqXsLMo@yzk+IZzMSn5&Qs?7*Lrx~Orf1{j5l(>!bTgl(k(@+E9yP6d5^b7 z*i&+T+@}+f)bp+zvG00Ex#q;*Qdef#jq&3t+iu%G%H`^x7K&SJP70v!s|p3&ww~D0 ztzR`-0Pyk_43|3aYn*8FG%Bb+F zOmckCD!9m}RF^Q&2(}000m2JY6@R^gpb&T6#GTM+_vRyjR8J1ZU&L7keG9Pv<26h3 zrYA97!^FSDovD;r{ur{g^e36EVTo42oVT2ZN7WnLz&z`*Iofu5fF2&P^WILY^ZmSw z)03;9a&}5o&;j~FKb-vw@Q>u{CtD3m9%^HR_lklkIyd8B!s^DLjo(*wX9vxQkgf-Ez4o$|EF+Vqtb+4qm~%WXaAz{6~t*xUTWd(o$jbGj{#gb zs?Mq;ev{paA!)k7h4Ar7&TS(tm?m217rbp=7`xZsuURg>e6t8yykV)&T44fDAJt_l zv{T8yX#2Xa8lKVf}Y&c78x< z7DJYGdRcM4eA{H~VvpelNo;CmjX2wYK;d;n28;X|!u^S{;X<;)j|W8x{aU++z5O~} z-WLNMi>0Qw(}MHN`Alb9EV;hu(xq_$a4}E9QpnPI^l?7z)Vyevg1S}49Awp>m=%kBuRNpT; zM$2=ISrKRaY=0dF;gd?U;45b@wSOJwop$v#%_wCil7(5r4|OWp4W+>Z6=zgAhy0GNi1 zjp^o8sq?*>(*EAm)>Bo)edJ=Ki*UJpt9^Z6Ody|>Cf}UFZECX z)nbwI9B(b>5V_OpxD~|1BgGG7O@Pil@rRVPqkM+swK#Pcg}S*l_OC z18O>rD#lRw6BVl~XTt)xQ~-OtR<-eN<4gdj%Ot0%01VrolJ1vm7Z>A0FXGR>EL3ti zVCKH=kmu5%g(I-E?pVrZa&q(hQPhO`DCI-u)8U(^ytCMeUqg$XsHA1-Zc(nDDe=y! zXO&52V6{pP%sU2JQ-LN4LXmOC>u1TDj8R(F99hUi%+FaVjnr1c^P8~t z(BO?zOLG<&N1~>uN@0rrT#D&%dY!=xQ<-brT>Fo{bq2I-FR9ajOmz_GFHIlAtC!sAiU2%>UtN;FM<_i_gSrlMU-qL ze*-c}vjvymPiVlE`w228);z_25=MtmF1hvyWVMMUW}hzUEb(f}B)Xi25A*f0&pCmToy%M>v$${LzPr4XghQ8L~Cx01YNMzCCZ z&7R50I8qZKZt`i+SYneL^Q9<$#P`10Yt(VwgH4@YV` zz1{_?FE7NPE_IUC(8L15Q{}tafTVaqyA$;K=xyhMBq^Z$z8cpp+D%sTictjbP8-cT z&-U+)F=343Y?y0Z!$?k&2J`s`;F&XWR{0o&x~Q!HedMoyq~nh;q@7&s*0M>3KfOc; zCxRn5Rf|;aa)}>nfBF+E8SBgMmGh9JM*ZL*pQdsrrveAC-EuLqLno7 zUtzeSU6~y>Xg3^Cu&UPERmy+wGRk0=#FP*KCF{fgL&;j+Z0pu~7krNbcn^@Qof|j% z=bw~-;xZ6p^7+ZN)@RqcMGOCADxtnOv-)iob$OC@c)fe~e*XcPZ7iZ@_Ab(m7G^=_7#|-31z8O)y@?+EX zs}wDg67Qk=d{eetHm{qBEin;+0IUvJ{J)5gf7$UT=g>4>;sdsqB+hdy%u`&IYomV0 z)z}WNYdtRW1{~p+oo_KZc{17x0HSjH&1{-TvpAZVh1-eG{BLO_3SPNqQomf{>Rkqr zeZo&h^5;E0c*cF_ZC1~cw`G+8i6<{0e`mM0vlYm=OTrqagb?xUx(ja3Bjb7$5fwBV z%NH9qY8=T-BARpSlY1jpm{EEgDK&Tf^))sOsp5PWJ=jW6qK1Zq{$V<}emzUDhNkZ= z&?NMZ_w$pl2UourwAaSQklalzFk^TWcA|SGcWJ-;27=@+!#}MS+6oXYFDbrxt7*Mv zVoJ%BaQC-;{a2;7XLp>Z3n)qEABI%!-vFLI{{}d;6{}Wx_4V?7`N5QWrCQdA@&2n4 zGaiD7X+IMd4gCZo>E zgz}$jTP;)AaD;r{p?<-YpS6G}n{_9P15;9RjI*$b2PD>t{6>|2=BYX^E+b>=?hpS; zwb@1f1ioTZ{^3OT=WR$iI@c(V^!1BRwpFE=u=eenIYSE;;$PY;xC)RYSJg@M0k!#u zz=Q+@Vr}i54h#b>?-owcvsq(|PNS=KL27ZW^PB6-A1J@7M%q)UB z6>>6*^9!xuS5vOuDjr z0*rNySKEZ4rXgrTSFUowMOQb*Y-4GQUM7%fx%TyC;IN0bZM%z2jQbTZxFR9asmjQ7 z^cES(qvut|#;rzPhFu^FuTiQK1e`BDYn|7w8Ia_YJp1>{Jb*77pwe1847Wd*MG-7ZN8a>g+A-{ zo-aP=4y~5XoHg+EZ(npz5rcnVOa+JBRrmcV#Ri1sMlOPiHwN`5jw}<-W;X(5L zl}0dEUSB|O0nC$nL16Z%x4!KH1-v)%TeD592BAz~6kA*v($`URU<&3BS>zi(cE+uc z@3}a-WbSg>ziYWLE<0XxysMkctM~w!@0wIvjao-CRA4yrLPnJY796Cojof@PoOEu& z38r)@S?eid_r1$VKycVPh^+>mMKslPfMlYsxz6}rB$&3@$)-Qp41IaKW!mwT|3uev z?MUIHOw~nyL~^}RV-e5`^|t@KVqh0YFYNll_ovP3&y({IC)pmIB7Y>yw?=eEMh$@J z1QMeLgU1Rz8^_$IUUcj0m6GoGZaRbAsLRPFzD`68y-o$}Ml`2157LM7TFpi}mldUR zy%@g57Hgaf;#gawGk~VJ-dD6rncq-ST#ckwL>KXrmc1|J)rGa6jq8*fqt2mFYSl~i zwbiw+J!XGL7GB6-%0phrVPYGODja3}H_M8QP1iIp^K2u{vgcuOlY+JauW5k2h$J>+ z)y!hNt)*zpDDG6hCb4jQVn@+-$%Ib3Z*ovun zWu%r#aYJZu`Pwj>{q=~KEfnmYk7AqNYbO4@(*ljbc+~qL4-psaRthbxLxT7lXi*z| zYU~-tiy2Rb2IG&?>Wwt?(02n|VG`Fl-Gk?-Nm8_%%r*obW7bbeFeav24#rVNrY3Yt zs>X5k)zFHdrZ%4z0j~X76Zo>sSVCQ=Blv10NNXCi0Ok;ovl6K+-{tmdmLV^G7kHn*z{UkU1=NQ$Cy8h}VL;T!pICEc$z z(+sAidRzkG7b~f}$>%|duC?9>)S^UZG1hT(kcR2cJ)QVf%&QFM1VXmgZ9K*zPkk5(`Gj^m5QZX2+YmR^O{3-bj;b50X!L(&zR zoG8I77We83X!D`57hz`I3d|W*OKD=O#~^P=C-c}|D5A1h)}nQnRB#9vis(1OpP|#!%)(n84+0#4YD}J*_=%`(;-6 z6*I*0vAHbqtiH1RzpA?OcqrF4Jcg0wI5A}#U!AODPqLJN6%{QMO5dY%zCX@??{EH?_gSv{y6@|L?)!N~ z9_DUE#khc+g`4qJJ~P=78ZnG}cCsv=dp3;Ph%qTh&Y56f_}!{#)Q+~GuR!um*qA-I zk>VTZ7xcWoMz)(+nl738RwnQf;vk6Ouz9F=qV+ zS?-wbfS^k1@By!HeIeFu2m((sdNS>S|7p|S7L8uBWOj}*;yA2Wh+8`6*3h2c?6-YX zTF^j7uRy4ZaoPNhk&5wMI0L%-w}sQ(ri;%Ls&VO~?<|Jy2UUlUMJ=M{&BlJ`?W7fN z0`)j3zrw+ihcbfL=<4Rdxw$v*DiowEA{HvmCTY8Ath5s`yFlA)yeBb@SCvhqymp0e z{Bb*kADv)ya<=hFm|JMh!W|QF_3T@;f$UX+ z$5qOHVA7Kf7x-+(@qjOqPvv99%rdR7N@T9qF1vI4oKM#KtJ~%sh^2%0y-07fDhTv>Zr;yu^HO9g z4`~HS>QoB_7e^m3#U|#G-;+VOQWu9TY9 zReni0ZE{VMTnvj3AV+EwQY0YeM|O|0fqxym1RdPwEiyLGsvpEDaxF-oDO`~kueop( z+1S@-;qhF2Xtwg9D|ow#5VwgLO>odJrgbVmh>|44G+@>AVibqh^pSoaSyTsn)o_r1 z+#CXxD4|3qx7kMD| z6(krqLrU}TD;`kvRJjB6y1<@9_#*b6e>*m-WvUVjxCzzSB~R*;mELf#D_F`y5W!%s zZB`wKY3=-*m2%Y~KK?W-joFj}t@S86c-@sy%ufJ(x!o;8|I&Cr!Y)94z$%>VVz_>3 z;WB(EpnfIk5i5e>Jz)k5t!eE?s(9nYW*)=utMbSQ*MKy=^pnHe_hyfHeC{jloajsm zxs7i_e?6qOLmRrBV6zQ)r@iXXh|K};U@uRi6czx)siRa#eOZA-cKGmtE7lpl%s!dl zskH)oQ_xSii=)*L%dUBx>tRfrYZ17xCZldVSL!8U65Z^#+r^dbLF}U9kIS5?YrpsX z<@j&hdA^l2YY)bEZ18GA_{l=PZG356#dKtH^Y!wE4#;b{6Sre;qAl~CH9$zNBFAEK zW^*xM|9`O4B(iDDNi4~~_S7ST-dI6Q$yEt(iLE@4RQhBCLv9-FtqY5=^H$A();VXn zoKWtTkYah6qCM>*m^pe~Hg6@+jn1W77t0ikXRuqA&bqz7VmKvw_uxY1^wyT~el`4$ z!RU0@dRNM|G~RKUAz*3JP9PfscRmPuLzL{Mwf8}MIFY?5aNC3L}{sQe-D z(@P64WIDYXb;}y<(rtTI@2ygQ$5|4tto)^El^jy2O}J|^ARf@zPW7ERJ|h#lV;W;3 zi%)0Q=s_=At{Gx+uQkT21|#p>&b!Lwwa?#X1(ln>(p1Nb#(?X~b!g zT^rw9L?fN|)~NqerjsI_oA&)EOg@>NcWW~F&{)>+3?ZU;z&2ov#8I{%d(Wqy804x_ zniZb;`>J7VQM9uRyV3St8Bp&h; z0J|=cEj%a(Lsb-WAS-J;OY|22D!O^)1dUI))CE_ zKYT5AN7FIR)>L$lg*43UVQ5IbC*q0cmtUlCIXq^0w^t&@<6~5#7pbZPMH2V;*wKkw zJV$h3AtoKcvThnRNSHVf*>U?~S?0Hz>}4I{vbeagT~2O9#FL;!RR6X?ZM#3BZe(}b zxbFl0@3$=BsYLyeqje~6fx;ZQ&9dNE)ER#>SC$+DK(YL%?%(|5rnN4dZ-i2Btf~sh zAzJly9(U?d<=PWjp_;T|HZn)}tb>kl^2iKK7Pt@Q!~63~pdy()iRd0zyjbKQ|5utE zCi-hkiEzX-4`}?!;D3$P(V6(>P;zEAh_+sW`W}lk7vgHSlQR)<(Lh%{?8GYCIc{7H zmg^>j`zM{{iJ@QRqAwIb`)%=CFE)r$hGR_;*#`zMZqlms=Ja>|4DSUTXj_=7N3Adu z{c`~-QD%g#(0 zkpsC@V#CId#35vFGhCy4q&1JYh4L#$Dc)P2B(98vB^@AAChg&{$gFs*+Bc=CAj;>M z4$T+@8Vs`rl_z|Vo__aF)9!8+%|w|8aGfx+@?$2BmE0fg$*fY0?sG6J6%FaBjpIP*QO6sJxd-)E`(*!cLzJ^eP+D9 zsO?(3cOHBNV5EOhA><@*)WLR}iG-g>HQxnE8vp0)$qZlTi&{1En79n=)46(&N}8#` znia!dKVB&Hi@BWcu#`*5L%f*oyf_?z-NaK;U5+i`)e&kOp`8lJsWS(3=z)Y4{ cZqJV`>PPWP>lul^ZXO-FI307WvL__`5A_ufg8%>k diff --git a/docs/theme-store-light.png b/docs/theme-store-light.png deleted file mode 100644 index 132431a84ddada580a94ead82c40d0c1a869a1e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66107 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 JIgslQ{tI9FZA$ { const derived = await derivePassword(password, config.password_client_salt); - const result = await api.login(username.trim(), derived, turnstileToken, totpCode); - setToken(result.token); + await api.login(username.trim(), derived, turnstileToken, totpCode); setAccess("ok"); await reload(); }, [config.password_client_salt, reload]); @@ -241,11 +240,13 @@ export function AppProvider({ children }: { children: ReactNode }) { servers: serversRef.current, })), onConnectedChange: (connected) => { liveConnectedRef.current = connected; }, - onOverviewConnected: () => { + onWakeRequested: () => { const now = Date.now() / 1000; - const serverIds = serversRef.current - .filter((server) => server.timestamp && now - server.timestamp <= config.offline_threshold_seconds) - .map((server) => server.id); + const serverIds = selectedId + ? [selectedId] + : serversRef.current + .filter((server) => server.timestamp && now - server.timestamp <= config.offline_threshold_seconds) + .map((server) => server.id); return serverIds.length ? api.wakeServers(serverIds) : Promise.resolve(); }, }); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 24b3866..3db0010 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1,6 +1,5 @@ import type { AdminServer, AlertRule, AlertRuleInput, Bootstrap, Config, DatabaseMigrationResult, DatabaseStats, ExchangeRates, HistoryPoint, LatencySample, LatencyTask, LatencyTaskInput, LatencyTestPoint, LoginSession, RemoteTask, RemoteTaskCreated, RemoteTaskInput, Server, ServerInput, Settings, TelegramSettings, TelegramSettingsInput, Theme, ThemeSettingsSchema, TotpSetup, TotpStatus } from "./types"; -const TOKEN_KEY = "nodeflare-admin-token"; export const ADMIN_UNAUTHORIZED_EVENT = "nodeflare:admin-unauthorized"; export class ApiError extends Error { @@ -9,25 +8,13 @@ export class ApiError extends Error { } } -export function getToken() { - return localStorage.getItem(TOKEN_KEY) ?? ""; -} - -export function setToken(token: string) { - if (token) localStorage.setItem(TOKEN_KEY, token); - else localStorage.removeItem(TOKEN_KEY); -} - async function requestResponse(path: string, init: RequestInit = {}, admin = false): Promise { const headers = new Headers(init.headers); if (typeof init.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json"); - const token = getToken(); - if (token) headers.set("Authorization", `Bearer ${token}`); - const response = await fetch(path, { ...init, headers }); + const response = await fetch(path, { ...init, headers, credentials: "same-origin" }); if (!response.ok) { const payload = await response.json().catch(() => ({ error: response.statusText })); if (response.status === 401 && admin) { - setToken(""); window.dispatchEvent(new Event(ADMIN_UNAUTHORIZED_EVENT)); throw new ApiError("", response.status); } @@ -42,6 +29,18 @@ async function request(path: string, init: RequestInit = {}, admin = false): return response.json() as Promise; } +interface SensitiveProof { + totpCode?: string; + passwordDerived?: string; +} + +function sensitiveHeaders(proof: SensitiveProof): Headers { + const headers = new Headers(); + if (proof.totpCode) headers.set("X-NodeFlare-TOTP", proof.totpCode); + if (proof.passwordDerived) headers.set("X-NodeFlare-Password", proof.passwordDerived); + return headers; +} + export const api = { bootstrap: () => request("/api/bootstrap"), config: () => request("/api/config"), @@ -59,18 +58,22 @@ export const api = { request<{ tasks: LatencyTestPoint[]; points: LatencySample[] }>(`/api/latency/${encodeURIComponent(id)}?hours=${hours}`), verifyTurnstile: (token: string) => request("/api/turnstile/verify", { method: "POST", body: JSON.stringify({ token }) }), - login: (username: string, passwordDerived: string, turnstileToken: string, totpCode = "") => - request<{ token: string }>("/api/admin/login", { + login: async (username: string, passwordDerived: string, turnstileToken: string, totpCode = "") => { + await request<{ token: string }>("/api/admin/login", { method: "POST", body: JSON.stringify({ username, password_derived: passwordDerived, turnstile_token: turnstileToken, totp_code: totpCode }), - }), + }); + }, logout: () => request("/api/admin/logout", { method: "POST" }), loginSessions: () => request<{ sessions: LoginSession[] }>("/api/admin/sessions", {}, true), revokeLoginSession: (id: string) => request(`/api/admin/sessions/${encodeURIComponent(id)}`, { method: "DELETE" }, true), settings: () => request("/api/admin/settings", {}, true), twoFactorStatus: () => request("/api/admin/2fa/status", {}, true), - setupTwoFactor: () => request("/api/admin/2fa/setup", { method: "POST" }, true), + setupTwoFactor: (proof: SensitiveProof) => request("/api/admin/2fa/setup", { + method: "POST", + headers: sensitiveHeaders(proof), + }, true), enableTwoFactor: (totpCode: string) => request("/api/admin/2fa/enable", { method: "POST", body: JSON.stringify({ totp_code: totpCode }), @@ -112,7 +115,7 @@ export const api = { deleteTheme: (id: string) => request(`/api/admin/themes/${encodeURIComponent(id)}`, { method: "DELETE" }, true), saveSettings: (input: Partial) => - request<{ settings: Settings; token: string | null }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }, true), + request("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }, true), createServer: (input: ServerInput) => request<{ id: string; agent_token: string }>( "/api/admin/servers", @@ -150,9 +153,9 @@ export const api = { { method: "POST" }, true, ), - migrateDatabase: (databaseUrl: string) => request( + migrateDatabase: (databaseUrl: string, proof: SensitiveProof) => request( "/api/admin/database/migrate", - { method: "POST", body: JSON.stringify({ database_url: databaseUrl }) }, + { method: "POST", headers: sensitiveHeaders(proof), body: JSON.stringify({ database_url: databaseUrl }) }, true, ), restartAfterDatabaseMigration: () => request<{ restarting: boolean }>( @@ -160,15 +163,24 @@ export const api = { { method: "POST" }, true, ), - databaseBackup: async () => { - const response = await requestResponse("/api/admin/database/backup", {}, true); + databaseBackup: async (proof: SensitiveProof) => { + const response = await requestResponse("/api/admin/database/backup", { + headers: sensitiveHeaders(proof), + }, true); const disposition = response.headers.get("Content-Disposition") ?? ""; const filename = /filename="?([^";]+)"?/i.exec(disposition)?.[1] ?? "nodeflare-database-backup.zip"; return { blob: await response.blob(), filename }; }, - restoreDatabaseBackup: (file: File) => { + restoreDatabaseBackup: ( + file: File, + proof: SensitiveProof, + ) => { const query = new URLSearchParams({ filename: file.name }); - return request<{ restored_rows: number }>(`/api/admin/database/restore?${query}`, { method: "POST", body: file }, true); + return request<{ restored_rows: number }>(`/api/admin/database/restore?${query}`, { + method: "POST", + body: file, + headers: sensitiveHeaders(proof), + }, true); }, createRemoteTask: (input: RemoteTaskInput) => request<{ tasks: RemoteTaskCreated[] }>("/api/admin/remote/task", { method: "POST", diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx index 18b3f32..9b5ac36 100644 --- a/frontend/src/components/AdminPanel.tsx +++ b/frontend/src/components/AdminPanel.tsx @@ -36,7 +36,7 @@ import { ExternalLink, } from "lucide-react"; import { ChangeEvent, DragEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ADMIN_UNAUTHORIZED_EVENT, api, ApiError, getToken, setToken } from "../api"; +import { ADMIN_UNAUTHORIZED_EVENT, api, ApiError } from "../api"; import { adminTabFromPath, adminTabPaths, canonicalAdminPath, type AdminTab } from "../adminRoutes"; import { formatBytes, formatByteSize, isOnline, parseByteSize } from "../format"; import { derivePassword } from "../password"; @@ -229,14 +229,14 @@ async function copyText(value: string) { if (!copied) throw new Error("copy failed"); } -async function waitForDatabaseSwitch(targetKind: DatabaseStats["kind"], token: string) { +async function waitForDatabaseSwitch(targetKind: DatabaseStats["kind"]) { const deadline = Date.now() + 120_000; while (Date.now() < deadline) { await new Promise((resolve) => window.setTimeout(resolve, 750)); try { const response = await fetch(`/api/admin/database?restart=${Date.now()}`, { cache: "no-store", - headers: token ? { Authorization: `Bearer ${token}` } : undefined, + credentials: "same-origin", signal: AbortSignal.timeout(2_500), }); if (response.status === 401) return; @@ -260,7 +260,8 @@ export function AdminPanel({ onToggleTheme: () => void; onChanged: () => void; }) { - const [authenticated, setAuthenticated] = useState(!!getToken()); + const [authenticated, setAuthenticated] = useState(false); + const [authChecked, setAuthChecked] = useState(false); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [turnstileToken, setTurnstileToken] = useState(""); @@ -308,6 +309,8 @@ export function AdminPanel({ const [twoFactorStatus, setTwoFactorStatus] = useState(null); const [twoFactorSetup, setTwoFactorSetup] = useState(null); const [twoFactorCode, setTwoFactorCode] = useState(""); + const [securityVerification, setSecurityVerification] = useState(""); + const [databaseVerification, setDatabaseVerification] = useState(""); const [twoFactorSecretCopied, setTwoFactorSecretCopied] = useState(false); const [loginSessions, setLoginSessions] = useState([]); const [loginSessionsLoaded, setLoginSessionsLoaded] = useState(false); @@ -324,7 +327,6 @@ export function AdminPanel({ remoteTasksRef.current = remoteTasks; const load = useCallback(async () => { - if (!getToken()) return; setBusy(true); setError(""); try { @@ -343,13 +345,13 @@ export function AdminPanel({ setAuthenticated(true); } catch (reason) { if (reason instanceof ApiError && reason.status === 401) { - setToken(""); setAuthenticated(false); setError(""); return; } setError(reason instanceof Error ? reason.message : "加载失败"); } finally { + setAuthChecked(true); setBusy(false); } }, []); @@ -357,6 +359,7 @@ export function AdminPanel({ useEffect(() => { void load(); }, [load]); useEffect(() => { + if (!authChecked) return; const syncAdminPath = () => { const canonicalPath = canonicalAdminPath(window.location.pathname, authenticated); if (window.location.pathname !== canonicalPath) { @@ -370,7 +373,7 @@ export function AdminPanel({ const handlePopState = () => syncAdminPath(); window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); - }, [authenticated]); + }, [authChecked, authenticated]); useEffect(() => { if (!authenticated) return; @@ -382,6 +385,7 @@ export function AdminPanel({ useEffect(() => { const resetAuthentication = () => { setAuthenticated(false); + setAuthChecked(true); setError(""); setNotice(""); }; @@ -413,8 +417,7 @@ export function AdminPanel({ setError(""); try { const passwordDerived = await derivePassword(password, config.password_client_salt); - const result = await api.login(username.trim(), passwordDerived, turnstileToken, loginTotpCode); - setToken(result.token); + await api.login(username.trim(), passwordDerived, turnstileToken, loginTotpCode); setPassword(""); setTurnstileToken(""); setLoginTotpCode(""); @@ -582,6 +585,8 @@ export function AdminPanel({ const payload: Partial = { ...settings }; delete payload.admin_password_configured; delete payload.totp_login_enabled; + delete payload.current_password_derived; + delete payload.current_totp_code; try { if (payload.new_password) { payload.new_password_derived = await derivePassword(payload.new_password, config.password_client_salt); @@ -590,9 +595,22 @@ export function AdminPanel({ delete payload.new_password; delete payload.new_password_derived; } + if (tab === "security") { + const verification = securityVerification.trim(); + if (twoFactorStatus?.enabled) { + if (!/^\d{6}$/.test(verification)) throw new Error("请输入当前 6 位两步验证码"); + payload.current_totp_code = verification; + } else { + if (!verification) throw new Error("请输入当前管理员密码"); + payload.current_password_derived = await derivePassword( + verification, + config.password_client_salt, + ); + } + } const result = await api.saveSettings(payload); - if (result.token) setToken(result.token); - setSettings(result.settings); + setSettings(result); + setSecurityVerification(""); if (tab === "security") await loadLoginSessions(); setNotice("设置已保存"); onChanged(); @@ -612,6 +630,18 @@ export function AdminPanel({ finally { setBusy(false); } } + async function sensitiveProof(value: string, action: string) { + const verification = value.trim(); + if (twoFactorStatus?.enabled) { + if (!/^\d{6}$/.test(verification)) throw new Error(`${action}请输入当前 6 位两步验证码`); + return { totpCode: verification }; + } + if (!verification) throw new Error(`${action}请输入当前管理员密码`); + return { + passwordDerived: await derivePassword(verification, config.password_client_salt), + }; + } + async function reclaimDatabase() { if (!window.confirm("回收空间期间数据库会短暂不可用,确认继续?")) return; setBusy(true); setError(""); setNotice(""); @@ -631,8 +661,10 @@ export function AdminPanel({ if (!window.confirm(`将 ${source} 迁移到 ${target}?目标库中已有的 NodeFlare 数据会被覆盖。`)) return; setBusy(true); setError(""); setNotice(""); try { - const result = await api.migrateDatabase(databaseUrl); + const proof = await sensitiveProof(databaseVerification, "迁移数据库前,"); + const result = await api.migrateDatabase(databaseUrl, proof); setDatabaseMigrationUrl(""); + setDatabaseVerification(""); setDatabaseMigrationResult(result); setDatabase((current) => current ? { ...current, restart_required: result.restart_required } : current); const targetName = result.target_kind === "postgresql" ? "PostgreSQL" : "SQLite"; @@ -648,12 +680,10 @@ export function AdminPanel({ : databaseMigrationResult?.target_kind === "sqlite" ? "sqlite" : database.kind === "postgresql" ? "sqlite" : "postgresql"; - const token = getToken(); setBusy(true); setRestarting(true); setError(""); setNotice("NodeFlare 正在重启"); try { await api.restartAfterDatabaseMigration(); - await waitForDatabaseSwitch(targetKind, token); - setToken(""); + await waitForDatabaseSwitch(targetKind); window.location.replace("/admin/login"); } catch (reason) { setError(reason instanceof Error ? reason.message : "重启 NodeFlare 失败"); @@ -676,7 +706,9 @@ export function AdminPanel({ async function exportDatabaseBackup() { setBusy(true); setError(""); setNotice(""); try { - const { blob, filename } = await api.databaseBackup(); + const proof = await sensitiveProof(databaseVerification, "导出备份前,"); + const { blob, filename } = await api.databaseBackup(proof); + setDatabaseVerification(""); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; @@ -698,8 +730,9 @@ export function AdminPanel({ if (!window.confirm("恢复将覆盖当前数据库,并使现有登录会话失效。确认继续?")) return; setBusy(true); setError(""); setNotice(""); try { - const result = await api.restoreDatabaseBackup(file); - setToken(""); + const proof = await sensitiveProof(databaseVerification, "恢复备份前,"); + const result = await api.restoreDatabaseBackup(file, proof); + setDatabaseVerification(""); window.alert(`数据库已恢复 ${result.restored_rows.toLocaleString()} 行,请使用备份中的管理员账号重新登录。`); window.location.reload(); } catch (reason) { setError(reason instanceof Error ? reason.message : "恢复数据库备份失败"); } @@ -807,7 +840,7 @@ export function AdminPanel({ async function logout() { try { await api.logout(); } finally { - setToken(""); setAuthenticated(false); setServers([]); setSettings(null); setSelectedIds([]); + setAuthenticated(false); setServers([]); setSettings(null); setSelectedIds([]); setTwoFactorStatus(null); setTwoFactorSetup(null); setTwoFactorCode(""); setRemoteTotpCode(""); setTwoFactorSecretCopied(false); setLoginSessions([]); setLoginSessionsLoaded(false); setRevokingSessionId(""); @@ -818,7 +851,9 @@ export function AdminPanel({ async function setupTwoFactor() { setBusy(true); setError(""); setNotice(""); try { - const setup = await api.setupTwoFactor(); + const proof = await sensitiveProof(securityVerification, "生成两步验证密钥前,"); + const setup = await api.setupTwoFactor(proof); + setSecurityVerification(""); setTwoFactorSetup(setup); setTwoFactorStatus({ enabled: false, has_secret: true }); setTwoFactorCode(""); @@ -869,12 +904,12 @@ export function AdminPanel({ const installCommand = useMemo(() => { if (!install) return ""; const origin = window.location.origin; - const installer = "https://raw.githubusercontent.com/imengying/NodeFlare/main/agent"; + const installer = `${origin}/agent`; const mirror = install.agent_mirror.trim().replace(/\/+$/, ""); const shellMirror = mirror ? ` -m ${shellLiteral(mirror)}` : ""; const powershellMirror = mirror ? ` -Mirror ${powershellLiteral(mirror)}` : ""; if (installPlatform === "windows") { - return `Invoke-WebRequest -Uri "${installer}/install.ps1" -OutFile "$env:TEMP\\nodeflare-install.ps1"\n& "$env:TEMP\\nodeflare-install.ps1" -e ${powershellLiteral(origin)} -t ${powershellLiteral(install.agent_token)}${powershellMirror}`; + return `Invoke-WebRequest -UseBasicParsing -Uri "${installer}/install.ps1" -OutFile "$env:TEMP\\nodeflare-install.ps1"\nUnblock-File "$env:TEMP\\nodeflare-install.ps1"\n& "$env:TEMP\\nodeflare-install.ps1" -e ${powershellLiteral(origin)} -t ${powershellLiteral(install.agent_token)}${powershellMirror}`; } if (installPlatform === "macos") { return `curl -fsSL ${installer}/install-macos.sh | sudo sh -s -- -e ${shellLiteral(origin)} -t ${shellLiteral(install.agent_token)}${shellMirror}`; @@ -1047,6 +1082,10 @@ export function AdminPanel({ }; }, [authenticated, refreshRemoteTasks, remoteTasksActive, tab]); + if (!authChecked) { + return