Skip to main content

no_proxy/
lib.rs

1#[cfg(feature = "graphql")]
2use async_graphql::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
3
4#[cfg(feature = "serialize")]
5mod serialize;
6
7use cidr::IpCidr;
8use std::collections::{hash_set::IntoIter, HashSet};
9use std::net::IpAddr;
10use std::str::FromStr;
11
12#[derive(Clone, Debug, Eq, Hash, PartialEq)]
13pub enum NoProxyItem {
14    Wildcard,
15    IpCidr(String, IpCidr),
16    WithDot(String, bool, bool),
17    Plain(String),
18}
19
20#[cfg(feature = "graphql")]
21#[Scalar]
22impl ScalarType for NoProxyItem {
23    fn parse(value: Value) -> InputValueResult<Self> {
24        match value {
25            Value::String(s) => Ok(Self::from(s)),
26            _ => Err(InputValueError::expected_type(value)),
27        }
28    }
29
30    fn is_valid(value: &Value) -> bool {
31        matches!(value, Value::String(_))
32    }
33
34    fn to_value(&self) -> Value {
35        Value::String(self.to_string())
36    }
37}
38
39impl NoProxyItem {
40    fn as_str(&self) -> &str {
41        match self {
42            Self::Wildcard => "*",
43            Self::IpCidr(value, _) | Self::WithDot(value, _, _) | Self::Plain(value) => {
44                value.as_str()
45            }
46        }
47    }
48}
49
50impl std::fmt::Display for NoProxyItem {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(self.as_str())
53    }
54}
55
56impl<V: AsRef<str> + Into<String>> From<V> for NoProxyItem {
57    fn from(value: V) -> Self {
58        let value_str = value.as_ref();
59        if value_str == "*" {
60            Self::Wildcard
61        } else if let Ok(ip_cidr) = IpCidr::from_str(value_str) {
62            Self::IpCidr(value.into(), ip_cidr)
63        } else {
64            let start = value_str.starts_with('.');
65            let end = value_str.ends_with('.');
66            if start || end {
67                Self::WithDot(value.into(), start, end)
68            } else {
69                Self::Plain(value.into())
70            }
71        }
72    }
73}
74
75fn parse_host(input: &str) -> &str {
76    // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off
77    // the end in order to parse correctly
78    if input.starts_with('[') {
79        let x: &[_] = &['[', ']'];
80        input.trim_matches(x)
81    } else {
82        input
83    }
84}
85
86impl NoProxyItem {
87    pub fn matches(&self, value: &str) -> bool {
88        let value = parse_host(value);
89        match self {
90            Self::Wildcard => true,
91            Self::IpCidr(source, ip_cidr) => {
92                if value == source {
93                    true
94                } else if let Ok(ip_value) = IpAddr::from_str(value) {
95                    ip_cidr.contains(&ip_value)
96                } else {
97                    false
98                }
99            }
100            Self::WithDot(source, start, end) => {
101                if *start && *end {
102                    value.contains(source)
103                } else if *start {
104                    value.ends_with(source)
105                } else if *end {
106                    value.starts_with(source)
107                } else {
108                    source == value
109                }
110            }
111            Self::Plain(source) => match value.strip_suffix(source.as_str()) {
112                Some(prefix) => prefix.is_empty() || prefix.ends_with('.'),
113                None => false,
114            },
115        }
116    }
117}
118
119#[derive(Clone, Debug, Default, Eq, PartialEq)]
120#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
121pub struct NoProxy {
122    content: HashSet<NoProxyItem>,
123    has_wildcard: bool,
124}
125
126impl NoProxy {
127    fn from_iterator<V: AsRef<str>, I: Iterator<Item = V>>(iterator: I) -> Self {
128        let content: HashSet<_> = iterator
129            .filter_map(|item| {
130                let short = item.as_ref().trim();
131                if short.is_empty() {
132                    None
133                } else {
134                    Some(NoProxyItem::from(short))
135                }
136            })
137            .collect();
138        let has_wildcard = content.contains(&NoProxyItem::Wildcard);
139        Self {
140            content,
141            has_wildcard,
142        }
143    }
144}
145
146impl<V: AsRef<str>> From<V> for NoProxy {
147    fn from(value: V) -> Self {
148        Self::from_iterator(value.as_ref().split(','))
149    }
150}
151
152impl IntoIterator for NoProxy {
153    type Item = NoProxyItem;
154    type IntoIter = IntoIter<NoProxyItem>;
155
156    fn into_iter(self) -> Self::IntoIter {
157        self.content.into_iter()
158    }
159}
160
161impl Extend<NoProxyItem> for NoProxy {
162    fn extend<T: IntoIterator<Item = NoProxyItem>>(&mut self, iter: T) {
163        self.content.extend(iter);
164        self.has_wildcard = self.content.contains(&NoProxyItem::Wildcard);
165    }
166}
167
168impl NoProxy {
169    pub fn is_empty(&self) -> bool {
170        self.content.is_empty()
171    }
172
173    pub fn matches(&self, input: &str) -> bool {
174        if self.has_wildcard {
175            return true;
176        }
177        self.content.iter().any(|item| item.matches(input))
178    }
179}
180
181impl std::fmt::Display for NoProxy {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        for (index, item) in self.content.iter().enumerate() {
184            if index > 0 {
185                write!(f, ",")?;
186            }
187            item.fmt(f)?;
188        }
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn should_match(pattern: &str, value: &str) {
198        let no_proxy = NoProxy::from(pattern);
199        assert!(
200            no_proxy.matches(value),
201            "{} should match {}",
202            pattern,
203            value
204        );
205    }
206
207    fn shouldnt_match(pattern: &str, value: &str) {
208        let no_proxy = NoProxy::from(pattern);
209        assert!(
210            !no_proxy.matches(value),
211            "{} should not match {}",
212            pattern,
213            value
214        );
215    }
216
217    #[test]
218    fn filter_empty() {
219        let no_proxy = NoProxy::from("");
220        assert!(no_proxy.is_empty());
221    }
222
223    #[test]
224    fn wildcard() {
225        should_match("*", "www.wikipedia.org");
226        should_match("*", "192.168.0.1");
227        should_match("localhost , *", "wikipedia.org");
228    }
229
230    #[test]
231    fn cidr() {
232        should_match("21.19.35.0/24", "21.19.35.4");
233        shouldnt_match("21.19.35.0/24", "127.0.0.1");
234    }
235
236    #[test]
237    fn leading_dot() {
238        should_match(".wikipedia.org", "fr.wikipedia.org");
239        shouldnt_match(".wikipedia.org", "fr.wikipedia.co.uk");
240        shouldnt_match(".wikipedia.org", "wikipedia.org");
241        shouldnt_match(".wikipedia.org", "google.com");
242        should_match(".168.0.1", "192.168.0.1");
243        shouldnt_match(".168.0.1", "192.169.0.1");
244    }
245
246    #[test]
247    fn trailing_dot() {
248        should_match("fr.wikipedia.", "fr.wikipedia.com");
249        should_match("fr.wikipedia.", "fr.wikipedia.org");
250        should_match("fr.wikipedia.", "fr.wikipedia.somewhere.dangerous");
251        shouldnt_match("fr.wikipedia.", "www.google.com");
252        should_match("192.168.0.", "192.168.0.1");
253        shouldnt_match("192.168.0.", "192.169.0.1");
254    }
255
256    #[test]
257    fn white_space() {
258        shouldnt_match("", "localhost");
259        shouldnt_match("", "somewhere.local");
260    }
261
262    #[test]
263    fn combination() {
264        let pattern = "127.0.0.1,localhost,.local,169.254.169.254,fileshare.company.com";
265        should_match(pattern, "localhost");
266        should_match(pattern, "somewhere.local");
267        shouldnt_match(pattern, "local");
268        should_match(pattern, "fileshare.company.com");
269        should_match(pattern, "sub.fileshare.company.com");
270        shouldnt_match(pattern, "other.company.com");
271    }
272
273    #[test]
274    fn from_reqwest() {
275        let pattern = ".foo.bar,bar.baz,10.42.1.0/24,::1,10.124.7.8,2001::/17";
276        shouldnt_match(pattern, "hyper.rs");
277        should_match(pattern, "foo.bar.baz");
278        shouldnt_match(pattern, "10.43.1.1");
279        shouldnt_match(pattern, "10.124.7.7");
280        shouldnt_match(pattern, "[ffff:db8:a0b:12f0::1]");
281        shouldnt_match(pattern, "[2005:db8:a0b:12f0::1]");
282
283        should_match(pattern, "hello.foo.bar");
284        should_match(pattern, "bar.baz");
285        should_match(pattern, "10.42.1.100");
286        should_match(pattern, "[::1]");
287        should_match(pattern, "[2001:db8:a0b:12f0::1]");
288        should_match(pattern, "10.124.7.8");
289    }
290
291    #[test]
292    fn plain_domain_matches_subdomains() {
293        should_match("example.com", "example.com");
294        should_match("example.com", "sub.example.com");
295        should_match("example.com", "deep.sub.example.com");
296        should_match("apps.example.com", "elasticsearch.apps.example.com");
297        shouldnt_match("example.com", "notexample.com");
298        shouldnt_match("example.com", "google.com");
299        shouldnt_match("example.com", "testexample.com");
300    }
301
302    #[test]
303    fn extending() {
304        let mut first = NoProxy::from("foo.bar");
305        let second = NoProxy::from("*");
306        first.extend(second);
307        assert!(first.has_wildcard);
308    }
309}