Allow configuring TLS backend (#386)

This commit is contained in:
Erick Guan 2025-02-26 00:27:44 +01:00 committed by GitHub
commit 09ef7f534e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 228 additions and 47 deletions

View file

@ -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

17
Cargo.lock generated
View file

@ -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",

View file

@ -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 = []

View file

@ -32,3 +32,21 @@ fetched from the latest `tldr-pages/tldr` GitHub release.
[updates]
archive_source = https://my-company.example.com/tldr/
### `tls_backend`
Specifies which TLS backend to use. Try changing this setting if you encounter certificate errors.
Available options:
- `rustls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots
- `rustls-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

View file

@ -48,7 +48,7 @@ Release build:
Release build with bundled CA roots:
$ cargo build --release --no-default-features --features webpki-roots
$ cargo build --release --no-default-features --features rustls-with-webpki-roots
Debug build with logging support:

View file

@ -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<P>(cache_dir: P, enable_styles: bool) -> Self
pub fn new<P>(cache_dir: P, enable_styles: bool, tls_backend: TlsBackend) -> Self
where
P: Into<PathBuf>,
{
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<Vec<u8>> {
fn build_client(tls_backend: TlsBackend) -> Result<Client> {
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<Vec<u8>> {
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<u8> = Self::download(&archive_url)?;
let bytes: Vec<u8> = 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.");
}
}

View file

@ -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,19 +197,39 @@ impl Default for RawUpdatesConfig {
auto_update: false,
auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS,
archive_source: default_archive_source(),
tls_backend: RawTlsBackend::default(),
}
}
}
impl From<RawUpdatesConfig> for UpdatesConfig {
fn from(raw_updates_config: RawUpdatesConfig) -> Self {
Self {
impl TryFrom<RawUpdatesConfig> for UpdatesConfig {
type Error = anyhow::Error;
fn try_from(raw_updates_config: RawUpdatesConfig) -> Result<Self> {
let tls_backend = match raw_updates_config.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_updates_config.tls_backend,
SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::<Vec<String>>().join(", ")
))
};
Ok(Self {
auto_update: raw_updates_config.auto_update,
auto_update_interval: Duration::from_secs(
raw_updates_config.auto_update_interval_hours * 3600,
),
archive_source: raw_updates_config.archive_source,
}
tls_backend,
})
}
}
@ -265,6 +296,7 @@ pub struct UpdatesConfig {
pub auto_update: bool,
pub auto_update_interval: Duration,
pub archive_source: String,
pub tls_backend: TlsBackend,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -291,6 +323,43 @@ pub struct DirectoriesConfig {
pub custom_pages_dir: Option<PathWithSource>,
}
#[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,
@ -307,7 +376,7 @@ impl Config {
fn from_raw(raw_config: RawConfig, relative_path_root: &Path) -> Result<Self> {
let style = raw_config.style.into();
let display = raw_config.display.into();
let updates = raw_config.updates.into();
let updates = raw_config.updates.try_into()?;
// Determine directories config. For this, we need to take some
// additional factory into account, like env variables, or the

View file

@ -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.updates.tls_backend,
);
// Clear cache, pass through
if args.clear_cache {

View file

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