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 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}