lychee_lib/basic_auth/
mod.rs

1use regex::RegexSet;
2use thiserror::Error;
3
4use crate::{BasicAuthCredentials, BasicAuthSelector, Uri};
5
6#[derive(Debug, Error)]
7pub enum BasicAuthExtractorError {
8    #[error("RegexSet error")]
9    RegexSetError(#[from] regex::Error),
10}
11
12/// Extracts basic auth credentials from a given URI.
13/// Credentials are extracted if the URI matches one of the provided
14/// [`BasicAuthSelector`] instances.
15#[derive(Debug, Clone)]
16pub struct BasicAuthExtractor {
17    credentials: Vec<BasicAuthCredentials>,
18    regex_set: RegexSet,
19}
20
21impl BasicAuthExtractor {
22    /// Creates a new [`BasicAuthExtractor`] from a list of [`BasicAuthSelector`]
23    /// instances.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if the provided [`BasicAuthSelector`] instances contain
28    /// invalid regular expressions.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use lychee_lib::{BasicAuthExtractor, BasicAuthSelector};
34    /// use std::str::FromStr;
35    ///
36    /// let selectors = vec![
37    ///    BasicAuthSelector::from_str("http://example.com foo:bar").unwrap(),
38    /// ];
39    ///
40    /// let extractor = BasicAuthExtractor::new(selectors).unwrap();
41    /// ```
42    pub fn new<T: AsRef<[BasicAuthSelector]>>(
43        selectors: T,
44    ) -> Result<Self, BasicAuthExtractorError> {
45        let mut raw_uri_regexes = Vec::new();
46        let mut credentials = Vec::new();
47
48        for selector in selectors.as_ref() {
49            raw_uri_regexes.push(selector.raw_uri_regex.clone());
50            credentials.push(selector.credentials.clone());
51        }
52
53        let regex_set = RegexSet::new(raw_uri_regexes)?;
54
55        Ok(Self {
56            credentials,
57            regex_set,
58        })
59    }
60
61    /// Matches the provided URI against the [`RegexSet`] and returns
62    /// [`BasicAuthCredentials`] if the a match was found. It should be noted
63    /// that only the first match will be used to return the appropriate
64    /// credentials.
65    pub(crate) fn matches(&self, uri: &Uri) -> Option<BasicAuthCredentials> {
66        let matches: Vec<_> = self.regex_set.matches(uri.as_str()).into_iter().collect();
67
68        if matches.is_empty() {
69            return None;
70        }
71
72        Some(self.credentials[matches[0]].clone())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use std::str::FromStr;
79
80    use super::*;
81
82    #[test]
83    fn test_basic_auth_extractor_new() {
84        let selector_str = "http://example.com foo:bar";
85        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
86        let extractor = BasicAuthExtractor::new([selector]).unwrap();
87
88        assert_eq!(extractor.credentials.len(), 1);
89        assert_eq!(extractor.credentials[0].username, "foo");
90        assert_eq!(extractor.credentials[0].password, "bar");
91    }
92
93    #[test]
94    fn test_basic_auth_extractor_matches() {
95        let selector_str = "http://example.com foo:bar";
96        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
97        let extractor = BasicAuthExtractor::new([selector]).unwrap();
98
99        let uri = Uri::try_from("http://example.com").unwrap();
100        let credentials = extractor.matches(&uri).unwrap();
101
102        assert_eq!(credentials.username, "foo");
103        assert_eq!(credentials.password, "bar");
104    }
105
106    #[test]
107    fn test_basic_auth_extractor_matches_multiple() {
108        let example_com = BasicAuthSelector::from_str("http://example.com foo1:bar1").unwrap();
109        let example_org = BasicAuthSelector::from_str("http://example.org foo2:bar2").unwrap();
110        let extractor = BasicAuthExtractor::new([example_com, example_org]).unwrap();
111
112        let uri = Uri::try_from("http://example.org").unwrap();
113        let credentials = extractor.matches(&uri).unwrap();
114
115        assert_eq!(credentials.username, "foo2");
116        assert_eq!(credentials.password, "bar2");
117    }
118
119    #[test]
120    fn test_basic_auth_regex_match() {
121        let selector_str = "https?://example.com/(.*)/bar foo:bar";
122        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
123        let extractor = BasicAuthExtractor::new([selector]).unwrap();
124
125        let uri = Uri::try_from("http://example.com/foo/bar").unwrap();
126        let credentials = extractor.matches(&uri).unwrap();
127
128        assert_eq!(credentials.username, "foo");
129        assert_eq!(credentials.password, "bar");
130
131        let uri = Uri::try_from("https://example.com/baz/bar").unwrap();
132        let credentials = extractor.matches(&uri).unwrap();
133
134        assert_eq!(credentials.username, "foo");
135        assert_eq!(credentials.password, "bar");
136    }
137
138    #[test]
139    fn test_basic_auth_first_match_wins() {
140        let example_com = BasicAuthSelector::from_str("http://example.com foo1:bar1").unwrap();
141        let example_org = BasicAuthSelector::from_str("http://example.com foo2:bar2").unwrap();
142        let extractor = BasicAuthExtractor::new([example_com, example_org]).unwrap();
143
144        let uri = Uri::try_from("http://example.com").unwrap();
145        let credentials = extractor.matches(&uri).unwrap();
146
147        assert_eq!(credentials.username, "foo1");
148        assert_eq!(credentials.password, "bar1");
149    }
150
151    #[test]
152    fn test_basic_auth_extractor_no_match() {
153        let selector_str = "http://example.com foo:bar";
154        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
155        let extractor = BasicAuthExtractor::new([selector]).unwrap();
156
157        let uri = Uri::try_from("http://test.com").unwrap();
158        let credentials = extractor.matches(&uri);
159
160        assert!(credentials.is_none());
161    }
162}