From 2477206f65a83f7daa9ce9d6d8606a14c14fa713 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sun, 27 Oct 2024 23:10:46 +0100 Subject: [PATCH] Allow configuring updates.tls_backend --- .github/workflows/ci.yml | 7 ++-- Cargo.lock | 17 +++++++--- Cargo.toml | 24 +++++++------ docs/src/config_updates.md | 18 ++++++++++ src/cache.rs | 60 ++++++++++++++++++++++++++++----- src/config.rs | 69 +++++++++++++++++++++++++++++++++++++- src/main.rs | 23 ++++++------- tests/lib.rs | 43 +++++++++++++++++++++++- 8 files changed, 220 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dee3fd87..03ee1771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,11 @@ jobs: toolchain: ${{ matrix.toolchain }} - name: Build with default features run: cargo build - - name: Build with logging and webpki roots - run: cargo build --features logging,webpki-roots --no-default-features + - name: Build with logging and Rustls with webpki roots + run: cargo build --features logging,rustls-with-webpki-roots --no-default-features + - name: Build with native TLS backend + # expects runners have the proper Native SSL library + run: cargo build --features native-tls --no-default-features - name: Run tests run: cargo test -- --test-threads 1 diff --git a/Cargo.lock b/Cargo.lock index 008695ce..6fcfe156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.19" @@ -1153,15 +1159,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1243,9 +1250,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64", "bytes", diff --git a/Cargo.toml b/Cargo.toml index bd0d07ac..010fa08a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ app_dirs = { version = "2", package = "app_dirs2" } clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false } env_logger = { version = "0.11", optional = true } log = "0.4" -reqwest = { version = "0.12.5", features = ["blocking"], default-features = false } +reqwest = { version = "0.12.9", features = ["blocking"], default-features = false } serde = "1.0.21" serde_derive = "1.0.21" toml = "0.8.19" @@ -44,21 +44,23 @@ tempfile = "3.1.0" filetime = "0.2.10" [features] -default = ["native-roots"] +default = ["native-tls", "rustls-with-webpki-roots", "rustls-with-native-roots"] logging = ["env_logger"] -# Reqwest (the HTTP client library) can handle TLS connections in three +# Reqwest (the HTTP client library) can handle TLS connections in four # different modes: # -# - Rustls with native roots -# - Rustls with WebPK roots -# - Native TLS (SChannel on Windows, Secure Transport on macOS and OpenSSL otherwise) +# - Rustls with: +# - native roots +# - WebPK roots +# - Native TLS (SChannel on Windows, Secure Transport on macOS and OpenSSL otherwise) with: +# - native roots +# - WebPK roots (not implemented in tealdeer) # -# Exactly one of the three variants must be selected. By default, Rustls with -# native roots is enabled. -native-roots = ["reqwest/rustls-tls-native-roots"] -webpki-roots = ["reqwest/rustls-tls-webpki-roots"] -native-tls = ["reqwest/native-tls"] +# At least one of variants must be selected. By default, uses native TLS and native roots. +native-tls = ["reqwest/rustls-tls-native-roots-no-provider", "reqwest/rustls-tls-webpki-roots-no-provider", "reqwest/native-tls"] +rustls-with-webpki-roots = ["reqwest/rustls-tls-native-roots-no-provider", "reqwest/rustls-tls-webpki-roots"] +rustls-with-native-roots = ["reqwest/rustls-tls-webpki-roots-no-provider", "reqwest/rustls-tls-native-roots"] ignore-online-tests = [] diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index 2678d4cf..9a97e755 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -32,3 +32,21 @@ fetched from the latest `tldr-pages/tldr` GitHub release. [updates] archive_source = https://my-company.example.com/tldr/ +### `tls_backend` + +An advance option. Specifies which TLS backend to use. Only modify this if you encounter certificate errors. + +Available options: +- `rusttls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots +- `rusttls-with-webpki-roots` - Rustls with [WebPKI][rustls-webpki] roots +- `native-tls` - Native TLS + - SChannel on Windows + - Secure Transport on macOS + - OpenSSL on other platforms + + [updates] + tls_backend = "native-tls" + + +[rustls]: https://github.com/rustls/rustls +[rustls-webpki]: https://github.com/rustls/webpki diff --git a/src/cache.rs b/src/cache.rs index 9d270112..a72d0e0a 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -13,7 +13,7 @@ use reqwest::{blocking::Client, Proxy}; use walkdir::{DirEntry, WalkDir}; use zip::ZipArchive; -use crate::{types::PlatformType, utils::print_warning}; +use crate::{config::TlsBackend, types::PlatformType, utils::print_warning}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; @@ -22,6 +22,7 @@ static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; pub struct Cache { cache_dir: PathBuf, enable_styles: bool, + tls_backend: TlsBackend, } #[derive(Debug)] @@ -86,13 +87,14 @@ pub enum CacheFreshness { } impl Cache { - pub fn new

(cache_dir: P, enable_styles: bool) -> Self + pub fn new

(cache_dir: P, enable_styles: bool, tls_backend: TlsBackend) -> Self where P: Into, { Self { cache_dir: cache_dir.into(), enable_styles, + tls_backend, } } @@ -135,9 +137,28 @@ impl Cache { self.cache_dir.join(TLDR_PAGES_DIR) } - /// Download the archive from the specified URL. - fn download(archive_url: &str) -> Result> { + fn build_client(tls_backend: TlsBackend) -> Result { let mut builder = Client::builder(); + builder = match tls_backend { + #[cfg(feature = "native-tls")] + TlsBackend::NativeTls => builder + .use_native_tls() + .tls_built_in_root_certs(true) + .tls_built_in_webpki_certs(false) + .tls_built_in_native_certs(false), + #[cfg(feature = "rustls-with-webpki-roots")] + TlsBackend::RustlsWithWebpkiRoots => builder + .use_rustls_tls() + .tls_built_in_root_certs(false) + .tls_built_in_webpki_certs(true) + .tls_built_in_native_certs(false), + #[cfg(feature = "rustls-with-native-roots")] + TlsBackend::RustlsWithNativeRoots => builder + .use_rustls_tls() + .tls_built_in_root_certs(false) + .tls_built_in_webpki_certs(false) + .tls_built_in_native_certs(true), + }; if let Ok(ref host) = env::var("HTTP_PROXY") { if let Ok(proxy) = Proxy::http(host) { builder = builder.proxy(proxy); @@ -148,9 +169,11 @@ impl Cache { builder = builder.proxy(proxy); } } - let client = builder - .build() - .context("Could not instantiate HTTP client")?; + builder.build().context("Could not instantiate HTTP client") + } + + /// Download the archive from the specified URL. + fn download(client: &Client, archive_url: &str) -> Result> { let mut resp = client .get(archive_url) .send()? @@ -166,10 +189,11 @@ impl Cache { pub fn update(&self, archive_source: &str) -> Result<()> { self.ensure_cache_dir_exists()?; - let archive_url = format!("{}/tldr.zip", archive_source); + let archive_url = format!("{archive_source}/tldr.zip"); + let client = Self::build_client(self.tls_backend)?; // First, download the compressed data - let bytes: Vec = Self::download(&archive_url)?; + let bytes: Vec = Self::download(&client, &archive_url)?; // Decompress the response body into an `Archive` let mut archive = ZipArchive::new(Cursor::new(bytes)) @@ -505,4 +529,22 @@ mod tests { assert_eq!(&buf, b"Hello\n"); } + + #[test] + #[cfg(feature = "native-tls")] + fn test_create_https_client_with_native_tls() { + Cache::build_client(TlsBackend::NativeTls).expect("fails to build a client."); + } + + #[test] + #[cfg(feature = "rustls-with-webpki-roots")] + fn test_create_https_client_with_rustls() { + Cache::build_client(TlsBackend::RustlsWithWebpkiRoots).expect("fails to build a client."); + } + + #[test] + #[cfg(feature = "rustls-with-native-roots")] + fn test_create_https_client_with_rustls_with_native_roots() { + Cache::build_client(TlsBackend::RustlsWithNativeRoots).expect("fails to build a client."); + } } diff --git a/src/config.rs b/src/config.rs index de868179..231ea3e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,9 +5,10 @@ use std::{ time::Duration, }; -use anyhow::{bail, ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use app_dirs::{get_app_root, AppDataType}; use log::debug; +use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; use yansi::{Color, Style}; @@ -16,6 +17,14 @@ use crate::types::PathSource; pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days +const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[ + #[cfg(feature = "native-tls")] + RawTlsBackend::NativeTls, + #[cfg(feature = "rustls-with-webpki-roots")] + RawTlsBackend::RustlsWithWebpkiRoots, + #[cfg(feature = "rustls-with-native-roots")] + RawTlsBackend::RustlsWithNativeRoots, +]; fn default_underline() -> bool { false @@ -178,6 +187,8 @@ struct RawUpdatesConfig { pub auto_update_interval_hours: u64, #[serde(default = "default_archive_source")] pub archive_source: String, + #[serde(default)] + pub tls_backend: RawTlsBackend, } impl Default for RawUpdatesConfig { @@ -186,6 +197,7 @@ impl Default for RawUpdatesConfig { auto_update: false, auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, archive_source: default_archive_source(), + tls_backend: RawTlsBackend::default(), } } } @@ -291,11 +303,49 @@ pub struct DirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum RawTlsBackend { + /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) + NativeTls, + /// Rustls with `WebPKI` roots. + RustlsWithWebpkiRoots, + /// Rustls with native roots. + RustlsWithNativeRoots, +} + +impl Default for RawTlsBackend { + fn default() -> Self { + *SUPPORTED_TLS_BACKENDS.first().unwrap() + } +} + +impl std::fmt::Display for RawTlsBackend { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.serialize(f) + } +} + +/// Allows choosing a `reqwest`'s TLS backend. Available TLS backends: +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TlsBackend { + /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) + #[cfg(feature = "native-tls")] + NativeTls, + /// Rustls with `WebPKI` roots. + #[cfg(feature = "rustls-with-webpki-roots")] + RustlsWithWebpkiRoots, + /// Rustls with native roots. + #[cfg(feature = "rustls-with-native-roots")] + RustlsWithNativeRoots, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config { pub style: StyleConfig, pub display: DisplayConfig, pub updates: UpdatesConfig, + pub tls_backend: TlsBackend, pub directories: DirectoriesConfig, } @@ -307,6 +357,22 @@ impl Config { fn from_raw(raw_config: RawConfig, relative_path_root: &Path) -> Result { let style = raw_config.style.into(); let display = raw_config.display.into(); + // consumes early to prevent the move violation because `tls_backend` is not in `UpdatesConfig` but as a separate config + let tls_backend = match raw_config.updates.tls_backend { + #[cfg(feature = "native-tls")] + RawTlsBackend::NativeTls => TlsBackend::NativeTls, + #[cfg(feature = "rustls-with-webpki-roots")] + RawTlsBackend::RustlsWithWebpkiRoots => TlsBackend::RustlsWithWebpkiRoots, + #[cfg(feature = "rustls-with-native-roots")] + RawTlsBackend::RustlsWithNativeRoots => TlsBackend::RustlsWithNativeRoots, + // when compiling without all TLS backend features, we want to handle config error. + #[allow(unreachable_patterns)] + _ => return Err(anyhow!( + "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", + raw_config.updates.tls_backend, + SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") + )) + }; let updates = raw_config.updates.into(); // Determine directories config. For this, we need to take some @@ -368,6 +434,7 @@ impl Config { style, display, updates, + tls_backend, directories, }) } diff --git a/src/main.rs b/src/main.rs index 32b693b5..19a132f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,18 +16,13 @@ #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] -#[cfg(any( - all(feature = "native-roots", feature = "webpki-roots"), - all(feature = "native-roots", feature = "native-tls"), - all(feature = "webpki-roots", feature = "native-tls"), - not(any( - feature = "native-roots", - feature = "webpki-roots", - feature = "native-tls" - )), -))] +#[cfg(not(any( + feature = "native-tls", + feature = "rustls-with-webpki-roots", + feature = "rustls-with-native-roots", +)))] compile_error!( - "exactly one of the features \"native-roots\", \"webpki-roots\" or \"native-tls\" must be enabled" + "at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled" ); use std::{ @@ -295,7 +290,11 @@ fn main() { } // Instantiate cache. This will not yet create the cache directory! - let cache = Cache::new(&config.directories.cache_dir.path, enable_styles); + let cache = Cache::new( + &config.directories.cache_dir.path, + enable_styles, + config.tls_backend, + ); // Clear cache, pass through if args.clear_cache { diff --git a/tests/lib.rs b/tests/lib.rs index 63edca6c..606424e4 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -220,7 +220,31 @@ fn test_update_cache_default_features() { fn test_update_cache_rustls_webpki() { let testenv = TestEnv::new() .no_default_features() - .with_feature("webpki-roots"); + .with_feature("rustls-with-webpki-roots"); + + testenv + .command() + .args(["sl"]) + .assert() + .failure() + .stderr(contains("Page cache not found. Please run `tldr --update`")); + + testenv + .command() + .args(["--update"]) + .assert() + .success() + .stderr(contains("Successfully updated cache.")); + + testenv.command().args(["sl"]).assert().success(); +} + +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] +#[test] +fn test_update_cache_native_tls() { + let testenv = TestEnv::new() + .no_default_features() + .with_feature("rustls-with-native-roots"); testenv .command() @@ -258,6 +282,23 @@ fn test_quiet_cache() { .stdout(is_empty()); } +#[test] +fn test_warn_invalid_tls_backend() { + let testenv = TestEnv::new() + .no_default_features() + .with_feature("rustls-with-webpki-roots") + .remove_initial_config(); + + testenv.append_to_config("updates.tls_backend = 'invalid-tls-backend'\n"); + + testenv + .command() + .args(["sl"]) + .assert() + .failure() + .stderr(contains("unknown variant `invalid-tls-backend`, expected one of `native-tls`, `rustls-with-webpki-roots`, `rustls-with-native-roots`")); +} + #[test] fn test_quiet_failures() { let testenv = TestEnv::new().install_default_cache();