par_term/profile/dynamic/
fetch.rs1use anyhow::Context;
10use par_term_config::DynamicProfileSource;
11
12use crate::profile::dynamic::cache::write_cache;
13
14#[derive(Debug, Clone)]
16pub struct FetchResult {
17 pub url: String,
19 pub profiles: Vec<par_term_config::Profile>,
21 pub etag: Option<String>,
23 pub error: Option<String>,
25}
26
27pub fn fetch_profiles(source: &DynamicProfileSource) -> FetchResult {
29 let url = &source.url;
30 crate::debug_info!("DYNAMIC_PROFILE", "Fetching profiles from {}", url);
31
32 match fetch_profiles_inner(source) {
33 Ok((profiles, etag)) => {
34 crate::debug_info!(
35 "DYNAMIC_PROFILE",
36 "Fetched {} profiles from {}",
37 profiles.len(),
38 url
39 );
40 if let Err(e) = write_cache(url, &profiles, etag.clone()) {
41 crate::debug_error!(
42 "DYNAMIC_PROFILE",
43 "Failed to cache profiles from {}: {}",
44 url,
45 e
46 );
47 }
48 FetchResult {
49 url: url.clone(),
50 profiles,
51 etag,
52 error: None,
53 }
54 }
55 Err(e) => {
56 crate::debug_error!("DYNAMIC_PROFILE", "Failed to fetch from {}: {}", url, e);
57 FetchResult {
58 url: url.clone(),
59 profiles: Vec::new(),
60 etag: None,
61 error: Some(e.to_string()),
62 }
63 }
64 }
65}
66
67fn fetch_profiles_inner(
69 source: &DynamicProfileSource,
70) -> anyhow::Result<(Vec<par_term_config::Profile>, Option<String>)> {
71 use par_term_config::url_policy::{Transport, validate_scheme};
72 use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
73
74 let transport = validate_scheme(&source.url, source.allow_http)
85 .map_err(|e| anyhow::anyhow!("Dynamic profile source rejected: {e}"))?;
86
87 if transport == Transport::Http {
88 if source.headers.keys().any(|k| {
91 let lower = k.to_lowercase();
92 lower == "authorization" || lower.contains("token") || lower.contains("secret")
93 }) {
94 anyhow::bail!(
95 "Refusing to send authentication headers over insecure HTTP for {}. Use HTTPS.",
96 source.url
97 );
98 }
99
100 crate::debug_error!(
102 "DYNAMIC_PROFILE",
103 "SECURITY WARNING: {} is using insecure HTTP (not HTTPS). \
104 A MITM attacker could inject malicious profiles. Use HTTPS.",
105 source.url
106 );
107 log::warn!(
108 "par-term dynamic profile: fetching '{}' over insecure HTTP \
109 (allow_http_profiles is enabled). MITM injection of profiles is possible. \
110 Switch to HTTPS when possible.",
111 source.url
112 );
113 }
114
115 let tls_config = TlsConfig::builder()
117 .provider(TlsProvider::NativeTls)
118 .root_certs(RootCerts::PlatformVerifier)
119 .build();
120
121 let agent: ureq::Agent = ureq::Agent::config_builder()
122 .tls_config(tls_config)
123 .https_only(transport == Transport::Https)
128 .timeout_global(Some(std::time::Duration::from_secs(
129 source.fetch_timeout_secs,
130 )))
131 .build()
132 .into();
133
134 let mut request = agent.get(&source.url);
135
136 for (key, value) in &source.headers {
137 request = request.header(key.as_str(), value.as_str());
138 }
139
140 let mut response = request
141 .call()
142 .with_context(|| format!("HTTP request failed for {}", source.url))?;
143
144 let etag = response
145 .headers()
146 .get("etag")
147 .and_then(|v| v.to_str().ok())
148 .map(|s| s.to_string());
149
150 let body = response
151 .body_mut()
152 .with_config()
153 .limit(source.max_size_bytes as u64)
154 .read_to_string()
155 .with_context(|| format!("Failed to read response body from {}", source.url))?;
156
157 let profiles: Vec<par_term_config::Profile> = serde_yaml_ng::from_str(&body)
158 .with_context(|| format!("Failed to parse YAML from {}", source.url))?;
159
160 Ok((profiles, etag))
161}