Skip to main content

runner_core/packs/resolver/
http.rs

1use std::io::copy;
2
3use anyhow::{Context, Result, bail};
4use greentic_config_types::{NetworkConfig, TlsMode};
5use reqwest::blocking::Client;
6use std::time::Duration;
7use tempfile::NamedTempFile;
8
9use super::{FetchResponse, PackResolver};
10
11pub struct HttpResolver {
12    scheme: &'static str,
13    client: Client,
14}
15
16impl HttpResolver {
17    pub fn new(scheme: &'static str, network: Option<&NetworkConfig>) -> Result<Self> {
18        let mut builder = Client::builder();
19        if let Some(cfg) = network {
20            if let Some(proxy) = &cfg.proxy_url {
21                builder = builder.proxy(reqwest::Proxy::all(proxy)?);
22            }
23            if let Some(timeout) = cfg.connect_timeout_ms {
24                builder = builder.connect_timeout(Duration::from_millis(timeout));
25            }
26            if let Some(timeout) = cfg.read_timeout_ms {
27                builder = builder.timeout(Duration::from_millis(timeout));
28            }
29            if matches!(cfg.tls_mode, TlsMode::Disabled) {
30                bail!("TLS certificate validation cannot be disabled");
31            }
32        }
33        Ok(Self {
34            scheme,
35            client: builder.build()?,
36        })
37    }
38}
39
40impl PackResolver for HttpResolver {
41    fn scheme(&self) -> &'static str {
42        self.scheme
43    }
44
45    fn fetch(&self, locator: &str) -> Result<FetchResponse> {
46        let mut response = self
47            .client
48            .get(locator)
49            .send()
50            .with_context(|| format!("failed to download {}", locator))?
51            .error_for_status()
52            .with_context(|| format!("download failed {}", locator))?;
53
54        let mut temp = NamedTempFile::new().context("failed to allocate temp file for download")?;
55        {
56            let mut writer = temp.as_file_mut();
57            copy(&mut response, &mut writer).context("failed to stream HTTP content")?;
58        }
59        Ok(FetchResponse::from_temp(temp.into_temp_path()))
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn new_rejects_disabled_tls_validation() {
69        let cfg = NetworkConfig {
70            proxy_url: None,
71            connect_timeout_ms: Some(100),
72            read_timeout_ms: Some(100),
73            tls_mode: TlsMode::Disabled,
74        };
75        let err = match HttpResolver::new("http", Some(&cfg)) {
76            Ok(_) => panic!("tls disabled must fail"),
77            Err(err) => err,
78        };
79        assert!(err.to_string().contains("cannot be disabled"));
80    }
81
82    #[test]
83    fn new_accepts_proxy_and_preserves_scheme() {
84        let cfg = NetworkConfig {
85            proxy_url: Some("http://proxy.example.com:8080".into()),
86            connect_timeout_ms: Some(250),
87            read_timeout_ms: Some(500),
88            tls_mode: TlsMode::Strict,
89        };
90        let resolver = HttpResolver::new("https", Some(&cfg)).expect("resolver");
91        assert_eq!(resolver.scheme(), "https");
92    }
93
94    #[test]
95    fn fetch_rejects_malformed_locator() {
96        let resolver = HttpResolver::new("http", None).expect("resolver");
97        let err = match resolver.fetch("http://[::1") {
98            Ok(_) => panic!("malformed url must fail"),
99            Err(err) => err,
100        };
101        assert!(err.to_string().contains("failed to download"));
102    }
103}