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 eff48342c8
commit 1715abfcfb
13 changed files with 232 additions and 35 deletions

View File

@@ -6,6 +6,7 @@ import { Step3CommandPreview } from './wizard/Step3CommandPreview'
import { BatchCommandTable, type BatchCommandRow } from './BatchCommandTable'
import { batchCreateNodes, createInstallToken } from '../../services/nodes'
import type { InstallTokenResult } from '../../types/nodes'
import { buildAgentInstallCommand } from './installCommands'
const Step = Steps.Step
@@ -162,7 +163,7 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
const rows: BatchCommandRow[] = tokens.map(({ c, tok }) => ({
nodeId: c.id,
nodeName: c.name,
command: `curl -fsSL ${tok.url} | sudo bash`,
command: buildAgentInstallCommand(tok.url, tok.fallbackUrl, tok.scriptBase64),
expiresAt: tok.expiresAt,
}))
if (mountedRef.current) setBatchRows(rows)

View File

@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { buildAgentDownloadCommand, buildAgentInstallCommand } from './installCommands'
describe('install command builders', () => {
it('adds script marker validation and fallback install path', () => {
const cmd = buildAgentInstallCommand('https://master.example.com/api/install/abc')
expect(cmd).toContain('BACKUPX_AGENT_INSTALL_V1')
expect(cmd).toContain("'https://master.example.com/api/install/abc'")
expect(cmd).toContain("'https://master.example.com/install/abc'")
expect(cmd).toContain('sh "$tmp"')
})
it('uses explicit fallback URL when provided', () => {
const cmd = buildAgentDownloadCommand(
'https://master.example.com/api/install/abc',
'https://master.example.com/install/abc',
)
expect(cmd).toContain('/tmp/bx-agent-install.sh')
expect(cmd).toContain("'https://master.example.com/install/abc'")
expect(cmd).toContain('non-script content')
})
it('prefers embedded script content when available', () => {
const cmd = buildAgentInstallCommand(
'https://master.example.com/api/install/abc',
'https://master.example.com/install/abc',
'IyEvYmluL3NoCg==',
)
expect(cmd).toContain('base64 -d')
expect(cmd).toContain('base64 -D')
expect(cmd).toContain("'IyEvYmluL3NoCg=='")
expect(cmd).not.toContain('https://master.example.com/api/install/abc')
})
})

View File

@@ -0,0 +1,67 @@
const INSTALL_MAGIC_MARKER = 'BACKUPX_AGENT_INSTALL_V1'
function shellQuote(value: string) {
return `'${value.replace(/'/g, `'\\''`)}'`
}
function legacyInstallUrl(url: string) {
return url.replace('/api/install/', '/install/')
}
function runScriptCommand(path: string) {
return `if [ "$(id -u)" -eq 0 ]; then sh ${path}; else sudo sh ${path}; fi`
}
export function buildAgentInstallCommand(url: string, fallbackUrl?: string, scriptBase64?: string) {
if (scriptBase64?.trim()) {
const marker = shellQuote(INSTALL_MAGIC_MARKER)
return [
'enc=$(mktemp)',
'tmp=$(mktemp)',
`printf %s ${shellQuote(scriptBase64.trim())} > "$enc"`,
'(base64 -d < "$enc" > "$tmp" 2>/dev/null || base64 -D < "$enc" > "$tmp")',
`{ grep -q ${marker} "$tmp" || { echo 'BackupX embedded installer is invalid.' >&2; head -5 "$tmp" >&2; false; }; }`,
runScriptCommand('"$tmp"'),
].join(' && ') + '; rc=$?; rm -f "$enc" "$tmp"; test $rc -eq 0'
}
const primary = url.trim()
const fallback = (fallbackUrl || legacyInstallUrl(primary)).trim()
const urls = fallback && fallback !== primary ? [primary, fallback] : [primary]
const marker = shellQuote(INSTALL_MAGIC_MARKER)
const fetchScript = urls.length > 1
? `(curl -fsSL ${shellQuote(urls[0])} -o "$tmp" && grep -q ${marker} "$tmp" || curl -fsSL ${shellQuote(urls[1])} -o "$tmp")`
: `(curl -fsSL ${shellQuote(urls[0])} -o "$tmp" && grep -q ${marker} "$tmp")`
return [
'tmp=$(mktemp)',
fetchScript,
`{ grep -q ${marker} "$tmp" || { echo 'BackupX install endpoint returned non-script content; check reverse proxy /api/install or /install forwarding.' >&2; head -5 "$tmp" >&2; false; }; }`,
runScriptCommand('"$tmp"'),
].join(' && ') + '; rc=$?; rm -f "$tmp"; test $rc -eq 0'
}
export function buildAgentDownloadCommand(url: string, fallbackUrl?: string, scriptBase64?: string) {
if (scriptBase64?.trim()) {
const marker = shellQuote(INSTALL_MAGIC_MARKER)
return [
`printf %s ${shellQuote(scriptBase64.trim())} > /tmp/bx-agent-install.b64`,
'(base64 -d < /tmp/bx-agent-install.b64 > /tmp/bx-agent-install.sh 2>/dev/null || base64 -D < /tmp/bx-agent-install.b64 > /tmp/bx-agent-install.sh)',
`{ grep -q ${marker} /tmp/bx-agent-install.sh || { echo 'BackupX embedded installer is invalid.' >&2; head -5 /tmp/bx-agent-install.sh >&2; false; }; }`,
runScriptCommand('/tmp/bx-agent-install.sh'),
].join(' && ')
}
const primary = url.trim()
const fallback = (fallbackUrl || legacyInstallUrl(primary)).trim()
const marker = shellQuote(INSTALL_MAGIC_MARKER)
const fetchScript = fallback && fallback !== primary
? `(curl -fsSL ${shellQuote(primary)} -o /tmp/bx-agent-install.sh && grep -q ${marker} /tmp/bx-agent-install.sh || curl -fsSL ${shellQuote(fallback)} -o /tmp/bx-agent-install.sh)`
: `(curl -fsSL ${shellQuote(primary)} -o /tmp/bx-agent-install.sh && grep -q ${marker} /tmp/bx-agent-install.sh)`
return [
fetchScript,
`{ grep -q ${marker} /tmp/bx-agent-install.sh || { echo 'BackupX install endpoint returned non-script content; check reverse proxy /api/install or /install forwarding.' >&2; head -5 /tmp/bx-agent-install.sh >&2; false; }; }`,
runScriptCommand('/tmp/bx-agent-install.sh'),
].join(' && ')
}

View File

@@ -3,6 +3,7 @@ import { Typography, Button, Space, Collapse, Spin, Message, Tag } from '@arco-d
import { IconCopy, IconRefresh } from '@arco-design/web-react/icon'
import { fetchScriptPreview } from '../../../services/nodes'
import type { InstallTokenResult, InstallMode } from '../../../types/nodes'
import { buildAgentDownloadCommand, buildAgentInstallCommand } from '../installCommands'
const { Text } = Typography
@@ -29,11 +30,8 @@ export function Step3CommandPreview({ nodeId, nodeName, token, mode, previewPara
}, [token.expiresAt])
const expired = remaining === 0
// 使用 bash 管道执行:避开 Debian/Ubuntu 默认 /bin/sh=dash 的差异,
// 同时让反向代理 / CDN 不再按 "sh" 的脚本类型做内容识别issue #46
const command = `curl -fsSL ${token.url} | sudo bash`
// 备用命令:若当前机器无 bash或中间代理过滤了管道响应可先落盘再执行。
const fallbackCommand = `curl -fsSL ${token.url} -o /tmp/bx-agent-install.sh && sudo sh /tmp/bx-agent-install.sh`
const command = buildAgentInstallCommand(token.url, token.fallbackUrl, token.scriptBase64)
const fallbackCommand = buildAgentDownloadCommand(token.url, token.fallbackUrl, token.scriptBase64)
const dockerComposeCmd = mode === 'docker' && token.composeUrl
? `curl -fsSL ${token.composeUrl} -o docker-compose.yml && docker-compose up -d`
: null
@@ -82,7 +80,7 @@ export function Step3CommandPreview({ nodeId, nodeName, token, mode, previewPara
<div style={{ background: 'var(--color-fill-2)', padding: '12px 14px', borderRadius: 6, marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
bash /
/tmp
</Text>
<Text style={{
fontFamily: 'monospace', fontSize: 13, wordBreak: 'break-all',
@@ -110,7 +108,7 @@ export function Step3CommandPreview({ nodeId, nodeName, token, mode, previewPara
)}
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 8 }}>
token
token TTL
</Text>
<Collapse bordered={false} onChange={(_key, keys) => {

View File

@@ -44,5 +44,8 @@ export interface InstallTokenResult {
installToken: string
expiresAt: string
url: string
fallbackUrl?: string
scriptBase64?: string
composeUrl: string
fallbackComposeUrl?: string
}