fix: prefer native domestic installer downloads
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 17:12:20 +00:00
parent a08a2c1a96
commit 3b7d6a5a24
10 changed files with 270 additions and 40 deletions
+120 -1
View File
@@ -10,7 +10,7 @@ import { basename, delimiter, dirname, join } from "node:path";
import test from "node:test";
import {
acquireClientInstallLock, cleanupLegacyInstallations, clientLauncherBodies, clientPaths, releaseKeyId,
nodeProxyArgs, npmChildEnvironment, npmFailureMessage, runClientInstaller, runNpm, verifySignedManifest,
downloadWithCurl, nodeProxyArgs, npmChildEnvironment, npmFailureMessage, runClientInstaller, runNpm, verifySignedManifest,
type ClientReleaseManifest,
} from "../src/client-installer.js";
@@ -130,6 +130,125 @@ test("runNpm passes the isolated proxy environment to the real child process", (
}
});
test("native curl downloader honors the proxy environment and never fails silently", async () => {
const root = mkdtempSync(join(tmpdir(), "ding-client-curl-"));
try {
const fakeBin = join(root, "bin");
const curl = join(fakeBin, "curl.exe");
const log = join(root, "curl.log");
mkdirSync(fakeBin, { recursive: true });
writeFileSync(curl, `#!/bin/sh
target=''
url=''
while [ "$#" -gt 0 ]; do
case "$1" in
--output) target="$2"; shift 2 ;;
https://*) url="$1"; shift ;;
*) shift ;;
esac
done
printf '%s|%s\n' "$url" "${'${HTTPS_PROXY:-}'}" >> "$DING_CURL_LOG"
case "$url" in
*/ok) printf 'native-payload' > "$target"; printf '200'; exit 0 ;;
*/missing) printf '404'; exit 22 ;;
*) exit 1 ;;
esac
`);
chmodSync(curl, 0o700);
const env = {
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
HTTPS_PROXY: "http://192.168.199.62:7890",
DING_CURL_LOG: log,
};
assert.equal((await downloadWithCurl(new URL("https://release.example.test/ok"), env, "win32")).toString(), "native-payload");
assert.match(readFileSync(log, "utf8"), /https:\/\/release\.example\.test\/ok\|http:\/\/192\.168\.199\.62:7890/);
await assert.rejects(downloadWithCurl(new URL("https://release.example.test/missing"), env, "win32"), /curl 下载失败.*HTTP 404/);
await assert.rejects(downloadWithCurl(new URL("https://release.example.test/silent"), env, "win32"), /curl 下载失败.*curl 退出码 1/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("installer uses native curl end to end in a proxied Windows-style environment", async () => {
const root = mkdtempSync(join(tmpdir(), "ding-client-curl-install-"));
try {
const appRoot = join(root, "dingtalk-approval");
const fakeBin = join(root, "bin");
const fixtureDir = join(root, "downloads");
const curlLog = join(root, "curl.log");
mkdirSync(fakeBin, { recursive: true });
mkdirSync(fixtureDir, { recursive: true });
createFakeNpm(fakeBin);
const keys = generateKeyPairSync("ed25519");
const publicPem = Buffer.from(keys.publicKey.export({ type: "spki", format: "pem" }));
const keyId = releaseKeyId(publicPem);
const bodies = {
installer: Buffer.from("installer"), publicKey: publicPem, client: Buffer.from("fake-client-tgz"),
skill: Buffer.from("---\nname: dingtalk-approval\ndescription: Test\n---\n"),
};
const manifest = manifestFixture(keyId, publicPem, bodies);
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
const signature = signatureEnvelope(keyId, sign(null, manifestBytes, keys.privateKey));
writeFileSync(join(fixtureDir, "manifest.json"), manifestBytes);
writeFileSync(join(fixtureDir, "manifest.sig"), signature);
for (const [name, body] of Object.entries(bodies)) writeFileSync(join(fixtureDir, name), body);
const curlBody = `#!/bin/sh
target=''
url=''
while [ "$#" -gt 0 ]; do
case "$1" in
--output) target="$2"; shift 2 ;;
https://*) url="$1"; shift ;;
*) shift ;;
esac
done
printf '%s|%s\n' "$url" "${'${HTTPS_PROXY:-}'}" >> "$DING_CURL_LOG"
name="${'${url##*/}'}"
if [ -f "$DING_CURL_FIXTURE_DIR/$name" ]; then
cp "$DING_CURL_FIXTURE_DIR/$name" "$target"
printf '200'
exit 0
fi
printf '404'
exit 22
`;
for (const name of ["curl", "curl.exe"]) {
writeFileSync(join(fakeBin, name), curlBody);
chmodSync(join(fakeBin, name), 0o700);
}
const publicPath = join(root, "release-public.pem");
writeFileSync(publicPath, publicPem, { mode: 0o600 });
const env = {
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
DINGTALK_APPROVAL_CLIENT_HOME: appRoot,
npm_execpath: process.platform === "win32"
? join(fakeBin, "node_modules", "npm", "bin", "npm-cli.js") : join(fakeBin, "fake-npm.mjs"),
HTTPS_PROXY: "http://192.168.199.62:7890",
NODE_OPTIONS: "--trace-warnings --use-env-proxy",
DING_CURL_FIXTURE_DIR: fixtureDir,
DING_CURL_LOG: curlLog,
};
const output: string[] = [];
const progress: string[] = [];
const code = await runClientInstaller([
"install", "--manifest-url", "https://release.example.test/manifest.json",
"--public-key", publicPath, "--app-root", appRoot, "--json",
], {
env, platform: process.platform, home: join(root, "home"), fetchFn: fetch,
stdout: (value) => { output.push(value); }, stderr: (value) => { progress.push(value); },
});
assert.equal(code, 0, progress.join(""));
assert.equal(JSON.parse(output.at(-1)!).status, "installed");
const calls = readFileSync(curlLog, "utf8");
assert.match(calls, /manifest\.json\|http:\/\/192\.168\.199\.62:7890/);
assert.match(calls, /client\|http:\/\/192\.168\.199\.62:7890/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("signed manifest verification rejects tampering and a foreign key", () => {
const trusted = generateKeyPairSync("ed25519");
const foreign = generateKeyPairSync("ed25519");
+5 -1
View File
@@ -25,7 +25,8 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(installer, /安装长时间无最终 JSON[\s\S]*残留 staging/s);
assert.match(installer, /系统无需预装 Node\.js/);
assert.match(installer, /阿里云精确版本.*npmmirror 精确版本.*Node 官方精确版本/s);
assert.match(installer, /国内备用发布源[\s\S]*主发布源.*国内 Gitea 备用源/s);
assert.match(installer, /备用发布源[\s\S]*优先使用国内 Gitea 静态发布源/s);
assert.match(installer, /优先使用系统 `curl`[\s\S]*Node `fetch` 仅在系统没有 `curl` 时兜底/s);
assert.match(installer, /不要设置全局 `NODE_OPTIONS=--use-env-proxy`[\s\S]*不要手工预装 `node_modules`/s);
assert.match(installer, /BOOTSTRAP_WINDOWS_URL|Windows 安装引导/);
assert.match(installer, /Linux\/macOS 安装引导/);
@@ -82,6 +83,9 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(builder, /cpSync\(resolve\("skills\/dingtalk-approval"\).*recursive: true/s);
assert.doesNotMatch(builder, /codexPlugin|codex-plugin|\.codex-plugin|openai\.yaml/);
assert.doesNotMatch(builder, /dingtalk-workspace-cli|DWS_URL|DWS_SHA256/);
const deploy = readFileSync("deploy/install.sh", "utf8");
assert.match(deploy, /--download-base-url "\$\{gitea_release_base_url\}"[\s\S]*--download-mirror-base-url "\$\{public_download_base_url\}"/s);
});
test("public install page explicitly declares UTF-8 for browser rendering", () => {