use std::net::SocketAddr;
use std::time::Duration;
use delta_kernel::object_store::Result as ObjectStoreResult;
use reqwest::Client;
#[cfg(any(feature = "rustls", feature = "native-tls"))]
use reqwest::{Certificate, Identity};
use super::generic_error;
#[derive(Debug, Clone, Default)]
pub struct RestClientOptions {
pub cert_path: Option<String>,
pub key_path: Option<String>,
pub ca_path: Option<String>,
pub allow_invalid_hostnames: bool,
pub dns_overrides: Vec<String>,
pub timeout_secs: Option<u64>,
}
#[cfg(feature = "rustls")]
fn build_identity(cert: &str, key: &str) -> ObjectStoreResult<Identity> {
let mut bundle = std::fs::read(cert).map_err(generic_error)?;
bundle.extend_from_slice(&std::fs::read(key).map_err(generic_error)?);
Identity::from_pem(&bundle).map_err(generic_error)
}
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
fn build_identity(cert: &str, key: &str) -> ObjectStoreResult<Identity> {
let cert_pem = std::fs::read(cert).map_err(generic_error)?;
let key_pem = std::fs::read(key).map_err(generic_error)?;
Identity::from_pkcs8_pem(&cert_pem, &key_pem).map_err(generic_error)
}
#[cfg(any(feature = "rustls", feature = "native-tls"))]
fn configure_mtls(
builder: reqwest::ClientBuilder,
cert: &str,
key: &str,
ca: &str,
) -> ObjectStoreResult<reqwest::ClientBuilder> {
let identity = build_identity(cert, key)?;
let ca_cert =
Certificate::from_pem(&std::fs::read(ca).map_err(generic_error)?).map_err(generic_error)?;
Ok(builder.identity(identity).add_root_certificate(ca_cert))
}
#[cfg(not(any(feature = "rustls", feature = "native-tls")))]
fn configure_mtls(
_builder: reqwest::ClientBuilder,
_cert: &str,
_key: &str,
_ca: &str,
) -> ObjectStoreResult<reqwest::ClientBuilder> {
Err(generic_error(
"mTLS requires the `rustls` or `native-tls` feature to be enabled",
))
}
pub fn build_rest_client(opts: &RestClientOptions) -> ObjectStoreResult<Client> {
let mut builder = Client::builder();
#[cfg(feature = "rustls")]
{
builder = builder.use_rustls_tls();
if opts.allow_invalid_hostnames {
let roots = hostname_bypass_roots(
rustls_native_certs::load_native_certs(),
opts.ca_path.is_some(),
)?;
builder = builder.tls_certs_only(roots);
}
}
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
{
builder = builder.use_native_tls();
}
#[cfg(any(feature = "rustls", feature = "native-tls"))]
{
builder = builder.tls_danger_accept_invalid_hostnames(opts.allow_invalid_hostnames);
}
#[cfg(not(any(feature = "rustls", feature = "native-tls")))]
if opts.allow_invalid_hostnames {
return Err(generic_error(
"allow_invalid_hostnames requires the `rustls` or `native-tls` feature to be enabled",
));
}
if let Some(secs) = opts.timeout_secs {
builder = builder.timeout(Duration::from_secs(secs));
}
match (&opts.cert_path, &opts.key_path, &opts.ca_path) {
(Some(cert), Some(key), Some(ca)) => {
builder = configure_mtls(builder, cert, key, ca)?;
}
(None, None, None) => {}
_ => {
return Err(generic_error(
"partial mTLS config: cert_path, key_path, and ca_path must be set \
together (or all omitted)",
));
}
}
for entry in &opts.dns_overrides {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (host, addr) = entry.split_once('=').ok_or_else(|| {
generic_error(format!(
"invalid DNS override (expected `host=ip:port`): {entry}"
))
})?;
let addr: SocketAddr = addr.parse().map_err(generic_error)?;
builder = builder.resolve(host, addr);
}
builder.build().map_err(generic_error)
}
#[cfg(feature = "rustls")]
fn hostname_bypass_roots(
roots: rustls_native_certs::CertificateResult,
has_ca_certificate: bool,
) -> ObjectStoreResult<Vec<Certificate>> {
for error in &roots.errors {
tracing::warn!(%error, "failed to load a system TLS root certificate");
}
if roots.certs.is_empty() && !has_ca_certificate {
return Err(generic_error(format!(
"cannot disable hostname verification without trusted CA certificates; \
system root loading errors: {:?}",
roots.errors
)));
}
roots
.certs
.iter()
.map(|cert| Certificate::from_der(cert.as_ref()))
.collect::<Result<Vec<_>, _>>()
.map_err(generic_error)
}
#[cfg(all(test, feature = "rustls"))]
mod tests {
use rstest::rstest;
use rustls_native_certs::{CertificateResult, Error, ErrorKind};
use super::hostname_bypass_roots;
#[rstest]
fn hostname_bypass_without_system_roots_requires_ca_certificate(
#[values(false, true)] has_ca_certificate: bool,
#[values(false, true)] load_error: bool,
) {
let mut roots = CertificateResult::default();
if load_error {
roots.errors.push(Error {
context: "cannot read test root store",
kind: ErrorKind::Os(
std::io::Error::from(std::io::ErrorKind::PermissionDenied).into(),
),
});
}
let result = hostname_bypass_roots(roots, has_ca_certificate);
if has_ca_certificate {
assert!(result.unwrap().is_empty());
} else {
let error = result.unwrap_err().to_string();
assert!(error.contains("without trusted CA certificates"));
if load_error {
assert!(error.contains("cannot read test root store"));
assert!(error.contains("PermissionDenied"));
}
}
}
}