use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use anyhow::Context as _;
use felix_client::{Client, ClientConfig, ClusterClient};
use rustls::RootCertStore;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use crate::context::Settings;
use crate::error::{Exit, MarkExit, fail};
pub(crate) struct Broker {
pub(crate) cluster: Arc<ClusterClient>,
pub(crate) tenant: String,
pub(crate) namespace: String,
config: ClientConfig,
server_name: String,
}
impl Broker {
pub(crate) async fn connect(settings: &Settings) -> anyhow::Result<Self> {
let addrs = addresses(settings.brokers()?).await?;
let config = client_config(settings)?;
let server_name = server_name(settings);
let cluster = ClusterClient::connect(&addrs, &server_name, config.clone())
.await
.mark(
Exit::Connection,
format!("connect to {}", settings.brokers.join(", ")),
)?;
Ok(Self {
cluster: Arc::new(cluster),
tenant: settings.tenant()?.to_string(),
namespace: settings.namespace.clone(),
config,
server_name,
})
}
pub(crate) async fn client_at(&self, addr: SocketAddr) -> anyhow::Result<Client> {
Client::connect(addr, &self.server_name, self.config.clone())
.await
.mark(Exit::Connection, format!("connect to {addr}"))
}
}
pub(crate) fn client_config(settings: &Settings) -> anyhow::Result<ClientConfig> {
let tenant = settings.tenant()?.to_string();
let token = settings.token()?;
let quinn = quic_tls(settings)?;
let mut config = ClientConfig::from_env_or_yaml(quinn, None)
.mark(Exit::Usage, "read the client configuration")?;
config.auth_tenant_id = Some(tenant);
config.auth_token = Some(token);
Ok(config)
}
pub(crate) fn server_name(settings: &Settings) -> String {
if let Some(name) = &settings.server_name {
return name.clone();
}
settings
.brokers
.first()
.and_then(|broker| broker.rsplit_once(':').map(|(host, _)| host))
.map(|host| host.trim_start_matches('[').trim_end_matches(']'))
.filter(|host| host.parse::<std::net::IpAddr>().is_err() && !host.is_empty())
.unwrap_or("localhost")
.to_string()
}
pub(crate) async fn addresses(brokers: &[String]) -> anyhow::Result<Vec<SocketAddr>> {
let mut addrs = Vec::new();
for broker in brokers {
if !broker.contains(':') {
return Err(fail(
Exit::Usage,
format!("broker address {broker:?} needs a port, as host:port"),
));
}
let resolved = tokio::net::lookup_host(broker.as_str())
.await
.mark(Exit::Connection, format!("resolve {broker}"))?;
addrs.extend(resolved);
}
Ok(addrs)
}
fn quic_tls(settings: &Settings) -> anyhow::Result<quinn::ClientConfig> {
let roots = settings
.ca_file
.as_deref()
.map(root_store)
.transpose()?
.map(Arc::new);
let identity = match (&settings.client_cert_file, &settings.client_key_file) {
(None, None) => None,
(Some(cert), Some(key)) => Some((cert.as_path(), key.as_path())),
_ => {
return Err(fail(
Exit::Usage,
"a client certificate needs both --client-cert-file and --client-key-file",
));
}
};
let Some((cert, key)) = identity else {
return felix_client::quic_client_config(roots, settings.alpn)
.mark(Exit::Usage, "set up TLS");
};
let chain = CertificateDer::pem_file_iter(cert)
.and_then(|certs| certs.collect::<Result<Vec<_>, _>>())
.mark(Exit::Usage, format!("read {}", cert.display()))?;
let key =
PrivateKeyDer::from_pem_file(key).mark(Exit::Usage, format!("read {}", key.display()))?;
let builder = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_protocol_versions(&[&rustls::version::TLS13])
.context("TLS 1.3")?;
let builder = match roots {
Some(roots) => builder.with_root_certificates(roots),
None => {
use rustls_platform_verifier::BuilderVerifierExt;
builder
.with_platform_verifier()
.context("the platform trust store")?
}
};
let mut tls = builder
.with_client_auth_cert(chain, key)
.mark(Exit::Usage, "use the client certificate")?;
if settings.alpn {
tls.alpn_protocols = vec![felix_wire::CLIENT_ALPN.to_vec()];
}
let crypto =
quinn::crypto::rustls::QuicClientConfig::try_from(tls).context("QUIC client TLS")?;
Ok(quinn::ClientConfig::new(Arc::new(crypto)))
}
fn root_store(path: &Path) -> anyhow::Result<RootCertStore> {
let mut roots = RootCertStore::empty();
let certs = CertificateDer::pem_file_iter(path)
.and_then(|certs| certs.collect::<Result<Vec<_>, _>>())
.mark(Exit::Usage, format!("read {}", path.display()))?;
if certs.is_empty() {
return Err(fail(
Exit::Usage,
format!("{} holds no certificates", path.display()),
));
}
for cert in certs {
roots.add(cert).mark(
Exit::Usage,
format!("trust a certificate from {}", path.display()),
)?;
}
Ok(roots)
}
#[cfg(test)]
mod tests;