1use std::borrow::Cow;
2use std::fmt;
3use std::io::Read;
4use std::io::Write;
5use std::str::FromStr;
6
7use base64::prelude::BASE64_STANDARD;
8use base64::read::DecoderReader;
9use base64::write::EncoderWriter;
10use http::Uri;
11use netrc::Netrc;
12use reqsign::aws::DefaultSigner;
13use reqwest::Request;
14use reqwest::header::HeaderValue;
15use serde::{Deserialize, Serialize};
16use url::Url;
17
18use uv_redacted::DisplaySafeUrl;
19use uv_static::EnvVars;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Credentials {
23 Basic {
25 username: Username,
27 password: Option<Password>,
29 },
30 Bearer {
32 token: Token,
34 },
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
38#[serde(transparent)]
39pub struct Username(Option<String>);
40
41impl Username {
42 pub(crate) fn new(value: Option<String>) -> Self {
46 Self(value.filter(|s| !s.is_empty()))
48 }
49
50 pub(crate) fn none() -> Self {
51 Self::new(None)
52 }
53
54 pub(crate) fn is_none(&self) -> bool {
55 self.0.is_none()
56 }
57
58 pub(crate) fn is_some(&self) -> bool {
59 self.0.is_some()
60 }
61
62 pub(crate) fn as_deref(&self) -> Option<&str> {
63 self.0.as_deref()
64 }
65}
66
67impl From<String> for Username {
68 fn from(value: String) -> Self {
69 Self::new(Some(value))
70 }
71}
72
73impl From<Option<String>> for Username {
74 fn from(value: Option<String>) -> Self {
75 Self::new(value)
76 }
77}
78
79#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
80#[serde(transparent)]
81pub struct Password(String);
82
83impl Password {
84 pub fn new(password: String) -> Self {
85 Self(password)
86 }
87
88 pub fn as_str(&self) -> &str {
90 self.0.as_str()
91 }
92
93 pub fn into_string(self) -> String {
95 self.0
96 }
97}
98
99impl fmt::Debug for Password {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "****")
102 }
103}
104
105#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
106#[serde(transparent)]
107pub struct Token(Vec<u8>);
108
109impl Token {
110 pub fn new(token: Vec<u8>) -> Self {
111 Self(token)
112 }
113
114 pub fn as_slice(&self) -> &[u8] {
116 self.0.as_slice()
117 }
118
119 pub fn into_bytes(self) -> Vec<u8> {
121 self.0
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.0.is_empty()
127 }
128}
129
130impl fmt::Debug for Token {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "****")
133 }
134}
135impl Credentials {
136 #[allow(dead_code)]
138 pub fn basic(username: Option<String>, password: Option<String>) -> Self {
139 Self::Basic {
140 username: Username::new(username),
141 password: password.map(Password),
142 }
143 }
144
145 #[allow(dead_code)]
147 pub fn bearer(token: Vec<u8>) -> Self {
148 Self::Bearer {
149 token: Token::new(token),
150 }
151 }
152
153 pub fn username(&self) -> Option<&str> {
154 match self {
155 Self::Basic { username, .. } => username.as_deref(),
156 Self::Bearer { .. } => None,
157 }
158 }
159
160 pub(crate) fn to_username(&self) -> Username {
161 match self {
162 Self::Basic { username, .. } => username.clone(),
163 Self::Bearer { .. } => Username::none(),
164 }
165 }
166
167 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
168 match self {
169 Self::Basic { username, .. } => Cow::Borrowed(username),
170 Self::Bearer { .. } => Cow::Owned(Username::none()),
171 }
172 }
173
174 pub fn password(&self) -> Option<&str> {
175 match self {
176 Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
177 Self::Bearer { .. } => None,
178 }
179 }
180
181 pub fn is_authenticated(&self) -> bool {
182 match self {
183 Self::Basic {
184 username: _,
185 password,
186 } => password.is_some(),
187 Self::Bearer { token } => !token.is_empty(),
188 }
189 }
190
191 pub(crate) fn is_empty(&self) -> bool {
192 match self {
193 Self::Basic { username, password } => username.is_none() && password.is_none(),
194 Self::Bearer { token } => token.is_empty(),
195 }
196 }
197
198 pub(crate) fn from_netrc(
202 netrc: &Netrc,
203 url: &DisplaySafeUrl,
204 username: Option<&str>,
205 ) -> Option<Self> {
206 let host = url.host_str()?;
207 let entry = netrc
208 .hosts
209 .get(host)
210 .or_else(|| netrc.hosts.get("default"))?;
211
212 if username.is_some_and(|username| username != entry.login) {
214 return None;
215 }
216
217 Some(Self::Basic {
218 username: Username::new(Some(entry.login.clone())),
219 password: Some(Password(entry.password.clone())),
220 })
221 }
222
223 pub fn from_url(url: &Url) -> Option<Self> {
227 if url.username().is_empty() && url.password().is_none() {
228 return None;
229 }
230 Some(Self::Basic {
231 username: if url.username().is_empty() {
234 None
235 } else {
236 Some(
237 percent_encoding::percent_decode_str(url.username())
238 .decode_utf8()
239 .expect("An encoded username should always decode")
240 .into_owned(),
241 )
242 }
243 .into(),
244 password: url.password().map(|password| {
245 Password(
246 percent_encoding::percent_decode_str(password)
247 .decode_utf8()
248 .expect("An encoded password should always decode")
249 .into_owned(),
250 )
251 }),
252 })
253 }
254
255 pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
260 let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
261 let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
262 if username.is_none() && password.is_none() {
263 None
264 } else {
265 Some(Self::basic(username, password))
266 }
267 }
268
269 pub(crate) fn from_request(request: &Request) -> Option<Self> {
273 Self::from_url(request.url()).or(
275 request
277 .headers()
278 .get(reqwest::header::AUTHORIZATION)
279 .map(Self::from_header_value)?,
280 )
281 }
282
283 pub(crate) fn from_header_value(header: &HeaderValue) -> Option<Self> {
292 if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
294 let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
295 let mut buf = String::new();
296 decoder
297 .read_to_string(&mut buf)
298 .expect("HTTP Basic Authentication should be base64 encoded");
299 let (username, password) = buf
300 .split_once(':')
301 .expect("HTTP Basic Authentication should include a `:` separator");
302 let username = if username.is_empty() {
303 None
304 } else {
305 Some(username.to_string())
306 };
307 let password = if password.is_empty() {
308 None
309 } else {
310 Some(password.to_string())
311 };
312 return Some(Self::Basic {
313 username: Username::new(username),
314 password: password.map(Password),
315 });
316 }
317
318 if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
320 return Some(Self::Bearer {
321 token: Token::new(token.to_vec()),
322 });
323 }
324
325 None
326 }
327
328 pub fn to_header_value(&self) -> HeaderValue {
332 match self {
333 Self::Basic { .. } => {
334 let mut buf = b"Basic ".to_vec();
336 {
337 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
338 write!(encoder, "{}:", self.username().unwrap_or_default())
339 .expect("Write to base64 encoder should succeed");
340 if let Some(password) = self.password() {
341 write!(encoder, "{password}")
342 .expect("Write to base64 encoder should succeed");
343 }
344 }
345 let mut header =
346 HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
347 header.set_sensitive(true);
348 header
349 }
350 Self::Bearer { token } => {
351 let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
352 .expect("Bearer token is always valid HeaderValue");
353 header.set_sensitive(true);
354 header
355 }
356 }
357 }
358
359 #[must_use]
363 pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
364 if let Some(username) = self.username() {
365 let _ = url.set_username(username);
366 }
367 if let Some(password) = self.password() {
368 let _ = url.set_password(Some(password));
369 }
370 url
371 }
372
373 #[must_use]
377 pub fn authenticate(&self, mut request: Request) -> Request {
378 request
379 .headers_mut()
380 .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
381 request
382 }
383}
384
385#[derive(Clone, Debug)]
386pub(crate) enum Authentication {
387 Credentials(Credentials),
389
390 Signer(DefaultSigner),
392}
393
394impl PartialEq for Authentication {
395 fn eq(&self, other: &Self) -> bool {
396 match (self, other) {
397 (Self::Credentials(a), Self::Credentials(b)) => a == b,
398 (Self::Signer(..), Self::Signer(..)) => true,
399 _ => false,
400 }
401 }
402}
403
404impl Eq for Authentication {}
405
406impl From<Credentials> for Authentication {
407 fn from(credentials: Credentials) -> Self {
408 Self::Credentials(credentials)
409 }
410}
411
412impl From<DefaultSigner> for Authentication {
413 fn from(signer: DefaultSigner) -> Self {
414 Self::Signer(signer)
415 }
416}
417
418impl Authentication {
419 pub(crate) fn password(&self) -> Option<&str> {
421 match self {
422 Self::Credentials(credentials) => credentials.password(),
423 Self::Signer(..) => None,
424 }
425 }
426
427 pub(crate) fn username(&self) -> Option<&str> {
429 match self {
430 Self::Credentials(credentials) => credentials.username(),
431 Self::Signer(..) => None,
432 }
433 }
434
435 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
437 match self {
438 Self::Credentials(credentials) => credentials.as_username(),
439 Self::Signer(..) => Cow::Owned(Username::none()),
440 }
441 }
442
443 pub(crate) fn to_username(&self) -> Username {
445 match self {
446 Self::Credentials(credentials) => credentials.to_username(),
447 Self::Signer(..) => Username::none(),
448 }
449 }
450
451 pub(crate) fn is_authenticated(&self) -> bool {
453 match self {
454 Self::Credentials(credentials) => credentials.is_authenticated(),
455 Self::Signer(..) => true,
456 }
457 }
458
459 pub(crate) fn is_empty(&self) -> bool {
461 match self {
462 Self::Credentials(credentials) => credentials.is_empty(),
463 Self::Signer(..) => false,
464 }
465 }
466
467 #[must_use]
471 pub(crate) async fn authenticate(&self, mut request: Request) -> Request {
472 match self {
473 Self::Credentials(credentials) => credentials.authenticate(request),
474 Self::Signer(signer) => {
475 let uri = Uri::from_str(request.url().as_str()).unwrap();
478 let mut http_req = http::Request::builder()
479 .method(request.method().clone())
480 .uri(uri)
481 .body(())
482 .unwrap();
483 *http_req.headers_mut() = request.headers().clone();
484
485 let (mut parts, ()) = http_req.into_parts();
487 signer
488 .sign(&mut parts, None)
489 .await
490 .expect("AWS signing should succeed");
491
492 request.headers_mut().extend(parts.headers);
494
495 if let Some(path_and_query) = parts.uri.path_and_query() {
497 request.url_mut().set_path(path_and_query.path());
498 request.url_mut().set_query(path_and_query.query());
499 }
500 request
501 }
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use insta::assert_debug_snapshot;
509
510 use super::*;
511
512 #[test]
513 fn from_url_no_credentials() {
514 let url = &Url::parse("https://example.com/simple/first/").unwrap();
515 assert_eq!(Credentials::from_url(url), None);
516 }
517
518 #[test]
519 fn from_url_username_and_password() {
520 let url = &Url::parse("https://example.com/simple/first/").unwrap();
521 let mut auth_url = url.clone();
522 auth_url.set_username("user").unwrap();
523 auth_url.set_password(Some("password")).unwrap();
524 let credentials = Credentials::from_url(&auth_url).unwrap();
525 assert_eq!(credentials.username(), Some("user"));
526 assert_eq!(credentials.password(), Some("password"));
527 }
528
529 #[test]
530 fn from_url_no_username() {
531 let url = &Url::parse("https://example.com/simple/first/").unwrap();
532 let mut auth_url = url.clone();
533 auth_url.set_password(Some("password")).unwrap();
534 let credentials = Credentials::from_url(&auth_url).unwrap();
535 assert_eq!(credentials.username(), None);
536 assert_eq!(credentials.password(), Some("password"));
537 }
538
539 #[test]
540 fn from_url_no_password() {
541 let url = &Url::parse("https://example.com/simple/first/").unwrap();
542 let mut auth_url = url.clone();
543 auth_url.set_username("user").unwrap();
544 let credentials = Credentials::from_url(&auth_url).unwrap();
545 assert_eq!(credentials.username(), Some("user"));
546 assert_eq!(credentials.password(), None);
547 }
548
549 #[test]
550 fn authenticated_request_from_url() {
551 let url = Url::parse("https://example.com/simple/first/").unwrap();
552 let mut auth_url = url.clone();
553 auth_url.set_username("user").unwrap();
554 auth_url.set_password(Some("password")).unwrap();
555 let credentials = Credentials::from_url(&auth_url).unwrap();
556
557 let mut request = Request::new(reqwest::Method::GET, url);
558 request = credentials.authenticate(request);
559
560 let mut header = request
561 .headers()
562 .get(reqwest::header::AUTHORIZATION)
563 .expect("Authorization header should be set")
564 .clone();
565 header.set_sensitive(false);
566
567 assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###);
568 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
569 }
570
571 #[test]
572 fn authenticated_request_from_url_with_percent_encoded_user() {
573 let url = Url::parse("https://example.com/simple/first/").unwrap();
574 let mut auth_url = url.clone();
575 auth_url.set_username("user@domain").unwrap();
576 auth_url.set_password(Some("password")).unwrap();
577 let credentials = Credentials::from_url(&auth_url).unwrap();
578
579 let mut request = Request::new(reqwest::Method::GET, url);
580 request = credentials.authenticate(request);
581
582 let mut header = request
583 .headers()
584 .get(reqwest::header::AUTHORIZATION)
585 .expect("Authorization header should be set")
586 .clone();
587 header.set_sensitive(false);
588
589 assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###);
590 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
591 }
592
593 #[test]
594 fn authenticated_request_from_url_with_percent_encoded_password() {
595 let url = Url::parse("https://example.com/simple/first/").unwrap();
596 let mut auth_url = url.clone();
597 auth_url.set_username("user").unwrap();
598 auth_url.set_password(Some("password==")).unwrap();
599 let credentials = Credentials::from_url(&auth_url).unwrap();
600
601 let mut request = Request::new(reqwest::Method::GET, url);
602 request = credentials.authenticate(request);
603
604 let mut header = request
605 .headers()
606 .get(reqwest::header::AUTHORIZATION)
607 .expect("Authorization header should be set")
608 .clone();
609 header.set_sensitive(false);
610
611 assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###);
612 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
613 }
614
615 #[test]
617 fn test_password_obfuscation() {
618 let credentials =
619 Credentials::basic(Some(String::from("user")), Some(String::from("password")));
620 let debugged = format!("{credentials:?}");
621 assert_eq!(
622 debugged,
623 "Basic { username: Username(Some(\"user\")), password: Some(****) }"
624 );
625 }
626
627 #[test]
628 fn test_bearer_token_obfuscation() {
629 let token = "super_secret_token";
630 let credentials = Credentials::bearer(token.into());
631 let debugged = format!("{credentials:?}");
632 assert!(
633 !debugged.contains(token),
634 "Token should be obfuscated in Debug impl: {debugged}"
635 );
636 }
637}