fix: harden proxied client installation
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s

This commit is contained in:
2026-08-18 16:01:06 +00:00
parent 40c5021c85
commit a08a2c1a96
15 changed files with 513 additions and 87 deletions
+49 -20
View File
@@ -14,10 +14,16 @@
- 公钥编号:`{{RELEASE_KEY_ID}}`
- 签名清单:`{{MANIFEST_URL}}`
- 清单签名:`{{MANIFEST_SIGNATURE_URL}}`
- 国内备用发布源:
{{DOWNLOAD_MIRROR_BASE_URLS_MARKDOWN}}
- 统一网关:`{{GATEWAY_URL}}`
- Windows 安装引导:`{{BOOTSTRAP_WINDOWS_URL}}`
- Windows 安装引导备用地址:
{{BOOTSTRAP_WINDOWS_MIRROR_URLS_MARKDOWN}}
- Windows 安装引导 SHA-256:`{{BOOTSTRAP_WINDOWS_SHA256}}`
- Linux/macOS 安装引导:`{{BOOTSTRAP_POSIX_URL}}`
- Linux/macOS 安装引导备用地址:
{{BOOTSTRAP_POSIX_MIRROR_URLS_MARKDOWN}}
- Linux/macOS 安装引导 SHA-256:`{{BOOTSTRAP_POSIX_SHA256}}`
- 私有 Node.js:`{{NODE_VERSION}}`(最低兼容版本 `{{NODE_MINIMUM_VERSION}}`)
@@ -51,6 +57,8 @@
为执行平台安装引导的 Agent 工具调用设置至少 10 分钟超时。引导和安装器会把 `[dingtalk-approval]` 开头的阶段进度写入 stderr,把唯一的最终结果 JSON 写入 stdout;进度文本不是失败,也不能混入 JSON 解析。进程仍在运行时不得并发启动第二次安装。
如果当前环境通过 `HTTPS_PROXY`、`HTTP_PROXY` 或 `ALL_PROXY` 出网,安装引导会在兼容 Node 上自动为下载、登录和 Bridge 启用环境代理,并在离线 npm 展开前隔离 Node 的代理开关。不要设置全局 `NODE_OPTIONS=--use-env-proxy`,也不要手工预装 `node_modules`;这会让 npm 与 Node 的代理机制叠加,并破坏安装器的原子安装与故障恢复。
### Windows(PowerShell)
无论当前 Agent 默认使用 PowerShell、CMD 还是 Git Bash,Windows 都必须调用普通 PowerShell 执行下面的完整流程。不得从 Git Bash 直接运行内部的 `node dingtalk-approval-installer.mjs ...` 命令;内部安装器不是用户安装入口。
@@ -61,9 +69,20 @@
$dingBootstrapDir = Join-Path ([System.IO.Path]::GetTempPath()) ("dingmcp-bootstrap-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $dingBootstrapDir -Force | Out-Null
$dingBootstrap = Join-Path $dingBootstrapDir "dingtalk-approval-bootstrap.ps1"
Invoke-WebRequest -UseBasicParsing -Uri "{{BOOTSTRAP_WINDOWS_URL}}" -OutFile $dingBootstrap
$dingBootstrapHash = (Get-FileHash -Algorithm SHA256 $dingBootstrap).Hash.ToLowerInvariant()
if ($dingBootstrapHash -ne "{{BOOTSTRAP_WINDOWS_SHA256}}") { throw "Windows 安装引导 SHA-256 不匹配,停止安装" }
$dingBootstrapUrls = @({{BOOTSTRAP_WINDOWS_URLS_POWERSHELL}})
$dingBootstrapDownloaded = $false
foreach ($dingBootstrapUrl in $dingBootstrapUrls) {
try {
Invoke-WebRequest -UseBasicParsing -Uri $dingBootstrapUrl -OutFile $dingBootstrap -TimeoutSec 300
} catch {
continue
}
$dingBootstrapHash = (Get-FileHash -Algorithm SHA256 $dingBootstrap).Hash.ToLowerInvariant()
if ($dingBootstrapHash -ne "{{BOOTSTRAP_WINDOWS_SHA256}}") { throw "Windows 安装引导 SHA-256 不匹配,停止安装:$dingBootstrapUrl" }
$dingBootstrapDownloaded = $true
break
}
if (-not $dingBootstrapDownloaded) { throw "所有 Windows 安装引导下载源均不可用" }
$dingInstallJson = & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $dingBootstrap
if ($LASTEXITCODE -ne 0) { throw "钉钉审批安装引导执行失败:$dingInstallJson" }
$dingInstall = $dingInstallJson | ConvertFrom-Json
@@ -78,21 +97,31 @@ $dingInstall | ConvertTo-Json -Depth 20
```bash
ding_bootstrap_dir="$(mktemp -d)"
ding_bootstrap="$ding_bootstrap_dir/dingtalk-approval-bootstrap.sh"
if command -v curl >/dev/null 2>&1; then
curl --fail --location --output "$ding_bootstrap" "{{BOOTSTRAP_POSIX_URL}}"
elif command -v wget >/dev/null 2>&1; then
wget --https-only --output-document="$ding_bootstrap" "{{BOOTSTRAP_POSIX_URL}}"
else
echo '缺少 curl 或 wget,无法下载安装引导' >&2
exit 1
fi
if command -v sha256sum >/dev/null 2>&1; then
ding_bootstrap_hash="$(sha256sum "$ding_bootstrap" | awk '{print $1}')"
else
ding_bootstrap_hash="$(shasum -a 256 "$ding_bootstrap" | awk '{print $1}')"
fi
if [ "$ding_bootstrap_hash" != '{{BOOTSTRAP_POSIX_SHA256}}' ]; then
echo 'Linux/macOS 安装引导 SHA-256 不匹配,停止安装' >&2
ding_bootstrap_downloaded=0
for ding_bootstrap_url in {{BOOTSTRAP_POSIX_URLS_POSIX}}; do
rm -f "$ding_bootstrap"
if command -v curl >/dev/null 2>&1; then
curl --fail --location --connect-timeout 10 --max-time 300 --output "$ding_bootstrap" "$ding_bootstrap_url" || continue
elif command -v wget >/dev/null 2>&1; then
wget --https-only --timeout=300 --output-document="$ding_bootstrap" "$ding_bootstrap_url" || continue
else
echo '缺少 curl 或 wget,无法下载安装引导' >&2
exit 1
fi
if command -v sha256sum >/dev/null 2>&1; then
ding_bootstrap_hash="$(sha256sum "$ding_bootstrap" | awk '{print $1}')"
else
ding_bootstrap_hash="$(shasum -a 256 "$ding_bootstrap" | awk '{print $1}')"
fi
if [ "$ding_bootstrap_hash" != '{{BOOTSTRAP_POSIX_SHA256}}' ]; then
echo "Linux/macOS 安装引导 SHA-256 不匹配,停止安装:$ding_bootstrap_url" >&2
exit 1
fi
ding_bootstrap_downloaded=1
break
done
if [ "$ding_bootstrap_downloaded" -ne 1 ]; then
echo '所有 Linux/macOS 安装引导下载源均不可用' >&2
exit 1
fi
ding_install_json="$(sh "$ding_bootstrap")"
@@ -106,9 +135,9 @@ printf '%s\n' "$ding_install_json"
步骤 2 的平台引导是唯一通用安装入口。它会:
1. 检查系统 Node;缺失或版本不兼容时,根据当前操作系统和 CPU 架构选择私有 Node;
2. 按“阿里云精确版本 → npmmirror 精确版本 → Node 官方精确版本”的顺序下载。只有网络失败、超时或 404 才切换来源;任一已下载文件的 SHA-256 不匹配都会立即终止;
2. 按“阿里云精确版本 → npmmirror 精确版本 → Node 官方精确版本”的顺序下载 Node;项目安装引导和发布物则按“主发布源 → 国内 Gitea 备用源 → 其他已配置镜像”回退。网络或 HTTP 下载失败才切换来源,任一已下载文件的签名或 SHA-256 不匹配都会立即终止;
3. 把私有 Node 放在插件自己的当前用户数据目录,不修改系统 PATH,不要求管理员权限;
4. 下载并校验首次安装器和发布公钥,再由安装器验证 Ed25519 清单签名与全部客户端发布物;
4. 下载并校验首次安装器和发布公钥,再由安装器验证 Ed25519 清单签名与全部客户端发布物;下载源不是信任根,备用源不能绕过签名或哈希;
5. 返回一份 JSON 安装结果。
安装器会在稳定的当前用户数据目录安装客户端与 Skill,不会全局安装 npm 包,也不会修改 Agent 私有配置。保留完整 JSON 结果,后续只使用其中的真实字段,不要猜路径。
+22 -5
View File
@@ -2,11 +2,12 @@
set -eu
ding_source_bases='{{NODE_SOURCE_BASES_POSIX}}'
ding_installer_url='{{INSTALLER_URL}}'
ding_installer_urls='{{INSTALLER_URLS_POSIX}}'
ding_installer_sha256='{{INSTALLER_SHA256}}'
ding_public_key_url='{{PUBLIC_KEY_URL}}'
ding_public_key_urls='{{PUBLIC_KEY_URLS_POSIX}}'
ding_public_key_sha256='{{PUBLIC_KEY_SHA256}}'
ding_manifest_url='{{MANIFEST_URL}}'
ding_manifest_mirror_urls='{{MANIFEST_MIRROR_URLS_POSIX}}'
ding_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/dingmcp-install.XXXXXX")"
trap 'rm -rf "$ding_temp_dir"' EXIT HUP INT TERM
@@ -164,8 +165,24 @@ fi
ding_progress '3/4 下载并校验签名安装器与发布公钥'
ding_installer="$ding_temp_dir/installer.mjs"
ding_public_key="$ding_temp_dir/release-signing-public.pem"
ding_download_verified "$ding_installer" "$ding_installer_sha256" "$ding_installer_url"
ding_download_verified "$ding_public_key" "$ding_public_key_sha256" "$ding_public_key_url"
ding_download_verified "$ding_installer" "$ding_installer_sha256" $ding_installer_urls
ding_download_verified "$ding_public_key" "$ding_public_key_sha256" $ding_public_key_urls
ding_progress '4/4 执行安装器;最终 JSON 输出到 stdout,阶段进度输出到 stderr'
"$ding_node_path" "$ding_installer" install --manifest-url "$ding_manifest_url" --public-key "$ding_public_key" --json
set -- "$ding_installer" install --manifest-url "$ding_manifest_url"
for ding_manifest_mirror_url in $ding_manifest_mirror_urls; do
set -- "$@" --manifest-mirror-url "$ding_manifest_mirror_url"
done
set -- "$@" --public-key "$ding_public_key" --json
ding_node_proxy_arg=''
if [ -n "${HTTPS_PROXY:-}${https_proxy:-}${HTTP_PROXY:-}${http_proxy:-}${ALL_PROXY:-}${all_proxy:-}" ] \
&& ! printf '%s' "${NODE_OPTIONS:-}" | grep -Eq '(^|[[:space:]])--use-env-proxy($|[[:space:]])' \
&& ! printf '%s' "${NODE_USE_ENV_PROXY:-}" | grep -Eiq '^(1|true)$' \
&& "$ding_node_path" -e 'process.exit(process.allowedNodeEnvironmentFlags.has("--use-env-proxy") ? 0 : 1)'; then
ding_node_proxy_arg='--use-env-proxy'
fi
if [ -n "$ding_node_proxy_arg" ]; then
"$ding_node_path" "$ding_node_proxy_arg" "$@"
else
"$ding_node_path" "$@"
fi
+18 -5
View File
@@ -6,11 +6,12 @@ if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction Sile
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$dingSourceBases = @({{NODE_SOURCE_BASES_POWERSHELL}})
$dingInstallerUrl = "{{INSTALLER_URL}}"
$dingInstallerUrls = @({{INSTALLER_URLS_POWERSHELL}})
$dingInstallerSha256 = "{{INSTALLER_SHA256}}"
$dingPublicKeyUrl = "{{PUBLIC_KEY_URL}}"
$dingPublicKeyUrls = @({{PUBLIC_KEY_URLS_POWERSHELL}})
$dingPublicKeySha256 = "{{PUBLIC_KEY_SHA256}}"
$dingManifestUrl = "{{MANIFEST_URL}}"
$dingManifestMirrorUrls = @({{MANIFEST_MIRROR_URLS_POWERSHELL}})
function Write-DingProgress([string] $dingMessage) {
[Console]::Error.WriteLine("[dingtalk-approval] $dingMessage")
@@ -113,10 +114,22 @@ New-Item -ItemType Directory -Path $dingInstallTemp -Force | Out-Null
try {
$dingInstaller = Join-Path $dingInstallTemp "installer.mjs"
$dingPublicKey = Join-Path $dingInstallTemp "release-signing-public.pem"
Save-DingDownload @($dingInstallerUrl) $dingInstaller $dingInstallerSha256
Save-DingDownload @($dingPublicKeyUrl) $dingPublicKey $dingPublicKeySha256
Save-DingDownload $dingInstallerUrls $dingInstaller $dingInstallerSha256
Save-DingDownload $dingPublicKeyUrls $dingPublicKey $dingPublicKeySha256
Write-DingProgress "4/4 执行安装器;最终 JSON 输出到 stdout,阶段进度输出到 stderr"
$dingInstallOutput = & $dingNodePath $dingInstaller install --manifest-url $dingManifestUrl --public-key $dingPublicKey --json
$dingInstallerArgs = @($dingInstaller, "install", "--manifest-url", $dingManifestUrl)
foreach ($dingManifestMirrorUrl in $dingManifestMirrorUrls) {
$dingInstallerArgs += @("--manifest-mirror-url", $dingManifestMirrorUrl)
}
$dingInstallerArgs += @("--public-key", $dingPublicKey, "--json")
$dingNodeProxyArgs = @()
$dingProxyConfigured = $env:HTTPS_PROXY -or $env:HTTP_PROXY -or $env:ALL_PROXY
$dingProxyAlreadyEnabled = ($env:NODE_OPTIONS -match '(^|\s)--use-env-proxy($|\s)') -or ($env:NODE_USE_ENV_PROXY -match '^(1|true)$')
if ($dingProxyConfigured -and -not $dingProxyAlreadyEnabled) {
& $dingNodePath -e 'process.exit(process.allowedNodeEnvironmentFlags.has("--use-env-proxy") ? 0 : 1)'
if ($LASTEXITCODE -eq 0) { $dingNodeProxyArgs = @("--use-env-proxy") }
}
$dingInstallOutput = & $dingNodePath @dingNodeProxyArgs @dingInstallerArgs
$dingInstallExit = $LASTEXITCODE
if ($dingInstallExit -ne 0) { throw "钉钉审批安装器执行失败:$dingInstallOutput" }
$dingResult = $dingInstallOutput | ConvertFrom-Json