auths_infra_http/
registry_client.rs1use auths_core::ports::network::{NetworkError, RateLimitInfo, RegistryClient, RegistryResponse};
2use std::future::Future;
3use std::time::Duration;
4
5use crate::error::map_reqwest_error;
6use crate::request::{
7 build_get_request, build_post_request, execute_request, parse_response_bytes,
8};
9use crate::ssrf::{SsrfBlocked, guard_registry_url};
10use crate::{default_client_builder, default_http_client};
11
12fn map_ssrf_error(err: SsrfBlocked) -> NetworkError {
14 NetworkError::InvalidResponse {
15 detail: err.to_string(),
16 }
17}
18
19pub struct HttpRegistryClient {
32 client: reqwest::Client,
33}
34
35impl HttpRegistryClient {
36 pub fn new() -> Self {
37 Self {
38 client: default_http_client(),
39 }
40 }
41
42 #[allow(clippy::expect_used)]
57 pub fn new_with_timeouts(connect_timeout: Duration, request_timeout: Duration) -> Self {
58 let client = default_client_builder()
59 .connect_timeout(connect_timeout)
60 .timeout(request_timeout)
61 .build()
62 .expect("failed to build HTTP client");
63 Self { client }
64 }
65}
66
67impl Default for HttpRegistryClient {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl RegistryClient for HttpRegistryClient {
74 fn fetch_registry_data(
75 &self,
76 registry_url: &str,
77 path: &str,
78 ) -> impl Future<Output = Result<Vec<u8>, NetworkError>> + Send {
79 let guard = guard_registry_url(registry_url).map_err(map_ssrf_error);
80 let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
81 let request = build_get_request(&self.client, &url);
82
83 async move {
84 guard?;
85 let response = execute_request(request, registry_url).await?;
86 parse_response_bytes(response, path).await
87 }
88 }
89
90 fn push_registry_data(
91 &self,
92 registry_url: &str,
93 path: &str,
94 data: &[u8],
95 ) -> impl Future<Output = Result<(), NetworkError>> + Send {
96 let guard = guard_registry_url(registry_url).map_err(map_ssrf_error);
97 let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
98 let request = build_post_request(&self.client, &url, data.to_vec());
99
100 async move {
101 guard?;
102 let response = execute_request(request, registry_url).await?;
103 let _ = parse_response_bytes(response, path).await?;
104 Ok(())
105 }
106 }
107
108 fn post_json(
109 &self,
110 registry_url: &str,
111 path: &str,
112 json_body: &[u8],
113 ) -> impl Future<Output = Result<RegistryResponse, NetworkError>> + Send {
114 let guard = guard_registry_url(registry_url).map_err(map_ssrf_error);
115 let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
116 let request = self
117 .client
118 .post(&url)
119 .header("Content-Type", "application/json")
120 .body(json_body.to_vec());
121 let endpoint = registry_url.to_string();
122
123 async move {
124 guard?;
125 let response = request
126 .send()
127 .await
128 .map_err(|e| map_reqwest_error(e, &endpoint))?;
129 let status = response.status().as_u16();
130 let rate_limit = extract_rate_limit_headers(&response);
131 let body = response.bytes().await.map(|b| b.to_vec()).map_err(|e| {
132 NetworkError::InvalidResponse {
133 detail: e.to_string(),
134 }
135 })?;
136 Ok(RegistryResponse {
137 status,
138 body,
139 rate_limit,
140 })
141 }
142 }
143}
144
145fn extract_rate_limit_headers(response: &reqwest::Response) -> Option<RateLimitInfo> {
146 let headers = response.headers();
147 let limit = headers
148 .get("x-ratelimit-limit")
149 .and_then(|v| v.to_str().ok())
150 .and_then(|s| s.parse::<i32>().ok());
151 let remaining = headers
152 .get("x-ratelimit-remaining")
153 .and_then(|v| v.to_str().ok())
154 .and_then(|s| s.parse::<i32>().ok());
155 let reset = headers
156 .get("x-ratelimit-reset")
157 .and_then(|v| v.to_str().ok())
158 .and_then(|s| s.parse::<i64>().ok());
159 let tier = headers
160 .get("x-ratelimit-tier")
161 .and_then(|v| v.to_str().ok())
162 .map(String::from);
163
164 if limit.is_some() || remaining.is_some() || reset.is_some() || tier.is_some() {
165 Some(RateLimitInfo {
166 limit,
167 remaining,
168 reset,
169 tier,
170 })
171 } else {
172 None
173 }
174}