use crate::error::ConfigError;
use crate::providers::provider::{ConfigProvider, ProviderMetadata, ProviderType};
#[cfg(feature = "encryption")]
use crate::security::{SecureString, SensitivityLevel};
use crate::utils::file_format::parse_content;
use crate::utils::ssrf::validate_remote_url;
use crate::watcher::TlsConfig;
use figment::Figment;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct HttpConfigProvider {
url: String,
auth: Option<HttpAuth>,
tls_config: Option<TlsConfig>,
timeout: Option<String>,
priority: u8,
failure_threshold: u32,
reset_timeout: Duration,
}
#[cfg(feature = "encryption")]
#[derive(Clone)]
pub(crate) struct HttpAuth {
pub(crate) username: String,
pub(crate) password: Option<Arc<SecureString>>,
pub(crate) bearer_token: Option<Arc<SecureString>>,
}
#[cfg(feature = "encryption")]
impl HttpConfigProvider {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
auth: None,
tls_config: None,
timeout: None,
priority: 30,
failure_threshold: 3,
reset_timeout: Duration::from_secs(30),
}
}
pub fn from_url(url: impl Into<String>) -> Self {
Self::new(url)
}
pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
self.auth = Some(HttpAuth {
username: username.into(),
password: Some(Arc::new(SecureString::new(
password.into(),
SensitivityLevel::Critical,
))),
bearer_token: None,
});
self
}
pub fn with_auth_secure(
mut self,
username: impl Into<String>,
password: Arc<SecureString>,
) -> Self {
self.auth = Some(HttpAuth {
username: username.into(),
password: Some(password),
bearer_token: None,
});
self
}
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
self.auth = Some(HttpAuth {
username: String::new(),
password: None,
bearer_token: Some(Arc::new(SecureString::new(
token.into(),
SensitivityLevel::High,
))),
});
self
}
pub fn with_bearer_token_secure(mut self, token: Arc<SecureString>) -> Self {
self.auth = Some(HttpAuth {
username: String::new(),
password: None,
bearer_token: Some(token),
});
self
}
pub fn with_tls(
mut self,
ca_cert: impl Into<PathBuf>,
client_cert: Option<impl Into<PathBuf>>,
client_key: Option<impl Into<PathBuf>>,
) -> Self {
let ca_cert: PathBuf = ca_cert.into();
let mut tls = TlsConfig::new().with_ca_cert(&ca_cert);
if let Some(cert) = client_cert {
tls = tls.with_client_cert(cert);
}
if let Some(key) = client_key {
tls = tls.with_client_key(key);
}
self.tls_config = Some(tls);
self
}
pub fn with_timeout(mut self, timeout: impl Into<String>) -> Self {
self.timeout = Some(timeout.into());
self
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold;
self
}
pub fn with_reset_timeout(mut self, timeout: Duration) -> Self {
self.reset_timeout = timeout;
self
}
fn build_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
let mut builder =
reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(30));
if let Some(tls) = &self.tls_config {
if let Some(ca_path) = tls.ca_cert_path() {
let cert_data = std::fs::read(ca_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read CA cert: {}", e))
})?;
let cert = reqwest::Certificate::from_pem(&cert_data).map_err(|e| {
ConfigError::RemoteError(format!("Failed to parse CA cert: {}", e))
})?;
builder = builder.add_root_certificate(cert);
}
if let (Some(cert_path), Some(key_path)) =
(tls.client_cert_path(), tls.client_key_path())
{
let cert_data = std::fs::read(cert_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read client cert: {}", e))
})?;
let key_data = std::fs::read(key_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read client key: {}", e))
})?;
let mut combined = cert_data;
combined.extend_from_slice(b"\n");
combined.extend_from_slice(&key_data);
let identity = reqwest::Identity::from_pem(&combined).map_err(|e| {
ConfigError::RemoteError(format!("Failed to parse client identity: {}", e))
})?;
builder = builder.identity(identity);
}
}
builder
.build()
.map_err(|e| ConfigError::RemoteError(format!("Failed to create HTTP client: {}", e)))
}
fn parse_response(&self, content_type: &str, response: &str) -> Result<Figment, ConfigError> {
parse_content(response, Some(content_type))
.map(|parsed| parsed.figment)
.map_err(ConfigError::RemoteError)
}
}
#[cfg(not(feature = "encryption"))]
#[derive(Clone)]
pub(crate) struct HttpAuth {
pub(crate) username: String,
pub(crate) password: Option<String>,
pub(crate) bearer_token: Option<String>,
}
#[cfg(not(feature = "encryption"))]
impl HttpConfigProvider {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
auth: None,
tls_config: None,
timeout: None,
priority: 30,
failure_threshold: 3,
reset_timeout: Duration::from_secs(30),
}
}
pub fn from_url(url: impl Into<String>) -> Self {
Self::new(url)
}
pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
self.auth = Some(HttpAuth {
username: username.into(),
password: Some(password.into()),
bearer_token: None,
});
self
}
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
self.auth = Some(HttpAuth {
username: String::new(),
password: None,
bearer_token: Some(token.into()),
});
self
}
pub fn with_tls(
mut self,
ca_cert: impl Into<PathBuf>,
client_cert: Option<impl Into<PathBuf>>,
client_key: Option<impl Into<PathBuf>>,
) -> Self {
self.tls_config = Some(TlsConfig {
ca_cert: Some(ca_cert.into()),
client_cert: client_cert.map(|p| p.into()),
client_key: client_key.map(|p| p.into()),
});
self
}
pub fn with_timeout(mut self, timeout: impl Into<String>) -> Self {
self.timeout = Some(timeout.into());
self
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold;
self
}
pub fn with_reset_timeout(mut self, timeout: Duration) -> Self {
self.reset_timeout = timeout;
self
}
fn build_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
let mut builder =
reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(30));
if let Some(tls) = &self.tls_config {
if let Some(ca_path) = &tls.ca_cert {
let cert_data = std::fs::read(ca_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read CA cert: {}", e))
})?;
let cert = reqwest::Certificate::from_pem(&cert_data).map_err(|e| {
ConfigError::RemoteError(format!("Failed to parse CA cert: {}", e))
})?;
builder = builder.add_root_certificate(cert);
}
if let (Some(cert_path), Some(key_path)) = (&tls.client_cert, &tls.client_key) {
let cert_data = std::fs::read(cert_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read client cert: {}", e))
})?;
let key_data = std::fs::read(key_path).map_err(|e| {
ConfigError::RemoteError(format!("Failed to read client key: {}", e))
})?;
let mut combined = cert_data;
combined.extend_from_slice(b"\n");
combined.extend_from_slice(&key_data);
let identity = reqwest::Identity::from_pem(&combined).map_err(|e| {
ConfigError::RemoteError(format!("Failed to parse client identity: {}", e))
})?;
builder = builder.identity(identity);
}
}
builder
.build()
.map_err(|e| ConfigError::RemoteError(format!("Failed to create HTTP client: {}", e)))
}
fn parse_response(&self, content_type: &str, response: &str) -> Result<Figment, ConfigError> {
parse_content(response, Some(content_type))
.map(|parsed| parsed.figment)
.map_err(ConfigError::RemoteError)
}
}
impl ConfigProvider for HttpConfigProvider {
fn load(&self) -> Result<Figment, ConfigError> {
validate_remote_url(&self.url)?;
let client = self.build_client()?;
let mut request = client.get(&self.url);
if let Some(auth) = &self.auth {
if let Some(token) = &auth.bearer_token {
request = request.bearer_auth(token.as_str());
} else {
request =
request.basic_auth(&auth.username, auth.password.as_ref().map(|p| p.as_str()));
}
}
let response = request
.send()
.map_err(|e| ConfigError::RemoteError(format!("HTTP request failed: {}", e)))?;
if !response.status().is_success() {
return Err(ConfigError::RemoteError(format!(
"HTTP request failed with status: {}",
response.status()
)));
}
let content_type = response
.headers()
.get("content-type")
.and_then(|h| h.to_str().ok())
.unwrap_or("")
.to_string();
let body = response
.text()
.map_err(|e| ConfigError::RemoteError(format!("Failed to read response: {}", e)))?;
self.parse_response(&content_type, &body)
}
fn name(&self) -> &str {
"http"
}
fn is_available(&self) -> bool {
!self.url.is_empty()
&& (self.url.starts_with("http://") || self.url.starts_with("https://"))
}
fn priority(&self) -> u8 {
self.priority
}
fn metadata(&self) -> ProviderMetadata {
let auth_type = if self
.auth
.as_ref()
.and_then(|a| a.bearer_token.as_ref())
.is_some()
{
"bearer_token"
} else if self
.auth
.as_ref()
.map(|a| !a.username.is_empty())
.unwrap_or(false)
{
"basic_auth"
} else {
"none"
};
ProviderMetadata {
name: self.name().to_string(),
description: format!("HTTP provider for URL: {} (auth: {})", self.url, auth_type),
source_type: ProviderType::Remote,
requires_network: true,
supports_watch: false,
priority: self.priority,
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[deprecated(since = "0.4.0", note = "Use HttpConfigProvider instead")]
pub type HttpProvider = HttpConfigProvider;