mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-28 11:37:04 +08:00
* 修复: #46 Agent 一键安装脚本在 Debian dash 下执行失败 根因(多因素,任何一个都可能导致用户复现的 "sh: 2: Syntax error: newline unexpected"): - Debian/Ubuntu 默认 /bin/sh → dash;pipe 方式下 shebang 被忽略 - Content-Type: text/x-shellscript 会触发部分 CDN/反向代理的脚本识别与改写 - 如果响应被改写为 HTML,sh 在第 2 行(<html>)即报此语法错误 修复: 1. 前端命令改为 `curl -fsSL URL | sudo bash`(避开 dash) 2. 命令面板增加"先下载再执行"备用命令(代理过滤场景兜底) 3. install handler Content-Type 改为 text/plain;加 nosniff / no-store / Content-Disposition 三头,减少中间层改写的概率 4. 脚本模板加 magic marker `BACKUPX_AGENT_INSTALL_V1`,用户可通过 `head -3` 自查响应完整性;加 bash 自举段,文件执行时优先切到 bash 测试: - installscript/issue46_test.go 断言 magic + bash-bootstrap 存在于三种模式 - install_flow_test.go 断言新 headers 与 marker - go test ./... 全绿,前端 build 通过 * 修复: #46 用户截图证实 nginx SPA fallback 返回 index.html 用户反馈截图显示 curl 下载到的是 BackupX 前端 HTML,而非 shell 脚本—— 说明 /install/:token 未被反向代理转发到后端,nginx 按 try_files fallback 到 /index.html,sh 读第 2 行 <html> 报语法错误。 真正的根因修复: 1. 后端 install 端点额外暴露 /api/install/:token 别名,让反向代理 已有的 /api/ 转发规则自动接管 2. 节点创建时返回的 url/composeUrl 统一使用 /api/install/ 前缀 3. 更新 deploy/nginx.conf 模板: - 新增 location /install/ 转发(兼容旧版本生成的命令) - 新增 /health /ready /metrics 单独转发,避免 SPA fallback 测试: - install_flow_test.go 新增 TestInstallScriptAliasUnderAPI 断言 /api/install/:token 路径可用 + 新生成的 url 用 /api/install/ 前缀
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -171,6 +172,22 @@ func TestOneClickInstallFlow(t *testing.T) {
|
||||
if !strings.Contains(scriptRec.Body.String(), "systemctl enable --now backupx-agent") {
|
||||
t.Fatalf("script missing systemctl enable:\n%s", scriptRec.Body.String())
|
||||
}
|
||||
// Issue #46 防嗅探 headers:text/plain + nosniff + no-store + Content-Disposition
|
||||
if ct := scriptRec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
|
||||
t.Errorf("script Content-Type should be text/plain*, got %q", ct)
|
||||
}
|
||||
if nosniff := scriptRec.Header().Get("X-Content-Type-Options"); nosniff != "nosniff" {
|
||||
t.Errorf("missing X-Content-Type-Options: nosniff (got %q)", nosniff)
|
||||
}
|
||||
if cc := scriptRec.Header().Get("Cache-Control"); !strings.Contains(cc, "no-store") {
|
||||
t.Errorf("missing Cache-Control: no-store (got %q)", cc)
|
||||
}
|
||||
if cd := scriptRec.Header().Get("Content-Disposition"); !strings.Contains(cd, "backupx-agent-install.sh") {
|
||||
t.Errorf("Content-Disposition should name the script file (got %q)", cd)
|
||||
}
|
||||
if !strings.Contains(scriptRec.Body.String(), "BACKUPX_AGENT_INSTALL_V1") {
|
||||
t.Errorf("script missing magic marker BACKUPX_AGENT_INSTALL_V1")
|
||||
}
|
||||
|
||||
// 4. 再次消费应 410
|
||||
scriptReq2 := httptest.NewRequest(http.MethodGet, "/install/"+genResp.Data.InstallToken, nil)
|
||||
@@ -181,6 +198,73 @@ func TestOneClickInstallFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallScriptAliasUnderAPI 验证 /api/install/:token 别名路径可用,
|
||||
// 这是 Issue #46 的根本修复:让 install 端点自动命中反向代理的 /api/ 转发规则,
|
||||
// 避免 nginx SPA fallback 把请求当前端路由返回 index.html。
|
||||
func TestInstallScriptAliasUnderAPI(t *testing.T) {
|
||||
router, token := setupInstallFlowRouter(t)
|
||||
|
||||
// 1. 创建一个节点,生成 install token
|
||||
batchBody, _ := json.Marshal(map[string][]string{"names": {"alias-node"}})
|
||||
batchReq := httptest.NewRequest(http.MethodPost, "/api/nodes/batch", bytes.NewReader(batchBody))
|
||||
batchReq.Header.Set("Content-Type", "application/json")
|
||||
batchReq.Header.Set("Authorization", "Bearer "+token)
|
||||
batchRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(batchRec, batchReq)
|
||||
if batchRec.Code != 200 {
|
||||
t.Fatalf("batch create failed: %d %s", batchRec.Code, batchRec.Body.String())
|
||||
}
|
||||
var batchResp struct {
|
||||
Data []struct {
|
||||
ID uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(batchRec.Body.Bytes(), &batchResp)
|
||||
if len(batchResp.Data) == 0 {
|
||||
t.Fatalf("batch create returned no nodes: %s", batchRec.Body.String())
|
||||
}
|
||||
nodeID := batchResp.Data[0].ID
|
||||
|
||||
genBody, _ := json.Marshal(map[string]any{
|
||||
"mode": "systemd", "arch": "auto", "agentVersion": "v1.7.0", "downloadSrc": "github", "ttlSeconds": 600,
|
||||
})
|
||||
genReq := httptest.NewRequest(http.MethodPost,
|
||||
"/api/nodes/"+strconv.FormatUint(uint64(nodeID), 10)+"/install-tokens", bytes.NewReader(genBody))
|
||||
genReq.Header.Set("Content-Type", "application/json")
|
||||
genReq.Header.Set("Authorization", "Bearer "+token)
|
||||
genRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(genRec, genReq)
|
||||
if genRec.Code != 200 {
|
||||
t.Fatalf("gen install token failed: %d %s", genRec.Code, genRec.Body.String())
|
||||
}
|
||||
var genResp struct {
|
||||
Data struct {
|
||||
InstallToken string `json:"installToken"`
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(genRec.Body.Bytes(), &genResp)
|
||||
|
||||
// 2. 新生成的 url 应指向 /api/install/... —— 让反向代理的 /api/ 转发规则自动接管
|
||||
if !strings.Contains(genResp.Data.URL, "/api/install/") {
|
||||
t.Errorf("new install URL should use /api/install/ prefix, got %s", genResp.Data.URL)
|
||||
}
|
||||
|
||||
// 3. /api/install/:token 必须可消费(与 /install/:token 等价)
|
||||
aliasReq := httptest.NewRequest(http.MethodGet, "/api/install/"+genResp.Data.InstallToken, nil)
|
||||
aliasRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(aliasRec, aliasReq)
|
||||
if aliasRec.Code != 200 {
|
||||
t.Fatalf("/api/install alias failed: %d %s", aliasRec.Code, aliasRec.Body.String())
|
||||
}
|
||||
if !strings.Contains(aliasRec.Body.String(), "systemctl enable --now backupx-agent") {
|
||||
t.Errorf("alias should return rendered script, got:\n%s", aliasRec.Body.String())
|
||||
}
|
||||
if ct := aliasRec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
|
||||
t.Errorf("alias Content-Type should be text/plain*, got %q", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallTokenRateLimit(t *testing.T) {
|
||||
router, jwt := setupInstallFlowRouter(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user