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 as AwsDefaultSigner;
13use reqsign::google::DefaultSigner as GcsDefaultSigner;
14use reqwest::Request;
15use reqwest::header::HeaderValue;
16use serde::{Deserialize, Serialize};
17use url::Url;
18
19use uv_redacted::DisplaySafeUrl;
20use uv_static::EnvVars;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum Credentials {
24 Basic {
26 username: Username,
28 password: Option<Password>,
30 },
31 Bearer {
33 token: Token,
35 },
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
39#[serde(transparent)]
40pub struct Username(Option<String>);
41
42impl Username {
43 pub(crate) fn new(value: Option<String>) -> Self {
47 Self(value.filter(|s| !s.is_empty()))
49 }
50
51 pub(crate) fn none() -> Self {
52 Self::new(None)
53 }
54
55 pub(crate) fn is_none(&self) -> bool {
56 self.0.is_none()
57 }
58
59 pub(crate) fn is_some(&self) -> bool {
60 self.0.is_some()
61 }
62
63 pub(crate) fn as_deref(&self) -> Option<&str> {
64 self.0.as_deref()
65 }
66}
67
68impl From<String> for Username {
69 fn from(value: String) -> Self {
70 Self::new(Some(value))
71 }
72}
73
74impl From<Option<String>> for Username {
75 fn from(value: Option<String>) -> Self {
76 Self::new(value)
77 }
78}
79
80#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
81#[serde(transparent)]
82pub struct Password(String);
83
84impl Password {
85 pub fn new(password: String) -> Self {
86 Self(password)
87 }
88
89 pub fn as_str(&self) -> &str {
91 self.0.as_str()
92 }
93
94 pub fn into_string(self) -> String {
96 self.0
97 }
98}
99
100impl fmt::Debug for Password {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "****")
103 }
104}
105
106#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
107#[serde(transparent)]
108pub struct Token(Vec<u8>);
109
110impl Token {
111 pub fn new(token: Vec<u8>) -> Self {
112 Self(token)
113 }
114
115 pub fn as_slice(&self) -> &[u8] {
117 self.0.as_slice()
118 }
119
120 pub fn into_bytes(self) -> Vec<u8> {
122 self.0
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.0.is_empty()
128 }
129}
130
131impl fmt::Debug for Token {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "****")
134 }
135}
136impl Credentials {
137 #[allow(dead_code)]
139 pub fn basic(username: Option<String>, password: Option<String>) -> Self {
140 Self::Basic {
141 username: Username::new(username),
142 password: password.map(Password),
143 }
144 }
145
146 #[allow(dead_code)]
148 pub fn bearer(token: Vec<u8>) -> Self {
149 Self::Bearer {
150 token: Token::new(token),
151 }
152 }
153
154 pub fn username(&self) -> Option<&str> {
155 match self {
156 Self::Basic { username, .. } => username.as_deref(),
157 Self::Bearer { .. } => None,
158 }
159 }
160
161 pub(crate) fn to_username(&self) -> Username {
162 match self {
163 Self::Basic { username, .. } => username.clone(),
164 Self::Bearer { .. } => Username::none(),
165 }
166 }
167
168 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
169 match self {
170 Self::Basic { username, .. } => Cow::Borrowed(username),
171 Self::Bearer { .. } => Cow::Owned(Username::none()),
172 }
173 }
174
175 pub fn password(&self) -> Option<&str> {
176 match self {
177 Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
178 Self::Bearer { .. } => None,
179 }
180 }
181
182 pub fn is_authenticated(&self) -> bool {
183 match self {
184 Self::Basic {
185 username: _,
186 password,
187 } => password.is_some(),
188 Self::Bearer { token } => !token.is_empty(),
189 }
190 }
191
192 pub(crate) fn is_empty(&self) -> bool {
193 match self {
194 Self::Basic { username, password } => username.is_none() && password.is_none(),
195 Self::Bearer { token } => token.is_empty(),
196 }
197 }
198
199 pub(crate) fn from_netrc(
203 netrc: &Netrc,
204 url: &DisplaySafeUrl,
205 username: Option<&str>,
206 ) -> Option<Self> {
207 let host = url.host_str()?;
208 let entry = netrc
209 .hosts
210 .get(host)
211 .or_else(|| netrc.hosts.get("default"))?;
212
213 if username.is_some_and(|username| username != entry.login) {
215 return None;
216 }
217
218 Some(Self::Basic {
219 username: Username::new(Some(entry.login.clone())),
220 password: Some(Password(entry.password.clone())),
221 })
222 }
223
224 pub fn from_url(url: &Url) -> Option<Self> {
228 if url.username().is_empty() && url.password().is_none() {
229 return None;
230 }
231 Some(Self::Basic {
232 username: if url.username().is_empty() {
235 None
236 } else {
237 Some(
238 percent_encoding::percent_decode_str(url.username())
239 .decode_utf8()
240 .expect("An encoded username should always decode")
241 .into_owned(),
242 )
243 }
244 .into(),
245 password: url.password().map(|password| {
246 Password(
247 percent_encoding::percent_decode_str(password)
248 .decode_utf8()
249 .expect("An encoded password should always decode")
250 .into_owned(),
251 )
252 }),
253 })
254 }
255
256 pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
261 let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
262 let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
263 if username.is_none() && password.is_none() {
264 None
265 } else {
266 Some(Self::basic(username, password))
267 }
268 }
269
270 pub(crate) fn from_request(request: &Request) -> Option<Self> {
274 Self::from_url(request.url()).or(
276 request
278 .headers()
279 .get(reqwest::header::AUTHORIZATION)
280 .map(Self::from_header_value)?,
281 )
282 }
283
284 pub(crate) fn from_header_value(header: &HeaderValue) -> Option<Self> {
293 if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
295 let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
296 let mut buf = String::new();
297 decoder
298 .read_to_string(&mut buf)
299 .expect("HTTP Basic Authentication should be base64 encoded");
300 let (username, password) = buf
301 .split_once(':')
302 .expect("HTTP Basic Authentication should include a `:` separator");
303 let username = if username.is_empty() {
304 None
305 } else {
306 Some(username.to_string())
307 };
308 let password = if password.is_empty() {
309 None
310 } else {
311 Some(password.to_string())
312 };
313 return Some(Self::Basic {
314 username: Username::new(username),
315 password: password.map(Password),
316 });
317 }
318
319 if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
321 return Some(Self::Bearer {
322 token: Token::new(token.to_vec()),
323 });
324 }
325
326 None
327 }
328
329 pub fn to_header_value(&self) -> HeaderValue {
333 match self {
334 Self::Basic { .. } => {
335 let mut buf = b"Basic ".to_vec();
337 {
338 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
339 write!(encoder, "{}:", self.username().unwrap_or_default())
340 .expect("Write to base64 encoder should succeed");
341 if let Some(password) = self.password() {
342 write!(encoder, "{password}")
343 .expect("Write to base64 encoder should succeed");
344 }
345 }
346 let mut header =
347 HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
348 header.set_sensitive(true);
349 header
350 }
351 Self::Bearer { token } => {
352 let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
353 .expect("Bearer token is always valid HeaderValue");
354 header.set_sensitive(true);
355 header
356 }
357 }
358 }
359
360 #[must_use]
364 pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
365 if let Some(username) = self.username() {
366 let _ = url.set_username(username);
367 }
368 if let Some(password) = self.password() {
369 let _ = url.set_password(Some(password));
370 }
371 url
372 }
373
374 #[must_use]
378 pub fn authenticate(&self, mut request: Request) -> Request {
379 request
380 .headers_mut()
381 .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
382 request
383 }
384}
385
386#[derive(Clone, Debug)]
387pub(crate) enum Authentication {
388 Credentials(Credentials),
390
391 AwsSigner(AwsDefaultSigner),
393
394 GcsSigner(GcsDefaultSigner),
396}
397
398impl PartialEq for Authentication {
399 fn eq(&self, other: &Self) -> bool {
400 match (self, other) {
401 (Self::Credentials(a), Self::Credentials(b)) => a == b,
402 (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
403 (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
404 _ => false,
405 }
406 }
407}
408
409impl Eq for Authentication {}
410
411impl From<Credentials> for Authentication {
412 fn from(credentials: Credentials) -> Self {
413 Self::Credentials(credentials)
414 }
415}
416
417impl From<AwsDefaultSigner> for Authentication {
418 fn from(signer: AwsDefaultSigner) -> Self {
419 Self::AwsSigner(signer)
420 }
421}
422
423impl From<GcsDefaultSigner> for Authentication {
424 fn from(signer: GcsDefaultSigner) -> Self {
425 Self::GcsSigner(signer)
426 }
427}
428
429impl Authentication {
430 pub(crate) fn password(&self) -> Option<&str> {
432 match self {
433 Self::Credentials(credentials) => credentials.password(),
434 Self::AwsSigner(..) | Self::GcsSigner(..) => None,
435 }
436 }
437
438 pub(crate) fn username(&self) -> Option<&str> {
440 match self {
441 Self::Credentials(credentials) => credentials.username(),
442 Self::AwsSigner(..) | Self::GcsSigner(..) => None,
443 }
444 }
445
446 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
448 match self {
449 Self::Credentials(credentials) => credentials.as_username(),
450 Self::AwsSigner(..) | Self::GcsSigner(..) => Cow::Owned(Username::none()),
451 }
452 }
453
454 pub(crate) fn to_username(&self) -> Username {
456 match self {
457 Self::Credentials(credentials) => credentials.to_username(),
458 Self::AwsSigner(..) | Self::GcsSigner(..) => Username::none(),
459 }
460 }
461
462 pub(crate) fn is_authenticated(&self) -> bool {
464 match self {
465 Self::Credentials(credentials) => credentials.is_authenticated(),
466 Self::AwsSigner(..) | Self::GcsSigner(..) => true,
467 }
468 }
469
470 pub(crate) fn is_empty(&self) -> bool {
472 match self {
473 Self::Credentials(credentials) => credentials.is_empty(),
474 Self::AwsSigner(..) | Self::GcsSigner(..) => false,
475 }
476 }
477
478 #[must_use]
482 pub(crate) async fn authenticate(&self, mut request: Request) -> Request {
483 match self {
484 Self::Credentials(credentials) => credentials.authenticate(request),
485 Self::AwsSigner(signer) => {
486 let uri = Uri::from_str(request.url().as_str()).unwrap();
489 let mut http_req = http::Request::builder()
490 .method(request.method().clone())
491 .uri(uri)
492 .body(())
493 .unwrap();
494 *http_req.headers_mut() = request.headers().clone();
495
496 let (mut parts, ()) = http_req.into_parts();
498 signer
499 .sign(&mut parts, None)
500 .await
501 .expect("AWS signing should succeed");
502
503 request.headers_mut().extend(parts.headers);
505
506 if let Some(path_and_query) = parts.uri.path_and_query() {
508 request.url_mut().set_path(path_and_query.path());
509 request.url_mut().set_query(path_and_query.query());
510 }
511 request
512 }
513 Self::GcsSigner(signer) => {
514 let uri = Uri::from_str(request.url().as_str()).unwrap();
517 let mut http_req = http::Request::builder()
518 .method(request.method().clone())
519 .uri(uri)
520 .body(())
521 .unwrap();
522 *http_req.headers_mut() = request.headers().clone();
523
524 let (mut parts, ()) = http_req.into_parts();
526 signer
527 .sign(&mut parts, None)
528 .await
529 .expect("GCS signing should succeed");
530
531 request.headers_mut().extend(parts.headers);
533
534 if let Some(path_and_query) = parts.uri.path_and_query() {
536 request.url_mut().set_path(path_and_query.path());
537 request.url_mut().set_query(path_and_query.query());
538 }
539 request
540 }
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use insta::assert_debug_snapshot;
548
549 use super::*;
550
551 #[test]
552 fn from_url_no_credentials() {
553 let url = &Url::parse("https://example.com/simple/first/").unwrap();
554 assert_eq!(Credentials::from_url(url), None);
555 }
556
557 #[test]
558 fn from_url_username_and_password() {
559 let url = &Url::parse("https://example.com/simple/first/").unwrap();
560 let mut auth_url = url.clone();
561 auth_url.set_username("user").unwrap();
562 auth_url.set_password(Some("password")).unwrap();
563 let credentials = Credentials::from_url(&auth_url).unwrap();
564 assert_eq!(credentials.username(), Some("user"));
565 assert_eq!(credentials.password(), Some("password"));
566 }
567
568 #[test]
569 fn from_url_no_username() {
570 let url = &Url::parse("https://example.com/simple/first/").unwrap();
571 let mut auth_url = url.clone();
572 auth_url.set_password(Some("password")).unwrap();
573 let credentials = Credentials::from_url(&auth_url).unwrap();
574 assert_eq!(credentials.username(), None);
575 assert_eq!(credentials.password(), Some("password"));
576 }
577
578 #[test]
583 fn from_url_empty_username_with_password() {
584 let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
586 let credentials = Credentials::from_url(&url).unwrap();
587 assert_eq!(credentials.username(), None);
588 assert_eq!(credentials.password(), Some("token"));
589 assert!(
590 credentials.is_authenticated(),
591 "URL with empty username but password should be considered authenticated"
592 );
593 }
594
595 #[test]
596 fn from_url_no_password() {
597 let url = &Url::parse("https://example.com/simple/first/").unwrap();
598 let mut auth_url = url.clone();
599 auth_url.set_username("user").unwrap();
600 let credentials = Credentials::from_url(&auth_url).unwrap();
601 assert_eq!(credentials.username(), Some("user"));
602 assert_eq!(credentials.password(), None);
603 }
604
605 #[test]
606 fn authenticated_request_from_url() {
607 let url = Url::parse("https://example.com/simple/first/").unwrap();
608 let mut auth_url = url.clone();
609 auth_url.set_username("user").unwrap();
610 auth_url.set_password(Some("password")).unwrap();
611 let credentials = Credentials::from_url(&auth_url).unwrap();
612
613 let mut request = Request::new(reqwest::Method::GET, url);
614 request = credentials.authenticate(request);
615
616 let mut header = request
617 .headers()
618 .get(reqwest::header::AUTHORIZATION)
619 .expect("Authorization header should be set")
620 .clone();
621 header.set_sensitive(false);
622
623 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
624 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
625 }
626
627 #[test]
628 fn authenticated_request_from_url_with_percent_encoded_user() {
629 let url = Url::parse("https://example.com/simple/first/").unwrap();
630 let mut auth_url = url.clone();
631 auth_url.set_username("user@domain").unwrap();
632 auth_url.set_password(Some("password")).unwrap();
633 let credentials = Credentials::from_url(&auth_url).unwrap();
634
635 let mut request = Request::new(reqwest::Method::GET, url);
636 request = credentials.authenticate(request);
637
638 let mut header = request
639 .headers()
640 .get(reqwest::header::AUTHORIZATION)
641 .expect("Authorization header should be set")
642 .clone();
643 header.set_sensitive(false);
644
645 assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
646 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
647 }
648
649 #[test]
650 fn authenticated_request_from_url_with_percent_encoded_password() {
651 let url = Url::parse("https://example.com/simple/first/").unwrap();
652 let mut auth_url = url.clone();
653 auth_url.set_username("user").unwrap();
654 auth_url.set_password(Some("password==")).unwrap();
655 let credentials = Credentials::from_url(&auth_url).unwrap();
656
657 let mut request = Request::new(reqwest::Method::GET, url);
658 request = credentials.authenticate(request);
659
660 let mut header = request
661 .headers()
662 .get(reqwest::header::AUTHORIZATION)
663 .expect("Authorization header should be set")
664 .clone();
665 header.set_sensitive(false);
666
667 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
668 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
669 }
670
671 #[test]
673 fn test_password_obfuscation() {
674 let credentials =
675 Credentials::basic(Some(String::from("user")), Some(String::from("password")));
676 let debugged = format!("{credentials:?}");
677 assert_eq!(
678 debugged,
679 "Basic { username: Username(Some(\"user\")), password: Some(****) }"
680 );
681 }
682
683 #[test]
684 fn test_bearer_token_obfuscation() {
685 let token = "super_secret_token";
686 let credentials = Credentials::bearer(token.into());
687 let debugged = format!("{credentials:?}");
688 assert!(
689 !debugged.contains(token),
690 "Token should be obfuscated in Debug impl: {debugged}"
691 );
692 }
693}