Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows configuring TLS backend #386

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 12 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 13 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 = []

Expand Down
18 changes: 18 additions & 0 deletions docs/src/config_updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
60 changes: 51 additions & 9 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)]
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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()?
Expand All @@ -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))
Expand Down Expand Up @@ -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.");
}
}
69 changes: 68 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -291,11 +303,49 @@ 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs the following to allow the correct strings in the config file:

Suggested change
pub enum RawTlsBackend {
#[serde(rename_all = "kebab-case")]
pub enum RawTlsBackend {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

/// 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,
}

Expand All @@ -307,6 +357,22 @@ 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();
// 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::<Vec<String>>().join(", ")
))
};
let updates = raw_config.updates.into();

// Determine directories config. For this, we need to take some
Expand Down Expand Up @@ -368,6 +434,7 @@ impl Config {
style,
display,
updates,
tls_backend,
directories,
})
}
Expand Down
Loading