authplane_sdk/cache/
document_fetcher.rs1use std::sync::Arc;
4
5use reqwest::Client;
6use serde_json::Value;
7
8use crate::cache::cache_headers::parse_expires_at;
9use crate::cache::document_cache::FetchResult;
10use crate::errors::transport_error;
11use crate::transport::{build_http_client, ssrf_safe_get, validate_fetch_url};
12use crate::{AuthplaneError, FetchSettings};
13
14#[derive(Debug, Clone)]
20pub struct DocumentFetcher {
21 url: String,
22 document_type: String,
23 settings: FetchSettings,
24 max_size: u64,
25 http: Arc<Client>,
26}
27
28impl DocumentFetcher {
29 pub const DEFAULT_METADATA_MAX_BYTES: u64 = 131_072;
31 pub const DEFAULT_JWKS_MAX_BYTES: u64 = 65_536;
33
34 pub fn new(
36 url: impl Into<String>,
37 document_type: impl Into<String>,
38 settings: FetchSettings,
39 max_size: u64,
40 ) -> Result<Self, AuthplaneError> {
41 let http = Arc::new(build_http_client(&settings)?);
42 Ok(Self {
43 url: url.into(),
44 document_type: document_type.into(),
45 settings,
46 max_size,
47 http,
48 })
49 }
50
51 pub fn with_client(
54 url: impl Into<String>,
55 document_type: impl Into<String>,
56 settings: FetchSettings,
57 max_size: u64,
58 http: Arc<Client>,
59 ) -> Self {
60 Self {
61 url: url.into(),
62 document_type: document_type.into(),
63 settings,
64 max_size,
65 http,
66 }
67 }
68
69 pub fn url(&self) -> &str {
71 &self.url
72 }
73
74 pub fn document_type(&self) -> &str {
76 &self.document_type
77 }
78
79 pub async fn fetch(&self) -> Result<FetchResult, AuthplaneError> {
82 validate_fetch_url(
83 &self.url,
84 &self.settings,
85 &format!("{} URL", self.document_type),
86 )?;
87
88 if self.settings.ssrf_protection {
89 let ssrf_response = ssrf_safe_get(&self.url, &self.settings, self.max_size).await?;
91
92 if !crate::transport::is_http_success(ssrf_response.status_code) {
93 return Err(transport_error(&format!(
94 "{} fetch returned HTTP {}",
95 self.document_type, ssrf_response.status_code
96 )));
97 }
98
99 let expires_at = parse_expires_at(
100 ssrf_response
101 .headers
102 .iter()
103 .map(|(k, v)| (k.as_str(), v.as_str())),
104 );
105
106 Ok(FetchResult {
107 document: ssrf_response.body,
108 expires_at,
109 })
110 } else {
111 let response = self.http.get(&self.url).send().await.map_err(|error| {
113 transport_error(&format!("{} fetch failed: {error}", self.document_type))
114 })?;
115
116 let status = response.status().as_u16();
117 if !crate::transport::is_http_success(status) {
118 return Err(transport_error(&format!(
119 "{} fetch returned HTTP {status}",
120 self.document_type
121 )));
122 }
123
124 let header_snapshot: Vec<(String, String)> = response
126 .headers()
127 .iter()
128 .filter_map(|(name, value)| {
129 value
130 .to_str()
131 .ok()
132 .map(|v| (name.to_string(), v.to_string()))
133 })
134 .collect();
135
136 let bytes = response.bytes().await.map_err(|error| {
137 transport_error(&format!("{} body read failed: {error}", self.document_type))
138 })?;
139
140 if bytes.len() as u64 > self.max_size {
141 return Err(transport_error(&format!(
142 "{} document exceeds {} bytes",
143 self.document_type, self.max_size
144 )));
145 }
146
147 let document: Value = serde_json::from_slice(&bytes).map_err(|error| {
148 transport_error(&format!(
149 "{} body is not valid JSON: {error}",
150 self.document_type
151 ))
152 })?;
153
154 let expires_at = parse_expires_at(
155 header_snapshot
156 .iter()
157 .map(|(k, v)| (k.as_str(), v.as_str())),
158 );
159
160 Ok(FetchResult {
161 document,
162 expires_at,
163 })
164 }
165 }
166}