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();