fix(install): detect curl version before using --ssl-revoke-best-effort (#1124)

* fix(install): detect curl version before using --ssl-revoke-best-effort

(cherry picked from commit da14737702)

* test(install): cover curl version gate and refactor for testability

Extract the version comparison out of curlSupportsSslRevokeBestEffort()
into a pure isCurlVersionSupported(output), so the >= 7.70.0 logic is unit
testable without spawning curl. Add cases for 7.55.1 / 7.69.0 / 7.70.0 /
8.x plus the unparseable and libcurl-token edge cases (the regex must read
the leading "curl X.Y.Z", not the trailing "libcurl/X.Y.Z").

Memoize the `curl --version` probe: curl's version is invariant for the
install's lifetime while download() runs once per mirror URL, so probe at
most once instead of re-spawning curl on every attempt.

---------

Co-authored-by: EllienTang <146210093+Ellien-Tang@users.noreply.github.com>
Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
This commit is contained in:
liangshuo-1
2026-05-28 22:51:16 +08:00
committed by GitHub
parent a2dde84158
commit a2cc5e124e
2 changed files with 105 additions and 4 deletions

View File

@@ -110,6 +110,52 @@ function getMirrorUrls(env) {
return urls;
}
/**
* Decide from a `curl --version` output whether curl is >= 7.70.0 — the
* release (2020-04-29) that introduced --ssl-revoke-best-effort. Kept pure
* (no I/O) so the version-comparison logic can be unit tested without
* spawning a process. Reads the leading "curl X.Y.Z" token, ignoring the
* trailing "libcurl/X.Y.Z" that may report a different version.
*
* @param {string} versionOutput raw stdout of `curl --version`
* @returns {boolean} true when the parsed version is >= 7.70.0
*/
function isCurlVersionSupported(versionOutput) {
const match = String(versionOutput).match(/^\s*curl\s+(\d+)\.(\d+)\.(\d+)/i);
if (!match) return false;
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
return major > 7 || (major === 7 && minor >= 70);
}
// Memoized probe result. curl's version is invariant for the lifetime of the
// install, while download() runs once per mirror URL — so probe at most once.
let _curlSupportsSslRevokeBestEffort;
/**
* Detect whether the system curl supports --ssl-revoke-best-effort. Older
* versions (notably the curl 7.55.1 shipped with older Windows 10 builds)
* exit with "unknown option" if the flag is passed.
*
* @returns {boolean} true when curl >= 7.70.0 is available
*/
function curlSupportsSslRevokeBestEffort() {
if (_curlSupportsSslRevokeBestEffort !== undefined) {
return _curlSupportsSslRevokeBestEffort;
}
try {
const output = execFileSync("curl", ["--version"], {
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
timeout: 5000,
});
_curlSupportsSslRevokeBestEffort = isCurlVersionSupported(output);
} catch (_) {
_curlSupportsSslRevokeBestEffort = false;
}
return _curlSupportsSslRevokeBestEffort;
}
function download(url, destPath) {
assertAllowedHost(url);
const args = [
@@ -119,8 +165,11 @@ function download(url, destPath) {
"--output", destPath,
];
// --ssl-revoke-best-effort: on Windows (Schannel), avoid CRYPT_E_REVOCATION_OFFLINE
// errors when the certificate revocation list server is unreachable
if (isWindows) args.unshift("--ssl-revoke-best-effort");
// errors when the certificate revocation list server is unreachable.
// Only use it when the system curl is new enough (>= 7.70.0).
if (isWindows && curlSupportsSslRevokeBestEffort()) {
args.unshift("--ssl-revoke-best-effort");
}
args.push(url);
execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
}
@@ -294,4 +343,4 @@ if (require.main === module) {
}
}
module.exports = { getExpectedChecksum, verifyChecksum, assertAllowedHost, resolveMirrorUrls };
module.exports = { getExpectedChecksum, verifyChecksum, assertAllowedHost, resolveMirrorUrls, curlSupportsSslRevokeBestEffort, isCurlVersionSupported };

View File

@@ -9,7 +9,7 @@ const os = require("os");
const crypto = require("crypto");
const { getExpectedChecksum, verifyChecksum, assertAllowedHost, resolveMirrorUrls } = require("./install.js");
const { getExpectedChecksum, verifyChecksum, assertAllowedHost, resolveMirrorUrls, isCurlVersionSupported } = require("./install.js");
describe("getExpectedChecksum", () => {
function makeTmpChecksums(content) {
@@ -278,3 +278,55 @@ describe("resolveMirrorUrls", () => {
);
});
});
describe("isCurlVersionSupported", () => {
// --ssl-revoke-best-effort was introduced in curl 7.70.0; below that the
// flag is unknown and `curl` exits non-zero (see issue #1099).
it("returns false for curl 7.55.1 (older Windows 10, flag unknown)", () => {
assert.equal(
isCurlVersionSupported("curl 7.55.1 (x86_64-pc-win32) libcurl/7.55.1"),
false
);
});
it("returns false for curl 7.69.0 (just below the 7.70.0 threshold)", () => {
assert.equal(
isCurlVersionSupported("curl 7.69.0 (x86_64-pc-win32) libcurl/7.69.0"),
false
);
});
it("returns true for curl 7.70.0 (flag introduced here)", () => {
assert.equal(
isCurlVersionSupported("curl 7.70.0 (x86_64-pc-win32) libcurl/7.70.0"),
true
);
});
it("returns true for a future major (curl 8.x)", () => {
assert.equal(
isCurlVersionSupported("curl 8.5.0 (x86_64-apple-darwin) libcurl/8.5.0"),
true
);
});
it("returns false when no version can be parsed", () => {
assert.equal(isCurlVersionSupported("not a curl version string"), false);
assert.equal(isCurlVersionSupported(""), false);
});
it("reads the leading 'curl X.Y.Z', not the trailing libcurl/X.Y.Z", () => {
// Guards the regex against latching onto "libcurl/7.55.1" when the
// curl binary itself is new enough.
assert.equal(
isCurlVersionSupported("curl 8.0.0 (x86_64) libcurl/7.55.1"),
true
);
});
it("does not match a 'libcurl X.Y.Z' token (anchored to leading curl)", () => {
// "libcurl 8.0.0" contains the substring "curl 8.0.0"; the leading
// anchor keeps it from being mistaken for a real curl version line.
assert.equal(isCurlVersionSupported("libcurl 8.0.0"), false);
});
});