use anyhow::Context;
use par_term_config::DynamicProfileSource;
use crate::profile::dynamic::cache::write_cache;
#[derive(Debug, Clone)]
pub struct FetchResult {
pub url: String,
pub profiles: Vec<par_term_config::Profile>,
pub etag: Option<String>,
pub error: Option<String>,
}
pub fn fetch_profiles(source: &DynamicProfileSource) -> FetchResult {
let url = &source.url;
crate::debug_info!("DYNAMIC_PROFILE", "Fetching profiles from {}", url);
match fetch_profiles_inner(source) {
Ok((profiles, etag)) => {
crate::debug_info!(
"DYNAMIC_PROFILE",
"Fetched {} profiles from {}",
profiles.len(),
url
);
if let Err(e) = write_cache(url, &profiles, etag.clone()) {
crate::debug_error!(
"DYNAMIC_PROFILE",
"Failed to cache profiles from {}: {}",
url,
e
);
}
FetchResult {
url: url.clone(),
profiles,
etag,
error: None,
}
}
Err(e) => {
crate::debug_error!("DYNAMIC_PROFILE", "Failed to fetch from {}: {}", url, e);
FetchResult {
url: url.clone(),
profiles: Vec::new(),
etag: None,
error: Some(e.to_string()),
}
}
}
}
fn fetch_profiles_inner(
source: &DynamicProfileSource,
) -> anyhow::Result<(Vec<par_term_config::Profile>, Option<String>)> {
use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
if source.url.starts_with("file://") {
anyhow::bail!(
"Dynamic profile URL '{}' uses file:// scheme which is not permitted. \
Only https:// URLs are supported.",
source.url
);
}
if !source.url.starts_with("https://") {
if source.headers.keys().any(|k| {
let lower = k.to_lowercase();
lower == "authorization" || lower.contains("token") || lower.contains("secret")
}) {
anyhow::bail!(
"Refusing to send authentication headers over insecure HTTP for {}. Use HTTPS.",
source.url
);
}
if !source.allow_http {
anyhow::bail!(
"Dynamic profile URL '{}' uses insecure HTTP. \
Set `allow_http_profiles: true` in your config to allow HTTP (not recommended). \
Use HTTPS to prevent MITM injection of profiles.",
source.url
);
}
crate::debug_error!(
"DYNAMIC_PROFILE",
"SECURITY WARNING: {} is using insecure HTTP (not HTTPS). \
A MITM attacker could inject malicious profiles. Use HTTPS.",
source.url
);
log::warn!(
"par-term dynamic profile: fetching '{}' over insecure HTTP \
(allow_http_profiles is enabled). MITM injection of profiles is possible. \
Switch to HTTPS when possible.",
source.url
);
}
let tls_config = TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.root_certs(RootCerts::PlatformVerifier)
.build();
let agent: ureq::Agent = ureq::Agent::config_builder()
.tls_config(tls_config)
.timeout_global(Some(std::time::Duration::from_secs(
source.fetch_timeout_secs,
)))
.build()
.into();
let mut request = agent.get(&source.url);
for (key, value) in &source.headers {
request = request.header(key.as_str(), value.as_str());
}
let mut response = request
.call()
.with_context(|| format!("HTTP request failed for {}", source.url))?;
let etag = response
.headers()
.get("etag")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let body = response
.body_mut()
.with_config()
.limit(source.max_size_bytes as u64)
.read_to_string()
.with_context(|| format!("Failed to read response body from {}", source.url))?;
let profiles: Vec<par_term_config::Profile> = serde_yaml_ng::from_str(&body)
.with_context(|| format!("Failed to parse YAML from {}", source.url))?;
Ok((profiles, etag))
}