mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-18 21:17:37 +08:00
fix: make agent install command proxy independent (#50)
This commit is contained in:
@@ -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)
|
||||
|
||||
37
web/src/pages/nodes/installCommands.test.ts
Normal file
37
web/src/pages/nodes/installCommands.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
67
web/src/pages/nodes/installCommands.ts
Normal file
67
web/src/pages/nodes/installCommands.ts
Normal 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(' && ')
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -44,5 +44,8 @@ export interface InstallTokenResult {
|
||||
installToken: string
|
||||
expiresAt: string
|
||||
url: string
|
||||
fallbackUrl?: string
|
||||
scriptBase64?: string
|
||||
composeUrl: string
|
||||
fallbackComposeUrl?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user