1#![warn(missing_docs)]
2
3use std::sync::Arc;
8use std::time::Duration;
9
10use wae_request::{HttpClient, HttpClientConfig, HttpResponse, RequestBuilder};
11
12use crate::types::{AuthResponse, ImageManifest};
13use docker_types::{DockerError, Result};
14
15pub trait RegistryClient {
19 async fn get_manifest(&self, image: &str, tag: &str) -> Result<ImageManifest>;
28
29 async fn download_layer(&self, image: &str, digest: &str) -> Result<HttpResponse>;
38
39 async fn download_layer_with_range(
49 &self,
50 image: &str,
51 digest: &str,
52 start: u64,
53 ) -> Result<HttpResponse>;
54
55 fn get_base_url(&self) -> &str;
60}
61
62pub struct DockerHubClient {
66 client: HttpClient,
68 base_url: String,
70}
71
72impl DockerHubClient {
73 pub fn new() -> Result<Self> {
78 let config = HttpClientConfig {
79 timeout: Duration::from_secs(30),
80 connect_timeout: Duration::from_secs(10),
81 ..Default::default()
82 };
83
84 let client = HttpClient::new(config);
85
86 Ok(Self {
87 client,
88 base_url: "https://registry-1.docker.io".to_string(),
89 })
90 }
91
92 async fn get_manifest_internal(&self, image: &str, tag: &str) -> Result<ImageManifest> {
101 let url = format!("{}/v2/{}/manifests/{}", self.base_url, image, tag);
102
103 for attempt in 0..3 {
105 let response = match self
106 .client
107 .get_with_headers(
108 &url,
109 [(
110 "Accept".to_string(),
111 "application/vnd.docker.distribution.manifest.v2+json".to_string(),
112 )]
113 .into(),
114 )
115 .await
116 {
117 Ok(response) => response,
118 Err(e) => {
119 if attempt < 2 {
120 tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
121 continue;
122 }
123 return Err(DockerError::request_error(&url, e.to_string()).into());
124 }
125 };
126
127 if !response.is_success() {
128 if response.status == 401 {
130 if let Some(www_auth) = response.headers.get("www-authenticate") {
131 let auth_header = www_auth;
132 let token = self.get_auth_token(auth_header, image).await?;
133
134 let response = self
136 .client
137 .get_with_headers(
138 &url,
139 [
140 (
141 "Accept".to_string(),
142 "application/vnd.docker.distribution.manifest.v2+json"
143 .to_string(),
144 ),
145 ("Authorization".to_string(), format!("Bearer {}", token)),
146 ]
147 .into(),
148 )
149 .await
150 .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
151
152 let manifest = response
153 .json::<ImageManifest>()
154 .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
155 return Ok(manifest);
156 }
157 }
158 return Err(DockerError::registry_error(format!(
159 "Failed to get manifest: {}",
160 response.status
161 ))
162 .into());
163 }
164
165 let manifest = response
166 .json::<ImageManifest>()
167 .map_err(|e| DockerError::json_error(e.to_string()))?;
168 return Ok(manifest);
169 }
170
171 Err(DockerError::registry_error(
172 "Failed to get manifest after multiple attempts".to_string(),
173 )
174 .into())
175 }
176
177 async fn get_auth_token(&self, auth_header: &str, image: &str) -> Result<String> {
186 let parts: Vec<&str> = auth_header.split(' ').collect();
188 if parts.len() != 2 || parts[0] != "Bearer" {
189 return Err(
190 DockerError::registry_error("Invalid WWW-Authenticate header".to_string()).into(),
191 );
192 }
193
194 let params: Vec<&str> = parts[1].split(',').collect();
195 let mut realm = "";
196 let mut service = "";
197 let mut scope = "";
198
199 for param in params {
200 let param_parts: Vec<&str> = param.split('=').collect();
201 if param_parts.len() == 2 {
202 let key = param_parts[0].trim();
203 let value = param_parts[1].trim().trim_matches('"');
204 match key {
205 "realm" => realm = value,
206 "service" => service = value,
207 "scope" => scope = value,
208 _ => {}
209 }
210 }
211 }
212
213 if realm.is_empty() || service.is_empty() {
214 return Err(
215 DockerError::registry_error("Invalid WWW-Authenticate header".to_string()).into(),
216 );
217 }
218
219 let scope = if scope.is_empty() {
220 format!("repository:{}/pull", image)
221 } else {
222 scope.to_string()
223 };
224
225 let auth_url = format!("{}?service={}&scope={}", realm, service, scope);
226
227 let response = self
228 .client
229 .get(&auth_url)
230 .await
231 .map_err(|e| DockerError::request_error(&auth_url, e.to_string()))?;
232
233 if !response.is_success() {
234 return Err(DockerError::registry_error(format!(
235 "Failed to get auth token: {}",
236 response.status
237 ))
238 .into());
239 }
240
241 let auth_response = response
242 .json::<AuthResponse>()
243 .map_err(|e| DockerError::json_error(e.to_string()))?;
244 Ok(auth_response.token)
245 }
246
247 async fn download_layer_internal(&self, image: &str, digest: &str) -> Result<HttpResponse> {
256 let url = format!("{}/v2/{}/blobs/{}", self.base_url, image, digest);
257
258 for attempt in 0..3 {
260 let response = match self.client.get(&url).await {
261 Ok(response) => response,
262 Err(e) => {
263 if attempt < 2 {
264 tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
265 continue;
266 }
267 return Err(DockerError::request_error(&url, e.to_string()).into());
268 }
269 };
270
271 if !response.is_success() {
272 if response.status == 401 {
274 if let Some(www_auth) = response.headers.get("www-authenticate") {
275 let auth_header = www_auth;
276 let token = self.get_auth_token(auth_header, image).await?;
277
278 let response = self
280 .client
281 .get_with_headers(
282 &url,
283 [("Authorization".to_string(), format!("Bearer {}", token))].into(),
284 )
285 .await
286 .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
287
288 return Ok(response);
289 }
290 }
291 return Err(DockerError::registry_error(format!(
292 "Failed to download layer: {}",
293 response.status
294 ))
295 .into());
296 }
297
298 return Ok(response);
299 }
300
301 Err(DockerError::registry_error(
302 "Failed to download layer after multiple attempts".to_string(),
303 )
304 .into())
305 }
306
307 async fn download_layer_with_range_internal(
317 &self,
318 image: &str,
319 digest: &str,
320 start: u64,
321 ) -> Result<HttpResponse> {
322 let url = format!("{}/v2/{}/blobs/{}", self.base_url, image, digest);
323
324 for attempt in 0..3 {
326 let response = match self
327 .client
328 .get_with_headers(
329 &url,
330 [("Range".to_string(), format!("bytes={}-", start))].into(),
331 )
332 .await
333 {
334 Ok(response) => response,
335 Err(e) => {
336 if attempt < 2 {
337 tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
338 continue;
339 }
340 return Err(DockerError::request_error(&url, e.to_string()).into());
341 }
342 };
343
344 if !response.is_success() && response.status != 206 {
345 if response.status == 401 {
347 if let Some(www_auth) = response.headers.get("www-authenticate") {
348 let auth_header = www_auth;
349 let token = self.get_auth_token(auth_header, image).await?;
350
351 let response = self
353 .client
354 .get_with_headers(
355 &url,
356 [
357 ("Range".to_string(), format!("bytes={}-", start)),
358 ("Authorization".to_string(), format!("Bearer {}", token)),
359 ]
360 .into(),
361 )
362 .await
363 .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
364
365 return Ok(response);
366 }
367 }
368 return Err(DockerError::registry_error(format!(
369 "Failed to download layer: {}",
370 response.status
371 ))
372 .into());
373 }
374
375 return Ok(response);
376 }
377
378 Err(DockerError::registry_error(
379 "Failed to download layer after multiple attempts".to_string(),
380 )
381 .into())
382 }
383}
384
385impl Default for DockerHubClient {
386 fn default() -> Self {
391 Self::new().expect("Failed to create DockerHubClient")
392 }
393}
394
395impl RegistryClient for DockerHubClient {
396 async fn get_manifest(&self, image: &str, tag: &str) -> Result<ImageManifest> {
397 self.get_manifest_internal(image, tag).await
398 }
399
400 async fn download_layer(&self, image: &str, digest: &str) -> Result<HttpResponse> {
401 self.download_layer_internal(image, digest).await
402 }
403
404 async fn download_layer_with_range(
405 &self,
406 image: &str,
407 digest: &str,
408 start: u64,
409 ) -> Result<HttpResponse> {
410 self.download_layer_with_range_internal(image, digest, start)
411 .await
412 }
413
414 fn get_base_url(&self) -> &str {
415 &self.base_url
416 }
417}