docker_image_pusher/registry/
auth.rs1use crate::error::{Result, PusherError};
4use reqwest::{Client, header::{AUTHORIZATION, WWW_AUTHENTICATE}};
5use serde::Deserialize;
6use base64::{Engine as _, engine::general_purpose};
7
8#[derive(Deserialize, Debug)]
9struct AuthResponse {
10 token: Option<String>,
11 access_token: Option<String>,
12}
13
14#[derive(Debug)]
15pub struct AuthChallenge {
16 pub realm: String,
17 pub service: String,
18 pub scope: Option<String>,
19}
20
21impl AuthChallenge {
22 pub fn parse(auth_header: &str) -> Result<Self> {
23 let mut realm = String::new();
24 let mut service = String::new();
25 let mut scope = None;
26
27 if auth_header.starts_with("Bearer ") {
28 let params = &auth_header[7..]; for param in params.split(',') {
30 let param = param.trim();
31 if let Some((key, value)) = param.split_once('=') {
32 let value = value.trim_matches('"');
33 match key {
34 "realm" => realm = value.to_string(),
35 "service" => service = value.to_string(),
36 "scope" => scope = Some(value.to_string()),
37 _ => {}
38 }
39 }
40 }
41 }
42
43 if realm.is_empty() || service.is_empty() {
44 return Err(PusherError::Authentication("Invalid auth challenge format".to_string()));
45 }
46
47 Ok(AuthChallenge { realm, service, scope })
48 }
49}
50
51pub struct Auth {
52 client: Client,
53 registry_url: String,
54}
55
56impl Auth {
57 pub fn new(registry_url: &str, skip_tls: bool) -> Result<Self> {
58 let client = if skip_tls {
59 Client::builder()
60 .danger_accept_invalid_certs(true)
61 .danger_accept_invalid_hostnames(true)
62 .build()
63 .map_err(PusherError::Network)?
64 } else {
65 Client::new()
66 };
67
68 Ok(Auth {
69 client,
70 registry_url: registry_url.to_string(),
71 })
72 }
73
74 pub async fn get_auth_challenge(&self) -> Result<Option<AuthChallenge>> {
75 let url = format!("{}/v2/", self.registry_url);
76 println!(" Getting auth challenge from: {}", url);
77
78 let response = self.client.get(&url).send().await?;
79 println!(" Auth challenge response status: {}", response.status());
80
81 if response.status() == 401 {
82 if let Some(auth_header) = response.headers().get(WWW_AUTHENTICATE) {
83 let auth_str = auth_header.to_str()
84 .map_err(|e| PusherError::Authentication(format!("Invalid auth header: {}", e)))?;
85 println!(" WWW-Authenticate header: {}", auth_str);
86 return Ok(Some(AuthChallenge::parse(auth_str)?));
87 }
88 }
89
90 Ok(None)
91 }
92
93 pub async fn get_token_for_repository(
94 &self,
95 challenge: &AuthChallenge,
96 username: &str,
97 password: &str,
98 repository: &str,
99 ) -> Result<String> {
100 let credentials = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
101
102 let scope = format!("repository:{}:pull,push", repository);
104 let query_params = vec![
105 ("service", challenge.service.as_str()),
106 ("scope", scope.as_str()),
107 ];
108
109 println!(" Token request to: {}", challenge.realm);
110 println!(" Service: {}", challenge.service);
111 println!(" Scope: {}", scope);
112 println!(" Username: {}", username);
113
114 self.request_token(&challenge.realm, &credentials, &query_params).await
115 }
116
117 pub async fn get_token(
118 &self,
119 challenge: &AuthChallenge,
120 username: &str,
121 password: &str,
122 repository: Option<&str>,
123 ) -> Result<String> {
124 let credentials = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
125
126 let mut query_params = vec![("service", challenge.service.as_str())];
127
128 let scope_value = if let Some(repo) = repository {
130 format!("repository:{}:pull,push", repo)
131 } else {
132 String::new()
133 };
134
135 if !scope_value.is_empty() {
136 query_params.push(("scope", scope_value.as_str()));
137 println!(" Token request to: {}", challenge.realm);
138 println!(" Service: {}", challenge.service);
139 println!(" Scope: {}", scope_value);
140 println!(" Username: {}", username);
141 }
142
143 match self.request_token(&challenge.realm, &credentials, &query_params).await {
144 Ok(token) => Ok(token),
145 Err(e) => {
146 println!(" Token request failed: {}", e);
147 if !scope_value.is_empty() {
149 println!(" Retrying without scope...");
150 let basic_params = vec![("service", challenge.service.as_str())];
151 self.request_token(&challenge.realm, &credentials, &basic_params).await
152 } else {
153 Err(PusherError::Authentication("Token request failed".to_string()))
154 }
155 }
156 }
157 }
158
159 async fn request_token(
160 &self,
161 realm: &str,
162 credentials: &str,
163 query_params: &[(&str, &str)],
164 ) -> Result<String> {
165 let response = self.client
166 .get(realm)
167 .header(AUTHORIZATION, format!("Basic {}", credentials))
168 .query(query_params)
169 .send()
170 .await?;
171
172 println!(" Token response status: {}", response.status());
173
174 if response.status().is_success() {
175 let response_text = response.text().await?;
176 println!(" Token response body: {}", response_text);
177
178 let auth_response: AuthResponse = serde_json::from_str(&response_text)
179 .map_err(|e| PusherError::Authentication(format!("Failed to parse token response: {}", e)))?;
180
181 let token = auth_response.token
182 .or(auth_response.access_token)
183 .ok_or_else(|| PusherError::Authentication("No token in response".to_string()))?;
184
185 println!(" Token received successfully");
186 Ok(token)
187 } else {
188 let error_text = response.text().await?;
189 println!(" Token request failed with body: {}", error_text);
190 Err(PusherError::Authentication(format!("Token request failed: {}", error_text)))
191 }
192 }
193
194 pub async fn login(&self, username: &str, password: &str) -> Result<Option<String>> {
195 if let Some(challenge) = self.get_auth_challenge().await? {
196 println!("Auth challenge received from registry:");
197 println!(" Realm: {}", challenge.realm);
198 println!(" Service: {}", challenge.service);
199 if let Some(scope) = &challenge.scope {
200 println!(" Scope: {}", scope);
201 }
202 let token = self.get_token(&challenge, username, password, None).await?;
203 Ok(Some(token))
204 } else {
205 println!("No auth challenge - registry may not require authentication");
206 Ok(None)
207 }
208 }
209
210 pub async fn login_with_repository(&self, username: &str, password: &str, repository: &str) -> Result<Option<String>> {
211 if let Some(challenge) = self.get_auth_challenge().await? {
212 println!("Getting repository-specific token for: {}", repository);
213 let token = self.get_token_for_repository(&challenge, username, password, repository).await?;
214 Ok(Some(token))
215 } else {
216 Ok(None)
217 }
218 }
219}