diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ea87171..99956b001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -437,3 +437,7 @@ - The formatter now properly indents multiline trailing comments inside of multiline lists and tuples. ([0xda157](https://github.com/0xda157)) + +- Fixed a bug where the compiler would panic on the first HTTPS request on + Android. + ([John Downey](https://github.com/jtdowney)) diff --git a/Cargo.lock b/Cargo.lock index 2faabd045..f46f48f7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1189,6 +1189,7 @@ dependencies = [ "regex", "reqwest", "rpassword", + "rustls-native-certs", "same-file", "serde", "serde_json", @@ -2903,9 +2904,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", diff --git a/compiler-cli/Cargo.toml b/compiler-cli/Cargo.toml index 2e0f854f0..74bc2c8e4 100644 --- a/compiler-cli/Cargo.toml +++ b/compiler-cli/Cargo.toml @@ -26,6 +26,8 @@ fs_extra = "1" tracing-subscriber = { version = "0.3.23", features = ["fmt", "env-filter"] } # HTTP client reqwest = { version = "^0.13", default-features = false, features = ["rustls"] } +# System trust store loading +rustls-native-certs = "0.8" # Checksums sha2 = "0" # Getting hostname diff --git a/compiler-cli/src/http.rs b/compiler-cli/src/http.rs index cc8c88265..66a9bc171 100644 --- a/compiler-cli/src/http.rs +++ b/compiler-cli/src/http.rs @@ -60,32 +60,67 @@ fn init_client() -> Result<&'static Client, Error> { return Ok(client); } - let certificate_path = match std::env::var("GLEAM_CACERTS_PATH") { - Ok(path) => { - tracing::trace!("Using GLEAM_CACERTS_PATH environment variable"); - path - } - Err(_) => { - return Ok(REQWEST_CLIENT.get_or_init(|| { - Client::builder() - .build() - .expect("Failed to create reqwest client") - })); + let client = build_client()?; + Ok(REQWEST_CLIENT.get_or_init(|| client)) +} + +/// Build the HTTP client with an appropriate certificate trust store. +/// +/// On most platforms reqwest's default `rustls` configuration uses +/// `rustls-platform-verifier`, which delegates to the operating system's trust +/// manager. This respects OS-installed and enterprise root certificates along +/// with the system's own trust decisions. +/// +/// On Android that verifier reaches the trust manager by calling into the JVM, +/// and without it panics on the first request (see issue #5823). There we read +/// the same system trust store from the filesystem and configure those roots +/// explicitly instead. +fn build_client() -> Result { + if cfg!(target_os = "android") { + let mut certificates = system_certificates(); + if let Ok(certificate_path) = std::env::var("GLEAM_CACERTS_PATH") { + let certificate = read_certificate(&certificate_path)?; + certificates.push(certificate); } - }; - let certificate_bytes = fs::read_bytes(&certificate_path)?; - let certificate = Certificate::from_pem(&certificate_bytes).map_err(|error| Error::FileIo { - kind: FileKind::File, - action: FileIoAction::Parse, - path: Utf8PathBuf::from(&certificate_path), - err: Some(error.to_string()), - })?; + Client::builder() + .tls_certs_only(certificates) + .build() + .map_err(Error::http) + } else { + let Ok(certificate_path) = std::env::var("GLEAM_CACERTS_PATH") else { + return Client::builder().build().map_err(Error::http); + }; - Ok(REQWEST_CLIENT.get_or_init(|| { + tracing::trace!("Using GLEAM_CACERTS_PATH environment variable"); + let certificate = read_certificate(&certificate_path)?; Client::builder() .add_root_certificate(certificate) .build() - .expect("Failed to create reqwest client") - })) + .map_err(Error::http) + } +} + +fn read_certificate(path: &str) -> Result { + let bytes = fs::read_bytes(path)?; + Certificate::from_pem(&bytes).map_err(|error| Error::FileIo { + kind: FileKind::File, + action: FileIoAction::Parse, + path: Utf8PathBuf::from(path), + err: Some(error.to_string()), + }) +} + +/// Load the system trust store (only used on Android) +fn system_certificates() -> Vec { + let loaded = rustls_native_certs::load_native_certs(); + for error in &loaded.errors { + tracing::warn!("Failed to load a system certificate: {error}"); + } + + loaded + .certs + .iter() + .filter_map(|certificate| Certificate::from_der(certificate.as_ref()).ok()) + .collect() }