diff --git a/scripts/install.js b/scripts/install.js index 3e643f107..1f967d268 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -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 }; diff --git a/scripts/install.test.js b/scripts/install.test.js index 664cd2337..ad669613e 100644 --- a/scripts/install.test.js +++ b/scripts/install.test.js @@ -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); + }); +});