Skip to content
Merged
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
88 changes: 63 additions & 25 deletions Cargo.lock

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

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ tower-http = { version = "0.6", features = [
] }

# ── HTTP client ────────────────────────────────────────────────────────────────
reqwest = { version = "0.12", default-features = false, features = [
reqwest = { version = "0.13", default-features = false, features = [
"form",
"json",
"rustls-tls",
"query",
"rustls-no-provider",
"stream",
] }

Expand Down Expand Up @@ -225,6 +227,7 @@ regex = "1"
# ── Test ───────────────────────────────────────────────────────────────────────
rstest = "0.26"
tempfile = "3"
ctor = "1.0"

[profile.dev.package."*"]
opt-level = 2 # WHY: optimize deps for faster runtime during development
Expand Down
8 changes: 8 additions & 0 deletions crates/archon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ async fn main() {
// no thread holds it across an await point.
let mut stdout = std::io::stdout();

// WHY: reqwest builds with `rustls-no-provider` (fleet convention:
// install the ring crypto provider once, explicitly, process-wide —
// never let a library link one implicitly). install_default returns Err
// if a provider is already installed (e.g. a dependency called it
// first); that is harmless.
// kanon:ignore RUST/no-silent-result-swallow — install_default returns Err when provider already installed by dependency; harmless
let _ = rustls::crypto::ring::default_provider().install_default();

let result = match cli.command {
Command::Serve(args) => serve::run_serve(args, &mut stdout).await,
Command::Db(db_args) => match db_args.command {
Expand Down
26 changes: 22 additions & 4 deletions crates/archon/src/render/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,28 @@ pub fn build_client_config(server_fingerprint: &str) -> Result<quinn::ClientConf

// WHY: .dangerous() only swaps the WebPKI verifier for the pinning verifier
// below; the pin plus real handshake-signature checks carry the trust.
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(PinnedFingerprintVerifier::new(expected)))
.with_no_client_auth();
//
// WHY builder_with_provider, not the plain builder(): the plain builder()
// asks rustls to auto-select a CryptoProvider from crate features, valid
// only when EXACTLY ONE of "ring"/"aws-lc-rs" is active for the whole
// binary. Nothing pulls aws-lc-rs into this workspace's build today, so
// the plain builder() happens to resolve unambiguously right now — but
// that is an accident of the current dependency graph, not a guarantee:
// librqbit 9 (landing next) forwards its `default` feature to
// `reqwest/default-tls`, which in reqwest 0.13 means `rustls`, which
// pulls `__rustls-aws-lc-rs` — the exact combination that made this same
// call panic on the librqbit-9 branch. This crate's own fleet convention
// (see main.rs's install_default call) is explicit provider selection,
// never implicit — so pin `ring` here explicitly rather than let this
// call keep working by chance until the next dependency bump breaks it.
let crypto = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("ring's default provider supports rustls's default TLS versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(PinnedFingerprintVerifier::new(expected)))
.with_no_client_auth();

let quic_config = quinn::crypto::rustls::QuicClientConfig::try_from(crypto).map_err(|e| {
RenderError::Tls {
Expand Down
46 changes: 45 additions & 1 deletion crates/archon/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1826,10 +1826,23 @@ async fn start_feed_scheduler(
// configured but unenforced — a stalled feed host could block
// `response.chunk().await` forever inside `komide::fetch::fetch_feed`,
// wedging that feed's poll task.
//
// WHY install_default here too (not just main.rs): reqwest builds with
// `rustls-no-provider` (fleet convention: install explicitly, never
// implicitly), and this function's own rebuild_supervisor_tests call
// sites (4 of them) construct a real client in a nextest process that
// never runs main(). Harmless in production — main() already installed
// it, and install_default() on an already-installed process just
// returns Err, discarded here.
let _ = rustls::crypto::ring::default_provider().install_default();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(config.fetch_timeout_secs))
.build()
.unwrap_or_default(); // WHY: reqwest::Client::default() is a valid fallback; build fails only with invalid TLS config
// WHY: unwrap_or_default() only catches a genuinely-invalid TLS
// config (an Err) — it can't catch rustls-no-provider's panic!, but
// install_default() above already ran unconditionally before this
// build(), so that panic path is unreachable here.
.unwrap_or_default();
let service = Arc::new(FeedSchedulerService::new(
clone_db_pools(db),
event_tx.clone(),
Expand Down Expand Up @@ -2703,6 +2716,11 @@ mod service_adapter_tests {

#[tokio::test]
async fn metadata_adapter_calls_live_epignosis_resolver() {
// WHY: ProviderBackedResolver::new builds real provider clients
// (epignosis's own reqwest::Client::builder()), which eagerly
// builds a TLS connector; see spawn_supervisor's WHY note above for
// the mechanism.
let _ = rustls::crypto::ring::default_provider().install_default();
let adapter = MetadataAdapter::new(Arc::new(ProviderBackedResolver::new(
horismos::EpignosisConfig::default(),
ProviderCredentials::default(),
Expand Down Expand Up @@ -3222,6 +3240,10 @@ mod search_adapter_tests {

#[tokio::test]
async fn search_adapter_calls_live_zetesis_service() {
// WHY: SearchIndexerService::new builds a real reqwest client
// (eksetasis's own build_http_client); see spawn_supervisor's WHY
// note above for the mechanism.
let _ = rustls::crypto::ring::default_provider().install_default();
let pool = SqlitePool::connect("sqlite::memory:")
.await
.expect("in-memory sqlite opens");
Expand Down Expand Up @@ -3573,6 +3595,11 @@ mod tests {
async fn build_cf_proxy_enabled_posts_to_byparr_endpoint() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};

// WHY: build_cf_proxy below constructs a real ByparrProxy, which
// eagerly builds a TLS connector; see spawn_supervisor's WHY note
// above for the mechanism.
let _ = rustls::crypto::ring::default_provider().install_default();

// One-shot Byparr stub: answers a single POST /v1 with a solved page.
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
Expand Down Expand Up @@ -3806,6 +3833,14 @@ mod http_supervisor_tests {
/// with the live config's `(listen_addr, port)` matching the bound
/// address so rebind-target tracking reflects the harness's listener.
async fn spawn_supervisor() -> HttpHarness {
// WHY: this harness's tests drive the real server via reqwest::get,
// which eagerly builds a TLS connector and panics with no provider
// installed — archon builds reqwest with `rustls-no-provider` (fleet
// convention: install explicitly, never implicitly — see main.rs),
// and this nextest test binary never runs main(). Safe to call
// repeatedly: install_default() on an already-installed process
// just returns Err, discarded here.
let _ = rustls::crypto::ring::default_provider().install_default();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind startup listener");
Expand Down Expand Up @@ -4440,6 +4475,10 @@ mod rebuild_supervisor_tests {
/// and shuts down cleanly on process shutdown.
#[tokio::test]
async fn epignosis_supervisor_rebuilds_resolver_on_config_change() {
// WHY: ProviderBackedResolver::new below builds real provider
// clients (epignosis's own reqwest::Client::builder()); see
// spawn_supervisor's WHY note above for the mechanism.
let _ = rustls::crypto::ring::default_provider().install_default();
let config = epignosis_test_config();
let (manager, handle) = ConfigManager::new(
config.clone(),
Expand Down Expand Up @@ -4501,6 +4540,7 @@ mod rebuild_supervisor_tests {
/// `configured_api_key_reaches_lookup_request` (providers/acoustid.rs).
#[tokio::test]
async fn epignosis_supervisor_rebuilds_resolver_on_credential_change() {
let _ = rustls::crypto::ring::default_provider().install_default();
let config = epignosis_test_config();
let (manager, handle) = ConfigManager::new(
config.clone(),
Expand Down Expand Up @@ -4590,6 +4630,10 @@ mod rebuild_supervisor_tests {
/// the staleness threshold itself comes from the live config section.
#[tokio::test]
async fn zetesis_supervisor_tick_refreshes_stale_caps_and_skips_fresh() {
// WHY: SearchIndexerService::new below builds a real reqwest client
// (eksetasis's own build_http_client); see spawn_supervisor's WHY
// note above for the mechanism.
let _ = rustls::crypto::ring::default_provider().install_default();
let pool = sqlx::SqlitePool::connect("sqlite::memory:")
.await
.expect("in-memory sqlite");
Expand Down
8 changes: 8 additions & 0 deletions crates/archon/tests/config_reload_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ async fn wait_for_opds_page_size(

#[tokio::test(flavor = "multi_thread")]
async fn sighup_reload_applies_live_rotates_jwt_and_holds_back_restart_class() {
// WHY: this test's reqwest::Client::new() below eagerly builds its TLS
// connector, and archon builds reqwest with `rustls-no-provider` (fleet
// convention: install explicitly, never implicitly — see main.rs). This
// integration test never runs archon's main() in THIS process (it spawns
// a separate `harmonia serve` subprocess instead), so nothing else in
// this process ever installs a provider.
let _ = rustls::crypto::ring::default_provider().install_default();

let workdir = tempfile::tempdir().expect("create tempdir");
let download_dir = workdir.path().join("downloads");
std::fs::create_dir_all(&download_dir).expect("create download dir");
Expand Down
Loading