fix(tools): skip an unreachable revocation check when curl downloads on Windows (#833)

Schannel curl fails every download with CRYPT_E_REVOCATION_OFFLINE when the CRL/OCSP servers cannot be reached (corporate proxies, firewalls, some regions), although the certificate is valid, so jdtls and other tools could not be installed. On Windows, pass --ssl-revoke-best-effort when `curl --version` reports 7.70+ with Schannel as the active TLS backend: the check is skipped only when the revocation server is offline, and actually revoked certificates are still rejected. Older curl, which rejects the option, is left as is. Fixes #801.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
kevinh
2026-11-03 23:55:58 +08:00
committed by GitHub
co-authored by l0ng-ai
parent deab41cfc1
commit 6a3a712488
+48 -2
View File
@@ -144,18 +144,46 @@ fn strip_ansi(s: &str) -> String {
out
}
/// Whether `curl` checks certificate revocation with Schannel and takes
/// `--ssl-revoke-best-effort` (7.70+). Schannel fails the whole download with
/// CRYPT_E_REVOCATION_OFFLINE when the CRL/OCSP servers are unreachable (corporate
/// proxies, firewalls); best effort skips only that check, and still rejects
/// certificates that are actually revoked.
fn revoke_best_effort(curl: &Path) -> bool {
static CHECKED: parking_lot::Mutex<Vec<(std::path::PathBuf, bool)>> = parking_lot::Mutex::new(Vec::new());
if let Some((_, ok)) = CHECKED.lock().iter().find(|(path, _)| path == curl) {
return *ok;
}
let mut cmd = Command::new(curl);
cmd.arg("--version");
let ok = run_quiet(cmd, Duration::from_secs(5))
.is_some_and(|(success, out)| success && curl_takes_revoke_best_effort(&out));
CHECKED.lock().push((curl.to_path_buf(), ok));
ok
}
/// Reads `curl --version`: `curl 8.9.1 (Windows) libcurl/8.9.1 Schannel …`.
fn curl_takes_revoke_best_effort(version: &str) -> bool {
let Some(first) = version.lines().find(|l| l.starts_with("curl ")) else { return false };
let mut parts = first.split_whitespace().nth(1).unwrap_or("").split('.').map(|n| n.parse::<u32>().ok());
let (Some(Some(major)), Some(Some(minor))) = (parts.next(), parts.next()) else { return false };
(major, minor) >= (7, 70) && first.split_whitespace().any(|w| w.eq_ignore_ascii_case("schannel"))
}
/// HTTP via the system `curl` (shipped with macOS, Windows 10+ and every
/// mainstream Linux), so the IDE carries no TLS stack of its own.
pub struct Http {
curl: std::path::PathBuf,
https_only: bool,
revoke_best_effort: bool,
}
impl Http {
pub fn new(search_path: &std::ffi::OsStr) -> IdeResult<Self> {
let curl = which::which_in("curl", Some(search_path), ".")
.map_err(|_| IdeError::unavailable("`curl` is required to download tools"))?;
Ok(Self { curl, https_only: false })
let revoke_best_effort = cfg!(windows) && revoke_best_effort(&curl);
Ok(Self { curl, https_only: false, revoke_best_effort })
}
/// Refuses every protocol but HTTPS, redirects included.
@@ -173,6 +201,9 @@ impl Http {
if self.https_only {
cmd.args(["--proto", "=https", "--proto-redir", "=https"]);
}
if self.revoke_best_effort {
cmd.arg("--ssl-revoke-best-effort");
}
if url.starts_with("https://api.github.com/")
&& let Ok(token) = std::env::var("GITHUB_TOKEN").or_else(|_| std::env::var("GH_TOKEN"))
&& !token.is_empty()
@@ -438,13 +469,28 @@ mod tests {
assert!(lines.contains(&"one".to_string()) && lines.contains(&"two".to_string()), "{lines:?}");
}
#[test]
fn revoke_best_effort_needs_schannel_curl_7_70() {
let schannel = "curl 8.9.1 (Windows) libcurl/8.9.1 Schannel zlib/1.3 WinIDN\nRelease-Date: 2024-07-31\n";
assert!(curl_takes_revoke_best_effort(schannel));
assert!(curl_takes_revoke_best_effort("curl 7.70.0 (x86_64-pc-win32) libcurl/7.70.0 Schannel\n"));
// Windows 10 1803 shipped 7.55, which rejects the unknown option.
assert!(!curl_takes_revoke_best_effort("curl 7.55.1 (Windows) libcurl/7.55.1 WinSSL\n"));
// OpenSSL never checks revocation this way; Schannel in parentheses is not the active backend.
assert!(!curl_takes_revoke_best_effort(
"curl 8.10.1 (x86_64-w64-mingw32) libcurl/8.10.1 OpenSSL/3.2.3 (Schannel)\n"
));
assert!(!curl_takes_revoke_best_effort("WARNING: this libcurl is Debug-enabled\n"));
assert!(curl_takes_revoke_best_effort(&format!("WARNING: this libcurl is Debug-enabled\n\n{schannel}")));
}
#[cfg(unix)]
#[test]
fn absent_build_is_empty_but_http_failures_are_not() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let curl = tmp.path().join("curl");
let http = Http { curl: curl.clone(), https_only: false };
let http = Http { curl: curl.clone(), https_only: false, revoke_best_effort: false };
for (code, exit, expected) in [(404, 22, Some("[]")), (200, 0, Some("payload")), (403, 22, None), (0, 7, None)]
{
std::fs::write(&curl, format!("#!/bin/sh\nprintf 'payload\\n{code:03}'\nexit {exit}\n")).unwrap();