systemprompt_loader/bundle/source/
https.rs1use std::path::Path;
13
14use systemprompt_models::net::{trusted_http_hosts_from_env, validate_outbound_url_with_trust};
15
16use super::stream::stream_to_file;
17use super::{BundleFetcher, FetchedBundle, MAX_BUNDLE_BYTES, RemoteRef};
18use crate::bundle::error::{BundleError, BundleResult};
19
20#[derive(Debug)]
21pub struct HttpsFetcher {
22 name: String,
23 url: String,
24 auth: Option<String>,
25 client: reqwest::Client,
26 max_bytes: u64,
27}
28
29impl HttpsFetcher {
30 #[must_use]
31 pub fn new(name: &str, url: &str, auth: Option<String>, client: reqwest::Client) -> Self {
32 Self {
33 name: name.to_owned(),
34 url: url.to_owned(),
35 auth,
36 client,
37 max_bytes: MAX_BUNDLE_BYTES,
38 }
39 }
40
41 #[must_use]
42 pub const fn with_max_bytes(mut self, max_bytes: u64) -> Self {
43 self.max_bytes = max_bytes;
44 self
45 }
46
47 fn checked_url(&self) -> BundleResult<url::Url> {
48 let trusted = trusted_http_hosts_from_env();
49 validate_outbound_url_with_trust(&self.url, &trusted)
50 .map_err(|e| BundleError::fetch(&self.name, e))
51 }
52}
53
54impl BundleFetcher for HttpsFetcher {
55 async fn head(&self) -> BundleResult<RemoteRef> {
56 let url = self.checked_url()?;
57 let mut request = self.client.head(url);
58 if let Some(token) = self.auth.as_ref() {
59 request = request.bearer_auth(token);
60 }
61 let response = request
62 .send()
63 .await
64 .map_err(|e| BundleError::fetch(&self.name, e))?;
65
66 if response.status() == reqwest::StatusCode::UNAUTHORIZED
67 || response.status() == reqwest::StatusCode::FORBIDDEN
68 {
69 return Err(BundleError::Auth {
70 source_name: self.name.clone(),
71 });
72 }
73 if !response.status().is_success() {
74 return Ok(RemoteRef {
75 digest: String::new(),
76 });
77 }
78
79 let etag = response
80 .headers()
81 .get(reqwest::header::ETAG)
82 .and_then(|v| v.to_str().ok())
83 .map(|v| v.trim_matches('"').to_owned())
84 .unwrap_or_default();
85 Ok(RemoteRef { digest: etag })
86 }
87
88 async fn fetch(&self, into: &Path) -> BundleResult<FetchedBundle> {
89 let url = self.checked_url()?;
90 let mut request = self.client.get(url);
91 if let Some(token) = self.auth.as_ref() {
92 request = request.bearer_auth(token);
93 }
94 let response = request
95 .send()
96 .await
97 .map_err(|e| BundleError::fetch(&self.name, e))?;
98
99 let status = response.status();
100 if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
101 return Err(BundleError::Auth {
102 source_name: self.name.clone(),
103 });
104 }
105 if status.is_redirection() {
106 return Err(BundleError::fetch(
107 &self.name,
108 format!("redirect ({status}) is not followed"),
109 ));
110 }
111 if !status.is_success() {
112 return Err(BundleError::fetch(&self.name, format!("status {status}")));
113 }
114
115 let digest = stream_to_file(response, into, &self.name, self.max_bytes).await?;
116 Ok(FetchedBundle {
117 archive: into.to_path_buf(),
118 digest,
119 })
120 }
121}