Files
inbuxa-server/crates/utils/src/http.rs
T
jcoffey-dev a993f9ab01
ci / fork-checks (pull_request) Successful in 47s
ci / build (pull_request) Successful in 5m4s
Write the brand in lowercase where people see it
The name is inbuxa, lowercase, like the wordmark; INBUXA reads as an
acronym. The admin and webmail already changed. Here that's everything
the server shows people: the brand macro behind the protocol greetings,
the HTTP and SCIM realms, the startup banner and the calendar and contact
PRODID; the first-party OAuth client descriptions; the legacy-protocol
refusals; the default calendar and address book names and the SMTP
greeting default, in the code and the schema served to the admin
(checksum regenerated); startup and shutdown events; the User-Agent;
the sign-in and RSVP pages; the service units; the OpenAPI realm; the
crate descriptions and the README, where it's set in bold.

Identifiers that are uppercase for their own reasons stay: INBUXA_*
settings, SUBSPACE_INBUXA. So do code comments and the AGPL 5(a) notice
lines.

Tests follow: the IMAP ID name, the default collection names, the PRODID
in the iTIP fixtures and the CalDAV free-busy expectations, and the e2e
legacy-protocol refusals. The webdav, imap and jmap suites pass, so do
the unit tests of every crate touched, and 73 of 75 SMTP tests; of the
other two, antispam fails on main too, and queue_retry is a timing flake
that passes on its own.
2026-09-22 20:08:10 -07:00

206 lines
6.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use base64::{Engine, engine::general_purpose};
use reqwest::{
Client, ClientBuilder,
header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT},
};
use rustls::{
ClientConfig, DigitallySignedStruct, Error as TlsError, SignatureScheme,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{CryptoProvider, aws_lc_rs},
};
use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
use std::{
str::FromStr,
sync::{Arc, LazyLock},
time::Duration,
};
struct SharedTlsConfigs {
strict: ClientConfig,
strict_http1: ClientConfig,
insecure: ClientConfig,
insecure_http1: ClientConfig,
}
#[derive(Debug)]
struct NoCertificateVerification(Arc<CryptoProvider>);
impl ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.0.signature_verification_algorithms.supported_schemes()
}
}
static SHARED_TLS_CONFIGS: LazyLock<SharedTlsConfigs> = LazyLock::new(|| {
let provider = Arc::new(aws_lc_rs::default_provider());
let verifier = rustls_platform_verifier::Verifier::new(provider.clone())
.expect("Failed to load the platform certificate verifier");
let mut strict = ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.expect("Failed to build the TLS client configuration")
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth();
strict.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let mut insecure = ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.expect("Failed to build the TLS client configuration")
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification(provider)))
.with_no_client_auth();
insecure.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let mut strict_http1 = strict.clone();
strict_http1.alpn_protocols = vec![b"http/1.1".to_vec()];
let mut insecure_http1 = insecure.clone();
insecure_http1.alpn_protocols = vec![b"http/1.1".to_vec()];
SharedTlsConfigs {
strict,
strict_http1,
insecure,
insecure_http1,
}
});
pub fn init_shared_tls_configs() {
LazyLock::force(&SHARED_TLS_CONFIGS);
}
pub fn shared_tls_config(allow_invalid_certs: bool) -> ClientConfig {
if allow_invalid_certs {
SHARED_TLS_CONFIGS.insecure.clone()
} else {
SHARED_TLS_CONFIGS.strict.clone()
}
}
pub fn http_client_builder(allow_invalid_certs: bool) -> ClientBuilder {
Client::builder().use_preconfigured_tls(shared_tls_config(allow_invalid_certs))
}
pub fn http1_client_builder(allow_invalid_certs: bool) -> ClientBuilder {
let tls = if allow_invalid_certs {
SHARED_TLS_CONFIGS.insecure_http1.clone()
} else {
SHARED_TLS_CONFIGS.strict_http1.clone()
};
Client::builder().http1_only().use_preconfigured_tls(tls)
}
pub fn unpooled_http_client(allow_invalid_certs: bool) -> Client {
http_client_builder(allow_invalid_certs)
.pool_max_idle_per_host(0)
.build()
.unwrap_or_default()
}
pub fn build_http_client(
raw_headers: impl IntoIterator<Item = (String, String)>,
username: Option<&str>,
password: Option<&str>,
token: Option<&str>,
content_type: Option<&str>,
timeout: Duration,
allow_invalid_certs: bool,
) -> Result<Client, String> {
let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?;
headers.insert(USER_AGENT, "inbuxa/1.0.0".parse().unwrap()); // types::brand!(); utils does not depend on types
match http_client_builder(allow_invalid_certs)
.connect_timeout(timeout)
.default_headers(headers)
.build()
{
Ok(client) => Ok(client),
Err(err) => Err(format!("Failed to build HTTP client: {}", err)),
}
}
pub fn build_http_headers(
raw_headers: impl IntoIterator<Item = (String, String)>,
username: Option<&str>,
password: Option<&str>,
token: Option<&str>,
content_type: Option<&str>,
) -> Result<HeaderMap, String> {
let mut headers = HeaderMap::new();
if let Some(content_type) = content_type {
headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type).unwrap());
}
for (header, value) in raw_headers
.into_iter()
.map(|(k, v)| {
Ok((
HeaderName::from_str(k.trim())
.map_err(|err| format!("Invalid header {k:?}: {err}",))?,
HeaderValue::from_str(v.trim())
.map_err(|err| format!("Invalid value {v:?}: {err}",))?,
))
})
.collect::<Result<Vec<(HeaderName, HeaderValue)>, String>>()?
{
headers.insert(header, value);
}
if let (Some(name), Some(secret)) = (username, password) {
headers.insert(
AUTHORIZATION,
format!(
"Basic {}",
general_purpose::STANDARD.encode(format!("{}:{}", name, secret))
)
.parse()
.unwrap(),
);
} else if let Some(token) = token {
headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse().unwrap());
}
Ok(headers)
}