use anyhow::{Context, Result};
use std::collections::HashMap;
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChannelCacheKey {
pub address: String,
pub timeout_seconds: u64,
pub tls_config: Option<TlsConfig>,
pub user_agent: String,
}
#[derive(Debug, Clone)]
pub struct GrpcClientConfig {
pub address: String,
pub timeout_seconds: u64,
pub tls_config: Option<TlsConfig>,
pub proto_config: Option<ProtoConfig>,
pub metadata: Option<HashMap<String, String>>,
pub target_service: Option<String>,
pub compression: CompressionMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TlsConfig {
pub ca_cert_path: Option<String>,
pub client_cert_path: Option<String>,
pub client_key_path: Option<String>,
pub server_name: Option<String>,
pub insecure_skip_verify: bool,
}
impl TlsConfig {
pub fn new() -> Self {
Self {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
server_name: None,
insecure_skip_verify: false,
}
}
pub fn with_ca_cert(mut self, path: impl Into<String>) -> Self {
self.ca_cert_path = Some(path.into());
self
}
pub fn with_client_cert(
mut self,
cert_path: impl Into<String>,
key_path: impl Into<String>,
) -> Self {
self.client_cert_path = Some(cert_path.into());
self.client_key_path = Some(key_path.into());
self
}
pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
self.server_name = Some(name.into());
self
}
pub fn with_insecure(mut self) -> Self {
self.insecure_skip_verify = true;
self
}
pub fn build(&self) -> Result<ClientTlsConfig> {
let mut tls = ClientTlsConfig::new();
if let Some(domain) = &self.server_name {
tls = tls.domain_name(domain);
}
if let Some(ca_path) = &self.ca_cert_path {
let ca_pem =
std::fs::read_to_string(ca_path).context("Failed to read CA certificate")?;
tls = tls.ca_certificate(Certificate::from_pem(ca_pem));
}
if let (Some(cert_path), Some(key_path)) = (&self.client_cert_path, &self.client_key_path) {
let cert_pem =
std::fs::read_to_string(cert_path).context("Failed to read client certificate")?;
let key_pem = std::fs::read_to_string(key_path).context("Failed to read client key")?;
tls = tls.identity(Identity::from_pem(cert_pem, key_pem));
}
if self.insecure_skip_verify {
tracing::warn!(
"⚠️ SECURITY WARNING: TLS certificate verification is disabled (insecure_skip_verify=true). \
This is insecure and should only be used for testing or development."
);
eprintln!("⚠️ WARNING: TLS verification disabled - this is insecure!");
}
Ok(tls)
}
pub fn is_enabled(&self) -> bool {
self.ca_cert_path.is_some()
|| self.client_cert_path.is_some()
|| self.client_key_path.is_some()
|| self.server_name.is_some()
|| self.insecure_skip_verify
}
pub fn is_insecure(&self) -> bool {
self.insecure_skip_verify
}
}
impl Default for TlsConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProtoConfig {
pub files: Vec<String>,
pub import_paths: Vec<String>,
pub descriptor: Option<String>,
}
impl ProtoConfig {
pub fn new() -> Self {
Self {
files: Vec::new(),
import_paths: Vec::new(),
descriptor: None,
}
}
pub fn with_file(mut self, file: impl Into<String>) -> Self {
self.files.push(file.into());
self
}
pub fn with_files(mut self, files: Vec<String>) -> Self {
self.files.extend(files);
self
}
pub fn with_import_path(mut self, path: impl Into<String>) -> Self {
self.import_paths.push(path.into());
self
}
pub fn with_descriptor(mut self, path: impl Into<String>) -> Self {
self.descriptor = Some(path.into());
self
}
pub fn is_empty(&self) -> bool {
self.files.is_empty() && self.descriptor.is_none()
}
}
impl Default for ProtoConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompressionMode {
#[default]
None,
Gzip,
}
impl CompressionMode {
pub fn from_env() -> Self {
match std::env::var(crate::config::ENV_GRPCTESTIFY_COMPRESSION)
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str()
{
"gzip" => Self::Gzip,
"" | "none" => Self::None,
_ => Self::None,
}
}
pub fn is_enabled(&self) -> bool {
matches!(self, CompressionMode::Gzip)
}
pub fn to_tonic_encoding(&self) -> Option<tonic::codec::CompressionEncoding> {
match self {
CompressionMode::Gzip => Some(tonic::codec::CompressionEncoding::Gzip),
CompressionMode::None => None,
}
}
}