fix: make agent install command proxy independent (#50)

This commit is contained in:
Wu Qing
2026-04-25 13:43:30 +08:00
committed by GitHub
parent bc8742977e
commit 67a42b09ba
13 changed files with 232 additions and 35 deletions

View File

@@ -3,6 +3,7 @@ package http
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -153,6 +154,8 @@ func TestOneClickInstallFlow(t *testing.T) {
Data struct {
InstallToken string `json:"installToken"`
URL string `json:"url"`
FallbackURL string `json:"fallbackUrl"`
ScriptBase64 string `json:"scriptBase64"`
} `json:"data"`
}
if err := json.Unmarshal(genRec.Body.Bytes(), &genResp); err != nil {
@@ -161,6 +164,16 @@ func TestOneClickInstallFlow(t *testing.T) {
if genResp.Data.InstallToken == "" {
t.Fatalf("missing installToken")
}
if !strings.Contains(genResp.Data.FallbackURL, "/install/") {
t.Fatalf("missing fallback install URL, got %q", genResp.Data.FallbackURL)
}
decodedScript, err := base64.StdEncoding.DecodeString(genResp.Data.ScriptBase64)
if err != nil {
t.Fatalf("scriptBase64 should be valid base64: %v", err)
}
if !strings.Contains(string(decodedScript), "BACKUPX_AGENT_INSTALL_V1") {
t.Fatalf("scriptBase64 should contain rendered install script")
}
// 3. 公开端点消费
scriptReq := httptest.NewRequest(http.MethodGet, "/install/"+genResp.Data.InstallToken, nil)
@@ -241,6 +254,8 @@ func TestInstallScriptAliasUnderAPI(t *testing.T) {
Data struct {
InstallToken string `json:"installToken"`
URL string `json:"url"`
FallbackURL string `json:"fallbackUrl"`
ScriptBase64 string `json:"scriptBase64"`
} `json:"data"`
}
_ = json.Unmarshal(genRec.Body.Bytes(), &genResp)
@@ -249,6 +264,12 @@ func TestInstallScriptAliasUnderAPI(t *testing.T) {
if !strings.Contains(genResp.Data.URL, "/api/install/") {
t.Errorf("new install URL should use /api/install/ prefix, got %s", genResp.Data.URL)
}
if !strings.Contains(genResp.Data.FallbackURL, "/install/") {
t.Errorf("fallback install URL should use /install/ prefix, got %s", genResp.Data.FallbackURL)
}
if genResp.Data.ScriptBase64 == "" {
t.Errorf("new install response should include scriptBase64 for proxy-independent commands")
}
// 3. /api/install/:token 必须可消费(与 /install/:token 等价)
aliasReq := httptest.NewRequest(http.MethodGet, "/api/install/"+genResp.Data.InstallToken, nil)

View File

@@ -59,16 +59,7 @@ func (h *InstallHandler) Script(c *gin.Context) {
return
}
h.recordConsumeAudit(c, consumed, "script")
script, err := installscript.RenderScript(installscript.Context{
MasterURL: resolveMasterURL(c, h.externalURL),
AgentToken: consumed.Node.Token,
AgentVersion: consumed.Record.AgentVer,
Mode: consumed.Record.Mode,
Arch: consumed.Record.Arch,
DownloadBase: installscript.DownloadBaseFor(consumed.Record.DownloadSrc),
InstallPrefix: "/opt/backupx-agent",
NodeID: consumed.Node.ID,
})
script, err := renderInstallScript(resolveMasterURL(c, h.externalURL), consumed.Node, consumed.Record)
if err != nil {
c.String(stdhttp.StatusInternalServerError, "render error\n")
return
@@ -141,6 +132,19 @@ func (h *InstallHandler) recordConsumeAudit(c *gin.Context, consumed *service.Co
})
}
func renderInstallScript(masterURL string, node *model.Node, record *model.AgentInstallToken) (string, error) {
return installscript.RenderScript(installscript.Context{
MasterURL: masterURL,
AgentToken: node.Token,
AgentVersion: record.AgentVer,
Mode: record.Mode,
Arch: record.Arch,
DownloadBase: installscript.DownloadBaseFor(record.DownloadSrc),
InstallPrefix: "/opt/backupx-agent",
NodeID: node.ID,
})
}
// resolveMasterURL 按优先级推导 Master URL外部配置 > X-Forwarded-* > Request.Host。
// 此为包级 helper供 install_handler 和 node_handler 共用。
func resolveMasterURL(c *gin.Context, externalURL string) string {

View File

@@ -1,6 +1,7 @@
package http
import (
"encoding/base64"
"fmt"
stdhttp "net/http"
"strconv"
@@ -262,16 +263,28 @@ func (h *NodeHandler) CreateInstallToken(c *gin.Context) {
fmt.Sprintf("生成 %s/%s install token TTL=%ds", input.Mode, input.Arch, input.TTLSeconds))
masterURL := resolveMasterURL(c, h.externalURL)
script, err := renderInstallScript(masterURL, out.Node, out.Record)
if err != nil {
response.Error(c, err)
return
}
// 使用 /api/install/... 而非 /install/... —— 让反向代理的 /api/ 转发规则
// 自动接管,避免 SPA fallback 把请求当成前端路由返回 index.htmlissue #46
// 同时返回 /install/... 备用地址,兼容会剥离 /api 前缀的外层反向代理。
// scriptBase64 让前端可以生成不依赖公开下载路径的嵌入式命令,解决 Lucky 等代理
// 把 /api/install/* 也 fallback 到 index.html 的场景。
body := gin.H{
"installToken": out.Token,
"expiresAt": out.ExpiresAt,
"url": masterURL + "/api/install/" + out.Token,
"composeUrl": "",
"installToken": out.Token,
"expiresAt": out.ExpiresAt,
"url": masterURL + "/api/install/" + out.Token,
"fallbackUrl": masterURL + "/install/" + out.Token,
"scriptBase64": base64.StdEncoding.EncodeToString([]byte(script)),
"composeUrl": "",
"fallbackComposeUrl": "",
}
if input.Mode == "docker" {
body["composeUrl"] = masterURL + "/api/install/" + out.Token + "/compose.yml"
body["fallbackComposeUrl"] = masterURL + "/install/" + out.Token + "/compose.yml"
}
response.Success(c, body)
}

View File

@@ -36,3 +36,20 @@ func TestRenderScriptBashBootstrap(t *testing.T) {
t.Errorf("script missing exec bash fallback:\n%s", got)
}
}
func TestRenderScriptCreatesBackupXUserAndGroup(t *testing.T) {
got, err := RenderScript(testCtx)
if err != nil {
t.Fatalf("render err: %v", err)
}
for _, want := range []string{
"getent group backupx",
"groupadd --system backupx",
"useradd --system --gid backupx",
"Group=backupx",
} {
if !strings.Contains(got, want) {
t.Errorf("script missing %q:\n%s", want, got)
}
}
}

View File

@@ -49,7 +49,27 @@ tar xzf "$TMPDIR/pkg.tar.gz" -C "$TMPDIR"
# 4. 安装二进制 + 用户
echo "[2/4] 安装到 ${INSTALL_PREFIX}"
id backupx >/dev/null 2>&1 || useradd --system --home-dir "$INSTALL_PREFIX" --shell /usr/sbin/nologin backupx
if ! getent group backupx >/dev/null 2>&1; then
if command -v groupadd >/dev/null 2>&1; then
groupadd --system backupx
elif command -v addgroup >/dev/null 2>&1; then
addgroup --system backupx
else
echo "需要 groupadd 或 addgroup 来创建 backupx 组" >&2
exit 1
fi
fi
if ! id backupx >/dev/null 2>&1; then
if command -v useradd >/dev/null 2>&1; then
useradd --system --gid backupx --home-dir "$INSTALL_PREFIX" --shell /usr/sbin/nologin backupx
elif command -v adduser >/dev/null 2>&1; then
adduser --system --ingroup backupx --home "$INSTALL_PREFIX" --shell /usr/sbin/nologin backupx
else
echo "需要 useradd 或 adduser 来创建 backupx 用户" >&2
exit 1
fi
fi
id backupx >/dev/null 2>&1 || { echo "backupx 用户创建失败" >&2; exit 1; }
install -d -o backupx -g backupx "$INSTALL_PREFIX" /var/lib/backupx-agent
install -m 0755 "$TMPDIR/backupx-${AGENT_VERSION}-linux-${ARCH}/backupx" "$INSTALL_PREFIX/backupx"
{{end}}
@@ -66,6 +86,7 @@ Wants=network-online.target
[Service]
Type=simple
User=backupx
Group=backupx
Environment="BACKUPX_AGENT_MASTER=${MASTER_URL}"
Environment="BACKUPX_AGENT_TOKEN=${AGENT_TOKEN}"
ExecStart=${INSTALL_PREFIX}/backupx agent --temp-dir /var/lib/backupx-agent