1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use regex::RegexSet;
use thiserror::Error;

use crate::{BasicAuthCredentials, BasicAuthSelector, Uri};

#[derive(Debug, Error)]
pub enum BasicAuthExtractorError {
    #[error("RegexSet error")]
    RegexSetError(#[from] regex::Error),
}

/// Extracts basic auth credentials from a given URI.
/// Credentials are extracted if the URI matches one of the provided
/// [`BasicAuthSelector`] instances.
#[derive(Debug, Clone)]
pub struct BasicAuthExtractor {
    credentials: Vec<BasicAuthCredentials>,
    regex_set: RegexSet,
}

impl BasicAuthExtractor {
    /// Creates a new [`BasicAuthExtractor`] from a list of [`BasicAuthSelector`]
    /// instances.
    ///
    /// # Errors
    ///
    /// Returns an error if the provided [`BasicAuthSelector`] instances contain
    /// invalid regular expressions.
    ///
    /// # Examples
    ///
    /// ```
    /// use lychee_lib::{BasicAuthExtractor, BasicAuthSelector};
    /// use std::str::FromStr;
    ///
    /// let selectors = vec![
    ///    BasicAuthSelector::from_str("http://example.com foo:bar").unwrap(),
    /// ];
    ///
    /// let extractor = BasicAuthExtractor::new(selectors).unwrap();
    /// ```
    pub fn new<T: AsRef<[BasicAuthSelector]>>(
        selectors: T,
    ) -> Result<Self, BasicAuthExtractorError> {
        let mut raw_uri_regexes = Vec::new();
        let mut credentials = Vec::new();

        for selector in selectors.as_ref() {
            raw_uri_regexes.push(selector.raw_uri_regex.clone());
            credentials.push(selector.credentials.clone());
        }

        let regex_set = RegexSet::new(raw_uri_regexes)?;

        Ok(Self {
            credentials,
            regex_set,
        })
    }

    /// Matches the provided URI against the [`RegexSet`] and returns
    /// [`BasicAuthCredentials`] if the a match was found. It should be noted
    /// that only the first match will be used to return the appropriate
    /// credentials.
    pub(crate) fn matches(&self, uri: &Uri) -> Option<BasicAuthCredentials> {
        let matches: Vec<_> = self.regex_set.matches(uri.as_str()).into_iter().collect();

        if matches.is_empty() {
            return None;
        }

        Some(self.credentials[matches[0]].clone())
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::*;

    #[test]
    fn test_basic_auth_extractor_new() {
        let selector_str = "http://example.com foo:bar";
        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
        let extractor = BasicAuthExtractor::new([selector]).unwrap();

        assert_eq!(extractor.credentials.len(), 1);
        assert_eq!(extractor.credentials[0].username, "foo");
        assert_eq!(extractor.credentials[0].password, "bar");
    }

    #[test]
    fn test_basic_auth_extractor_matches() {
        let selector_str = "http://example.com foo:bar";
        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
        let extractor = BasicAuthExtractor::new([selector]).unwrap();

        let uri = Uri::try_from("http://example.com").unwrap();
        let credentials = extractor.matches(&uri).unwrap();

        assert_eq!(credentials.username, "foo");
        assert_eq!(credentials.password, "bar");
    }

    #[test]
    fn test_basic_auth_extractor_matches_multiple() {
        let example_com = BasicAuthSelector::from_str("http://example.com foo1:bar1").unwrap();
        let example_org = BasicAuthSelector::from_str("http://example.org foo2:bar2").unwrap();
        let extractor = BasicAuthExtractor::new([example_com, example_org]).unwrap();

        let uri = Uri::try_from("http://example.org").unwrap();
        let credentials = extractor.matches(&uri).unwrap();

        assert_eq!(credentials.username, "foo2");
        assert_eq!(credentials.password, "bar2");
    }

    #[test]
    fn test_basic_auth_regex_match() {
        let selector_str = "https?://example.com/(.*)/bar foo:bar";
        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
        let extractor = BasicAuthExtractor::new([selector]).unwrap();

        let uri = Uri::try_from("http://example.com/foo/bar").unwrap();
        let credentials = extractor.matches(&uri).unwrap();

        assert_eq!(credentials.username, "foo");
        assert_eq!(credentials.password, "bar");

        let uri = Uri::try_from("https://example.com/baz/bar").unwrap();
        let credentials = extractor.matches(&uri).unwrap();

        assert_eq!(credentials.username, "foo");
        assert_eq!(credentials.password, "bar");
    }

    #[test]
    fn test_basic_auth_first_match_wins() {
        let example_com = BasicAuthSelector::from_str("http://example.com foo1:bar1").unwrap();
        let example_org = BasicAuthSelector::from_str("http://example.com foo2:bar2").unwrap();
        let extractor = BasicAuthExtractor::new([example_com, example_org]).unwrap();

        let uri = Uri::try_from("http://example.com").unwrap();
        let credentials = extractor.matches(&uri).unwrap();

        assert_eq!(credentials.username, "foo1");
        assert_eq!(credentials.password, "bar1");
    }

    #[test]
    fn test_basic_auth_extractor_no_match() {
        let selector_str = "http://example.com foo:bar";
        let selector = BasicAuthSelector::from_str(selector_str).unwrap();
        let extractor = BasicAuthExtractor::new([selector]).unwrap();

        let uri = Uri::try_from("http://test.com").unwrap();
        let credentials = extractor.matches(&uri);

        assert!(credentials.is_none());
    }
}