From d0be2fdcf186187469f0ee4f2158021f901ef07e Mon Sep 17 00:00:00 2001 From: Nexus Agent Date: Sun, 7 Jun 2026 17:34:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20SSH=20secrets=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E3=80=81Docker=20=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E4=B8=8E=E5=A4=96=E7=BD=91=E6=8E=A2=E6=B5=8B=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使 deploy-production.sh 可读本机 secrets 并在非 docker 组用户下用 sudo 识别 1Panel 栈;修复 security_probe_external.sh 的 curl shift 误用与 WebSocket 自动 venv。 --- deploy/deploy-production.sh | 24 +++++- deploy/gate_log.jsonl | 40 ++++++++++ deploy/nexus-1panel.secrets.sh.example | 4 + .../audit/2026-06-07-code-review-followups.md | 14 +++- .../2026-06-07-deploy-ssh-secrets.md | 29 +++++++ .../2026-06-07-probe-curl-parse-fix.md | 35 +++++++++ docs/project/linux-dev-paths.md | 12 ++- .../2026-06-07-bl07-deploy-verification.md | 53 +++++++++++++ scripts/security_probe_external.sh | 78 ++++++++++++++----- 9 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 docs/changelog/2026-06-07-deploy-ssh-secrets.md create mode 100644 docs/changelog/2026-06-07-probe-curl-parse-fix.md create mode 100644 docs/reports/2026-06-07-bl07-deploy-verification.md diff --git a/deploy/deploy-production.sh b/deploy/deploy-production.sh index c1aa8ea5..a835a0af 100644 --- a/deploy/deploy-production.sh +++ b/deploy/deploy-production.sh @@ -17,6 +17,12 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${ROOT}" +SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh" +if [[ -f "$SECRETS" ]]; then + # shellcheck disable=SC1090 + source "$SECRETS" +fi + NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}" SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15) if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then @@ -36,22 +42,34 @@ fi REMOTE_DETECT=$(ssh_cmd 'bash -s' <<'REMOTE' set -euo pipefail +docker_cmd() { + if docker "$@" 2>/dev/null; then return 0; fi + sudo docker "$@" 2>/dev/null +} +compose_cmd() { + if docker compose "$@" 2>/dev/null; then return 0; fi + sudo docker compose "$@" 2>/dev/null +} for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] || continue echo "ROOT=$d" - if [[ -f "$d/docker/.env.prod" ]] && docker compose version >/dev/null 2>&1; then + if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then prof="$d/docker/.host-profile" [[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env" if [[ -f "$prof" ]]; then args=(-f "$d/docker/docker-compose.prod.yml") - docker network inspect 1panel-network >/dev/null 2>&1 && \ + docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \ [[ -f "$d/docker/docker-compose.1panel.yml" ]] && \ args+=(-f "$d/docker/docker-compose.1panel.yml") - q=$(cd "$d" && docker compose "${args[@]}" \ + q=$(cd "$d" && compose_cmd "${args[@]}" \ --env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true) [[ -n "$q" ]] && echo RUNTIME=docker && exit 0 fi fi + if [[ -f "$d/deploy/nexus-1panel.sh" ]] && command -v nx >/dev/null 2>&1; then + q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true) + [[ -n "$q" ]] && echo RUNTIME=docker && exit 0 + fi if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then echo RUNTIME=supervisor exit 0 diff --git a/deploy/gate_log.jsonl b/deploy/gate_log.jsonl index 2aecbfe9..8ed617bc 100644 --- a/deploy/gate_log.jsonl +++ b/deploy/gate_log.jsonl @@ -226,3 +226,43 @@ {"ts":"2026-06-07T12:02:49+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} {"ts":"2026-06-07T12:02:49+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"} {"ts":"2026-06-07T12:02:49+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-code-review-followups.md 42lines"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-code-review-followups.md"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-07T12:32:11+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-deploy-ssh-secrets.md 29lines"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-code-review-followups.md"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"review","result":"BLOCK","detail":"audit missing changed files"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-07T12:35:38+08:00","date":"2026-06-07","gate":"summary","result":"BLOCK","detail":"6/7"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-deploy-ssh-secrets.md 29lines"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-code-review-followups.md"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-07T12:35:53+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-probe-curl-parse-fix.md 35lines"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-code-review-followups.md"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-07T17:25:18+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"} diff --git a/deploy/nexus-1panel.secrets.sh.example b/deploy/nexus-1panel.secrets.sh.example index 6aa23392..1d907c86 100644 --- a/deploy/nexus-1panel.secrets.sh.example +++ b/deploy/nexus-1panel.secrets.sh.example @@ -15,3 +15,7 @@ NEXUS_DOMAIN='api.synaglobal.vip' NEXUS_ROOT='/opt/nexus' NEXUS_PROFILE='2c8g' NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env' + +# 本机生产 SSH(deploy-production.sh 读取;私钥路径勿提交真实 .pem) +# NEXUS_SSH='azureuser@20.24.218.235' +# NEXUS_SSH_KEY='/path/to/nz-admin.pem' diff --git a/docs/audit/2026-06-07-code-review-followups.md b/docs/audit/2026-06-07-code-review-followups.md index 1f419820..99221b10 100644 --- a/docs/audit/2026-06-07-code-review-followups.md +++ b/docs/audit/2026-06-07-code-review-followups.md @@ -14,12 +14,18 @@ | `web/agent/agent.py` | 心跳 401 退出循环(对齐 BL-06 中心端) | | `server/api/install.py` | 路由级 lock Depends;归档先于写 lock;归档失败 fail-closed | | `.cursor/rules/nexus-security.mdc` | Agent 认证矩阵:仅 per-server key | -| `scripts/security_probe_external.sh` | WS 无 token HTTP 403 → PASS | +| `scripts/security_probe_external.sh` | curl `shift` 修复;`resolve_ws_python`;WS 403 → PASS | +| `docs/changelog/2026-06-07-deploy-ssh-secrets.md` | 本机 SSH secrets 文档 | +| `docs/changelog/2026-06-07-probe-curl-parse-fix.md` | 探测脚本 curl/WS 修复 | +| `docs/reports/2026-06-07-bl07-deploy-verification.md` | 生产部署验证报告 | | `tests/test_install_locked_404.py` | create-admin / test-credentials 锁定 404;router Depends 断言 | | `tests/test_security_unit.py` | `_reject_if_locked` 期望 404 | | `deploy/install-nx-cli.sh` | CRLF → LF(门控 pre-flight) | | `deploy/update.sh` | CRLF → LF | | `deploy/gate_log.jsonl` | 门控运行记录(自动生成) | +| `deploy/deploy-production.sh` | source `nexus-1panel.secrets.sh` 加载 SSH | +| `deploy/nexus-1panel.secrets.sh.example` | SSH 变量占位 | +| `docs/project/linux-dev-paths.md` | 本机 SSH/部署 SSOT | ## Step 3 规则扫描 @@ -30,6 +36,8 @@ | H3 | 归档失败仍 lock 成功 | FIXED 归档先于 lock;OSError → RuntimeError → 500 | | H4 | 安全 SSOT 与 BL-06 漂移 | FIXED `nexus-security.mdc` | | H5 | 外网 WS 403 误判反代 | FIXED 探测脚本 PASS + 报告澄清 | +| H6 | 探测 curl `shift 3` 误报 FAIL | FIXED `probe()` shift 2/3 + 独立 code 文件 | +| H7 | WS 探测 SKIP(无 websockets) | FIXED `resolve_ws_python()` 自动 venv | ## Closure @@ -39,7 +47,9 @@ | H2 | FIXED | `install.py` `APIRouter(..., dependencies=[Depends(...)])` | | H3 | FIXED | `_finalize_install_lock` 先 `_archive_install_wizard_html` | | H4 | FIXED | `nexus-security.mdc` 认证矩阵 | -| H5 | FIXED | `security_probe_external.sh` L167-168 | +| H5 | FIXED | `security_probe_external.sh` WS 403 PASS | +| H6 | FIXED | `probe()` `shift 2` + `code_file` | +| H7 | FIXED | `resolve_ws_python()` | ## 验证 diff --git a/docs/changelog/2026-06-07-deploy-ssh-secrets.md b/docs/changelog/2026-06-07-deploy-ssh-secrets.md new file mode 100644 index 00000000..d9ccecf2 --- /dev/null +++ b/docs/changelog/2026-06-07-deploy-ssh-secrets.md @@ -0,0 +1,29 @@ +# Changelog — 生产 SSH 凭据纳入 secrets + deploy 自动加载 + +**日期**:2026-06-07 + +## 摘要 + +本机生产 SSH 私钥路径写入 `deploy/nexus-1panel.secrets.sh`(gitignore);`deploy-production.sh` 启动时自动 source,与 `git-push.sh` 同一凭据文件。 + +## 动机 + +本机 `Host nexus` 未指向生产 VPS;部署脚本需 `NEXUS_SSH` / `NEXUS_SSH_KEY`,避免每次手动 export。 + +## 涉及文件 + +- `deploy/deploy-production.sh` +- `deploy/nexus-1panel.secrets.sh.example` +- `docs/project/linux-dev-paths.md` +- `deploy/nexus-1panel.secrets.sh`(本地 only,不入 git) + +## 迁移 / 重启 + +无。 + +## 验证 + +```bash +source deploy/nexus-1panel.secrets.sh +ssh -i "$NEXUS_SSH_KEY" "$NEXUS_SSH" "echo SSH_OK" +``` diff --git a/docs/changelog/2026-06-07-probe-curl-parse-fix.md b/docs/changelog/2026-06-07-probe-curl-parse-fix.md new file mode 100644 index 00000000..261341f8 --- /dev/null +++ b/docs/changelog/2026-06-07-probe-curl-parse-fix.md @@ -0,0 +1,35 @@ +# Changelog — 外网探测脚本 curl / WebSocket 修复 + +**日期**:2026-06-07 + +## 摘要 + +修复 `security_probe_external.sh` 中 `probe()` 对 `shift 3` 的误用,消除 `code=000000ok200` 类误报 `[FAIL]`;WebSocket 段自动解析项目 `venv` 或引导临时 venv 安装 `websockets`。 + +## 动机 + +`probe GET /path` 仅 2 个参数时,bash `shift 3` 不移位,`GET` 与 `/health` 被当作 curl 额外 URL,stdout 混入 `000` 错误与响应体,判定逻辑全部失真。 + +## 涉及文件 + +- `scripts/security_probe_external.sh` + +## 变更 + +- GET:`shift 2`;非 GET:`shift 2` + `shift 1`(消费 body),剩余 `$@` 为额外 curl 参数 +- HTTP 状态码写入独立临时文件,避免与 body 混淆 +- 统一 `-X "$method"` +- `script-callback` 探测 body 满足 Pydantic 最小长度,确保测到 401 而非 422 +- 新增 `resolve_ws_python()`:`NEXUS_PROBE_PYTHON` → `venv/` → 系统 python → `/tmp/nexus-probe-venv` + `websockets==14.1` + +## 迁移 / 重启 + +无。 + +## 验证 + +```bash +NEXUS_PROBE_BASE=https://api.synaglobal.vip bash scripts/security_probe_external.sh +# GET /health → [PASS];install POST → [PASS] 404;无 code=000000... 误报 +# WebSocket → [PASS] HTTP 403(不再 [SKIP]) +``` diff --git a/docs/project/linux-dev-paths.md b/docs/project/linux-dev-paths.md index 8af90528..7d06befb 100644 --- a/docs/project/linux-dev-paths.md +++ b/docs/project/linux-dev-paths.md @@ -20,12 +20,16 @@ | 用途 | Linux | |------|--------| -| SSH 配置 | `~/.ssh/config` 中 `Host nexus` | -| 私钥 | `~/.ssh/id_rsa_nexus` | -| 生产目录 | `/www/wwwroot/api.synaglobal.vip/` | +| SSH 目标 | `azureuser@20.24.218.235`(生产 VPS) | +| 私钥 | 本机路径写在 `deploy/nexus-1panel.secrets.sh` 的 `NEXUS_SSH_KEY`(**不入 git**) | +| 一键部署 | `bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh`(自动 source secrets) | +| 生产目录 | `/opt/nexus`(Docker)或 `/www/wwwroot/api.synaglobal.vip/`(Supervisor) | ```bash -ssh nexus "cd /www/wwwroot/api.synaglobal.vip && git pull && supervisorctl restart nexus" +# secrets 已配置时无需 export;否则: +# export NEXUS_SSH=azureuser@20.24.218.235 +# export NEXUS_SSH_KEY='/path/to/nz-admin.pem' +bash deploy/deploy-production.sh ``` ## Cursor MCP diff --git a/docs/reports/2026-06-07-bl07-deploy-verification.md b/docs/reports/2026-06-07-bl07-deploy-verification.md new file mode 100644 index 00000000..83981eda --- /dev/null +++ b/docs/reports/2026-06-07-bl07-deploy-verification.md @@ -0,0 +1,53 @@ +# BL-07 / code-review 跟进 — 生产部署验证(2026-06-07) + +## Git + +| 项 | 值 | +|----|-----| +| Commit | `72d82d7` — `fix(security): BL-07 WS 403 澄清与 code-review 跟进加固` | +| 上一版 | `e5e2582` | +| 远程 | Gitea `main` 已同步 | + +## 部署 + +| 项 | 状态 | +|----|------| +| 触发 | 菜单 **1** → `deploy-production.sh` 运行时检测为 `unknown`(azureuser 无 docker 组) | +| 实际执行 | `ssh azureuser@20.24.218.235` → `sudo bash /opt/nexus/deploy/nexus-1panel.sh upgrade` | +| MySQL 备份 | `/var/backups/nexus/nexus-before-upgrade-20260607-043622.sql.gz` | +| 容器 | `nexus-prod-nexus-1` 重建 **healthy** | +| `/health` | `ok` | +| `/app/` | HTTP 200 | +| install.html | 已归档 → HTTP 404 | + +## 外网探测(部署后) + +`NEXUS_PROBE_BASE=https://api.synaglobal.vip` + +| 探测项 | HTTP | 判定 | +|--------|------|------| +| `GET /health` | 200 | ✅ | +| `GET /health/detail` | 401 | ✅ BL-02 | +| `GET /openapi.json` `/docs` `/redoc` | 404 | ✅ BL-04 | +| `GET /api/install/*` | 404 | ✅ BL-05 | +| `POST /api/install/init-db` | **404** | ✅ 路由级 lock(此前为 422) | +| `POST /api/install/create-admin` 等 | 404 | ✅ | +| `POST /api/settings/bing-wallpapers/sync` | 401 | ✅ BL-03 | +| Agent 无 key / 假 key | 422 / 401 | ✅ BL-06 | +| `wss://.../ws/alerts` 无 token | **403** | ✅ BL-07 | +| `wss://.../ws/sync` 无 token | **403** | ✅ BL-07 | + +**备注**:2026-06-07 已修复 `security_probe_external.sh` 的 `shift 3` 误用;重跑应全 `[PASS]`(WS 需本机 `websockets` 包)。 + +## 本地门控(推送前) + +```bash +bash deploy/pre_deploy_check.sh # 7/7 PASS +bash scripts/local_verify.sh # 全绿 +``` + +## 后续 + +- `deploy/deploy-production.sh` 已补 `sudo docker` 回退检测,下次菜单 **1** 应可自动识别 Docker。 +- 边端 Agent 需从 `/agent/agent.py` 拉取新版本(401 停循环);子机不自动升级。 +- 可选:修 probe 脚本解析、commit deploy-ssh 相关文档(不含 secrets)。 diff --git a/scripts/security_probe_external.sh b/scripts/security_probe_external.sh index 621c4fa8..08155d39 100644 --- a/scripts/security_probe_external.sh +++ b/scripts/security_probe_external.sh @@ -3,8 +3,10 @@ # Usage: NEXUS_PROBE_BASE=https://api.synaglobal.vip bash scripts/security_probe_external.sh set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BASE="${NEXUS_PROBE_BASE:-https://api.synaglobal.vip}" REPORT_DIR="${NEXUS_PROBE_REPORT_DIR:-/tmp/nexus-external-probe}" +PROBE_VENV="${NEXUS_PROBE_VENV_DIR:-/tmp/nexus-probe-venv}" mkdir -p "$REPORT_DIR" OUT="$REPORT_DIR/probe-$(date -u +%Y%m%dT%H%M%SZ).txt" @@ -15,24 +17,65 @@ fail() { log " [FAIL] $*"; } info() { log " [INFO] $*"; } warn() { log " [WARN] $*"; } +# Resolve a Python with `websockets` for BL-07 probes (project venv → system → bootstrap venv). +resolve_ws_python() { + local py cand + if [[ -n "${NEXUS_PROBE_PYTHON:-}" && -x "${NEXUS_PROBE_PYTHON}" ]]; then + if "${NEXUS_PROBE_PYTHON}" -c 'import websockets' 2>/dev/null; then + echo "${NEXUS_PROBE_PYTHON}" + return 0 + fi + fi + for cand in "${ROOT}/venv/bin/python" "${ROOT}/.venv/bin/python"; do + [[ -x "$cand" ]] || continue + if "$cand" -c 'import websockets' 2>/dev/null; then + echo "$cand" + return 0 + fi + done + if command -v python3 >/dev/null 2>&1; then + if python3 -c 'import websockets' 2>/dev/null; then + echo python3 + return 0 + fi + fi + if ! command -v python3 >/dev/null 2>&1; then + return 1 + fi + if [[ ! -x "${PROBE_VENV}/bin/python" ]]; then + python3 -m venv "${PROBE_VENV}" 2>>"$OUT" || return 1 + fi + if ! "${PROBE_VENV}/bin/python" -c 'import websockets' 2>/dev/null; then + "${PROBE_VENV}/bin/pip" install -q "websockets==14.1" 2>>"$OUT" || return 1 + fi + echo "${PROBE_VENV}/bin/python" +} + # probe METHOD PATH [JSON_BODY] [extra curl args...] # Prints HTTP status code to stdout only. probe() { local method="$1" path="$2" body="${3:-}" - shift 3 || true + # shift 3 is wrong for GET (only 2 args): bash leaves $@ untouched when n > $#. + shift 2 + if [[ "$method" != "GET" ]]; then + shift 1 + fi local url="${BASE}${path}" - local tmp + local tmp code_file tmp="$(mktemp)" - local -a curl_args=(-sS -o "$tmp" -w "%{http_code}" --max-time 20) - if [[ "$method" == "GET" ]]; then - : - else - curl_args+=(-X "$method" -H "Content-Type: application/json") + code_file="$(mktemp)" + local -a curl_args=(-sS -o "$tmp" -w "%{http_code}" --max-time 20 -X "$method") + if [[ "$method" != "GET" ]]; then + curl_args+=(-H "Content-Type: application/json") [[ -n "$body" ]] && curl_args+=(-d "$body") fi - # shellcheck disable=SC2206 local code - code=$(curl "${curl_args[@]}" "$@" "$url" 2>>"$OUT" || echo "000") + if ! curl "${curl_args[@]}" "$@" "$url" >"$code_file" 2>>"$OUT"; then + code="000" + else + code="$(tr -d '\r\n' <"$code_file")" + fi + rm -f "$code_file" local snippet snippet="$(head -c 400 "$tmp" | tr '\n' ' ')" plog "" @@ -116,7 +159,7 @@ c=$(probe POST /api/agent/heartbeat '{"server_id":1,"is_online":true}' -H "X-API [[ "$c" == "401" || "$c" == "422" ]] && pass "POST /api/agent/heartbeat bad key rejected ($c)" || fail "code=$c" c=$(probe POST /api/agent/script-callback \ - '{"job_id":"probe","server_id":1,"secret":"x","exit_code":0}' \ + '{"job_id":"probe0001","server_id":1,"secret":"invalid-probe-secret","exit_code":0}' \ -H "X-API-Key: invalid-probe-key-000") [[ "$c" == "401" || "$c" == "404" || "$c" == "429" ]] && pass "POST script-callback bad key ($c)" || fail "code=$c" @@ -136,16 +179,13 @@ c=$(probe GET /agent/install.sh) [[ "$c" == "200" ]] && info "GET /agent/install.sh public (expected)" || info "code=$c" # ── WebSocket ── -if command -v python3 >/dev/null 2>&1; then +WS_PY="$(resolve_ws_python || true)" +if [[ -n "${WS_PY}" ]]; then log "" - log "── WebSocket probes" - python3 - "$BASE" 2>>"$OUT" <<'PY' | tee -a "$OUT" || true + log "── WebSocket probes (python=${WS_PY})" + "${WS_PY}" - "$BASE" 2>>"$OUT" <<'PY' | tee -a "$OUT" || true import asyncio, sys -try: - import websockets -except ImportError: - print(" [SKIP] websockets not installed") - sys.exit(0) +import websockets base = sys.argv[1].rstrip("/") ws_base = base.replace("https://", "wss://").replace("http://", "ws://") @@ -177,6 +217,8 @@ async def main(): asyncio.run(main()) PY +else + warn "WebSocket probes skipped: no python with websockets (set NEXUS_PROBE_PYTHON or install venv)" fi log ""