Files
dingMCP/test/portable-node-bootstrap.test.ts
gitea-admin a08a2c1a96
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
fix: harden proxied client installation
2026-08-18 16:01:06 +00:00

260 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
const NODE_VERSION = "22.23.2";
const RUNTIME_NAME = `node-v${NODE_VERSION}-test`;
const RUNTIME_ARCHIVE = `${RUNTIME_NAME}.tar.gz`;
test("POSIX bootstrap installs a private Node and falls back after a network failure", {
skip: process.platform === "win32",
}, () => {
const fixture = createPosixFixture();
try {
const result = runPosixFixture(fixture, "fallback");
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout.trim()), { ok: true, status: "installed", managedNode: true });
assert.match(result.stderr, /\[dingtalk-approval\] 1\/4[\s\S]*2\/4[\s\S]*3\/4[\s\S]*4\/4/);
assert.equal(existsSync(join(fixture.clientRoot, "node", RUNTIME_NAME, "bin", "node")), true);
const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/);
assert.deepEqual(calls.slice(0, 2), [
`https://primary.example.test/node/${RUNTIME_ARCHIVE}`,
`https://mirror.example.test/node/${RUNTIME_ARCHIVE}`,
]);
assert.ok(calls.includes("https://release.example.test/installer.mjs"));
assert.ok(calls.includes("https://release.example.test/release-signing-public.pem"));
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
});
test("POSIX bootstrap scopes the Node environment proxy flag to the networked installer", {
skip: process.platform === "win32",
}, () => {
const fixture = createPosixFixture();
try {
const result = runPosixFixture(fixture, "proxy");
assert.equal(result.status, 0, result.stderr);
const invocations = readFileSync(fixture.nodeArgsLog, "utf8").trim().split(/\r?\n/);
assert.ok(invocations.some((value) => value.startsWith("--use-env-proxy ") && value.includes("installer.mjs install")));
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
});
test("POSIX bootstrap falls back for the signed installer and public key downloads", {
skip: process.platform === "win32",
}, () => {
const fixture = createPosixFixture();
try {
const result = runPosixFixture(fixture, "release-fallback");
assert.equal(result.status, 0, result.stderr);
const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/);
assert.ok(calls.includes("https://release.example.test/installer.mjs"));
assert.ok(calls.includes("https://release-mirror.example.test/installer.mjs"));
assert.ok(calls.includes("https://release-mirror.example.test/release-signing-public.pem"));
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
});
test("POSIX bootstrap stops immediately when a downloaded runtime has the wrong hash", {
skip: process.platform === "win32",
}, () => {
const fixture = createPosixFixture();
try {
const result = runPosixFixture(fixture, "corrupt");
assert.notEqual(result.status, 0);
assert.match(result.stderr, /SHA-256 不匹配,停止安装/);
const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/);
assert.deepEqual(calls, [`https://primary.example.test/node/${RUNTIME_ARCHIVE}`]);
assert.equal(existsSync(join(fixture.clientRoot, "node", RUNTIME_NAME, "bin", "node")), false);
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
});
test("POSIX bootstrap does not hide a non-404 HTTP error by switching mirrors", {
skip: process.platform === "win32",
}, () => {
const fixture = createPosixFixture();
try {
const result = runPosixFixture(fixture, "server-error");
assert.notEqual(result.status, 0);
assert.match(result.stderr, /不允许换源.*HTTP 500/);
const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/);
assert.deepEqual(calls, [`https://primary.example.test/node/${RUNTIME_ARCHIVE}`]);
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
});
test("Windows bootstrap is valid PowerShell after release rendering", {
skip: process.platform !== "win32",
}, () => {
const root = mkdtempSync(join(tmpdir(), "dingmcp-windows-bootstrap-"));
try {
const script = join(root, "bootstrap.ps1");
writeFileSync(script, renderWindowsBootstrap());
const powershell = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
const parser = [
"$tokens = $null; $errors = $null",
"[System.Management.Automation.Language.Parser]::ParseFile($args[0], [ref]$tokens, [ref]$errors) | Out-Null",
"if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_.Message }; exit 1 }",
].join("; ");
const result = spawnSync(powershell, ["-NoProfile", "-NonInteractive", "-Command", parser, script], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
interface PosixFixture {
root: string;
clientRoot: string;
callLog: string;
script: string;
fakeBin: string;
archive: string;
installer: string;
publicKey: string;
nodeArgsLog: string;
}
function createPosixFixture(): PosixFixture {
const root = mkdtempSync(join(tmpdir(), "dingmcp-posix-bootstrap-"));
const archiveRoot = join(root, "archive", RUNTIME_NAME);
const fakeBin = join(root, "bin");
const clientRoot = join(root, "home", "dingtalk-approval");
const callLog = join(root, "download-calls.log");
const archive = join(root, RUNTIME_ARCHIVE);
const installer = join(root, "installer.mjs");
const publicKey = join(root, "release-signing-public.pem");
const nodeArgsLog = join(root, "node-args.log");
const script = join(root, "bootstrap.sh");
mkdirSync(join(archiveRoot, "bin"), { recursive: true });
mkdirSync(join(archiveRoot, "lib", "node_modules", "npm", "bin"), { recursive: true });
mkdirSync(fakeBin, { recursive: true });
mkdirSync(join(root, "home"), { recursive: true });
const privateNode = join(archiveRoot, "bin", "node");
writeFileSync(privateNode, [
"#!/bin/sh",
"if [ -n \"${DING_TEST_NODE_ARGS_LOG:-}\" ]; then printf '%s\\n' \"$*\" >> \"$DING_TEST_NODE_ARGS_LOG\"; fi",
"if [ \"${1:-}\" = '-e' ]; then exit 0; fi",
"printf '%s\\n' '{\"ok\":true,\"status\":\"installed\",\"managedNode\":true}'",
"",
].join("\n"));
chmodSync(privateNode, 0o700);
writeFileSync(join(archiveRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js"), "// fixture\n");
const tar = spawnSync("tar", ["-czf", archive, "-C", join(root, "archive"), RUNTIME_NAME], { encoding: "utf8" });
assert.equal(tar.status, 0, tar.stderr);
writeFileSync(installer, "// fixture installer\n");
writeFileSync(publicKey, "fixture public key\n");
writeFileSync(callLog, "");
writeFileSync(nodeArgsLog, "");
const curl = join(fakeBin, "curl");
writeFileSync(curl, `#!/bin/sh
ding_target=''
ding_url=''
while [ "$#" -gt 0 ]; do
case "$1" in
--output) ding_target="$2"; shift 2 ;;
http*) ding_url="$1"; shift ;;
*) shift ;;
esac
done
printf '%s\\n' "$ding_url" >> "$DING_TEST_CALL_LOG"
case "$ding_url" in
https://primary.example.test/node/*)
if [ "$DING_TEST_MODE" = 'corrupt' ]; then printf 'corrupt' > "$ding_target"; printf '200'; exit 0; fi
if [ "$DING_TEST_MODE" = 'server-error' ]; then printf '500'; exit 22; fi
printf '000'; exit 7 ;;
https://mirror.example.test/node/*) /bin/cp "$DING_TEST_ARCHIVE" "$ding_target"; printf '200' ;;
https://official.example.test/node/*) /bin/cp "$DING_TEST_ARCHIVE" "$ding_target"; printf '200' ;;
https://release.example.test/installer.mjs)
if [ "$DING_TEST_MODE" = 'release-fallback' ]; then printf '000'; exit 7; fi
/bin/cp "$DING_TEST_INSTALLER" "$ding_target"; printf '200' ;;
https://release.example.test/release-signing-public.pem)
if [ "$DING_TEST_MODE" = 'release-fallback' ]; then printf '000'; exit 7; fi
/bin/cp "$DING_TEST_PUBLIC_KEY" "$ding_target"; printf '200' ;;
https://release-mirror.example.test/installer.mjs) /bin/cp "$DING_TEST_INSTALLER" "$ding_target"; printf '200' ;;
https://release-mirror.example.test/release-signing-public.pem) /bin/cp "$DING_TEST_PUBLIC_KEY" "$ding_target"; printf '200' ;;
*) printf '404'; exit 22 ;;
esac
`);
chmodSync(curl, 0o700);
const rejectedSystemNode = join(fakeBin, "node");
writeFileSync(rejectedSystemNode, "#!/bin/sh\nexit 1\n");
chmodSync(rejectedSystemNode, 0o700);
let body = readFileSync("distribution/bootstrap-posix.template.sh", "utf8");
const replacements: Record<string, string> = {
"{{NODE_SOURCE_BASES_POSIX}}": "https://primary.example.test/node/ https://mirror.example.test/node/ https://official.example.test/node/",
"{{INSTALLER_URLS_POSIX}}": "https://release.example.test/installer.mjs https://release-mirror.example.test/installer.mjs",
"{{INSTALLER_SHA256}}": digest(installer),
"{{PUBLIC_KEY_URLS_POSIX}}": "https://release.example.test/release-signing-public.pem https://release-mirror.example.test/release-signing-public.pem",
"{{PUBLIC_KEY_SHA256}}": digest(publicKey),
"{{MANIFEST_URL}}": "https://release.example.test/manifest.json",
"{{MANIFEST_MIRROR_URLS_POSIX}}": "https://release-mirror.example.test/manifest.json",
};
for (const platform of ["LINUX_X64", "LINUX_ARM64", "DARWIN_X64", "DARWIN_ARM64"]) {
replacements[`{{NODE_${platform}_FILE}}`] = RUNTIME_ARCHIVE;
replacements[`{{NODE_${platform}_SHA256}}`] = digest(archive);
}
for (const [placeholder, value] of Object.entries(replacements)) body = body.replaceAll(placeholder, value);
assert.doesNotMatch(body, /\{\{/);
writeFileSync(script, body);
chmodSync(script, 0o700);
return { root, clientRoot, callLog, script, fakeBin, archive, installer, publicKey, nodeArgsLog };
}
function runPosixFixture(fixture: PosixFixture, mode: "fallback" | "corrupt" | "server-error" | "proxy" | "release-fallback") {
return spawnSync("/bin/sh", [fixture.script], {
encoding: "utf8",
env: {
HOME: join(fixture.root, "home"),
PATH: `${fixture.fakeBin}:/usr/bin:/bin`,
DINGTALK_APPROVAL_CLIENT_HOME: fixture.clientRoot,
DING_TEST_MODE: mode,
DING_TEST_CALL_LOG: fixture.callLog,
DING_TEST_ARCHIVE: fixture.archive,
DING_TEST_INSTALLER: fixture.installer,
DING_TEST_PUBLIC_KEY: fixture.publicKey,
DING_TEST_NODE_ARGS_LOG: fixture.nodeArgsLog,
...(mode === "proxy" ? { HTTPS_PROXY: "http://proxy.example.test:8080" } : {}),
},
});
}
function renderWindowsBootstrap(): string {
let body = readFileSync("distribution/bootstrap-windows.template.ps1", "utf8");
const replacements: Record<string, string> = {
"{{NODE_SOURCE_BASES_POWERSHELL}}": '"https://primary.example.test/node/", "https://official.example.test/node/"',
"{{INSTALLER_URLS_POWERSHELL}}": '"https://release.example.test/installer.mjs", "https://release-mirror.example.test/installer.mjs"',
"{{INSTALLER_SHA256}}": "a".repeat(64),
"{{PUBLIC_KEY_URLS_POWERSHELL}}": '"https://release.example.test/release-signing-public.pem", "https://release-mirror.example.test/release-signing-public.pem"',
"{{PUBLIC_KEY_SHA256}}": "b".repeat(64),
"{{MANIFEST_URL}}": "https://release.example.test/manifest.json",
"{{MANIFEST_MIRROR_URLS_POWERSHELL}}": '"https://release-mirror.example.test/manifest.json"',
};
for (const platform of ["WIN32_X64", "WIN32_ARM64"]) {
replacements[`{{NODE_${platform}_FILE}}`] = `node-v${NODE_VERSION}-win-x64.zip`;
replacements[`{{NODE_${platform}_SHA256}}`] = "c".repeat(64);
}
for (const [placeholder, value] of Object.entries(replacements)) body = body.replaceAll(placeholder, value);
assert.doesNotMatch(body, /\{\{/);
return body;
}
function digest(path: string): string {
return createHash("sha256").update(readFileSync(path)).digest("hex");
}