From 20a515e2b17b24fbfc3743d614d1210ab03d8407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Drouet?= Date: Thu, 22 Jul 2021 09:32:36 +0200 Subject: [PATCH] feat: first commit --- .github/workflows/testing.yml | 18 +++ .gitignore | 1 + Cargo.lock | 101 +++++++++++++++++ Cargo.toml | 10 ++ readme.md | 16 +++ src/lib.rs | 203 ++++++++++++++++++++++++++++++++++ 6 files changed, 349 insertions(+) create mode 100644 .github/workflows/testing.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 readme.md create mode 100644 src/lib.rs diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..f227a66 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,18 @@ +name: Testing the library + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: annotate commit with clippy warnings + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + - name: execute lib tests + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9fdfc29 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,101 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "cidr-utils" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390f6ff81e5cca35a3d60953fcbfe7989162ddcdc6be20a45c5d002a19038c4f" +dependencies = [ + "debug-helper", + "num-bigint", + "num-traits", + "once_cell", + "regex", +] + +[[package]] +name = "debug-helper" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fbd10dce159c002b9c688ae8ab7cd531151e185e0ad360f4bfea3b0eede3a8" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "no-proxy" +version = "0.1.0" +dependencies = [ + "cidr-utils", +] + +[[package]] +name = "num-bigint" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d90cad1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "no-proxy" +version = "0.1.0" +authors = ["Jérémie Drouet "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cidr-utils = "^0.5" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..33e1cfc --- /dev/null +++ b/readme.md @@ -0,0 +1,16 @@ +# no proxy + +This crate is a simple `NO_PROXY` parser and evaluator. It follows [this article from Gitlab](https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/) +on how to properly implement it. + +## Usage + +```rust +use no_proxy::NoProxy; + +let no_proxy = NoProxy::from(".foo.bar,bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17"); +if no_proxy.matches("bar.baz") { + println!("matches 'bar.baz'"); +} +``` + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0c60aa6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,203 @@ +use cidr_utils::cidr::IpCidr; +use std::collections::{hash_set::IntoIter, HashSet}; +use std::net::IpAddr; +use std::str::FromStr; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum NoProxyItem { + Wildcard, + IpCidr(String, IpCidr), + WithDot(String, bool, bool), + Plain(String), +} + +impl From for NoProxyItem { + fn from(value: String) -> Self { + if value == "*" { + Self::Wildcard + } else if let Ok(ip_cidr) = IpCidr::from_str(&value) { + Self::IpCidr(value, ip_cidr) + } else if value.starts_with('.') || value.ends_with('.') { + let start = value.starts_with('.'); + let end = value.ends_with('.'); + Self::WithDot(value, start, end) + } else { + Self::Plain(value) + } + } +} + +fn parse_host(input: &str) -> &str { + // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off + // the end in order to parse correctly + if input.starts_with('[') { + let x: &[_] = &['[', ']']; + input.trim_matches(x) + } else { + input + } +} + +impl NoProxyItem { + pub fn matches(&self, value: &str) -> bool { + let value = parse_host(value); + match self { + Self::Wildcard => true, + Self::IpCidr(source, ip_cidr) => { + if value == source { + true + } else if let Ok(ip_value) = IpAddr::from_str(value) { + ip_cidr.contains(ip_value) + } else { + false + } + } + Self::WithDot(source, start, end) => { + if *start && *end { + value.contains(source) + } else if *start { + value.ends_with(source) + } else if *end { + value.starts_with(source) + } else { + source == value + } + } + Self::Plain(source) => source == value, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct NoProxy { + content: HashSet, + has_wildcard: bool, +} + +impl> From for NoProxy { + fn from(value: T) -> Self { + let content: HashSet<_> = value + .as_ref() + .split(',') + .map(|item| NoProxyItem::from(item.trim().to_string())) + .collect(); + let has_wildcard = content.contains(&NoProxyItem::Wildcard); + Self { + content, + has_wildcard, + } + } +} + +impl IntoIterator for NoProxy { + type Item = NoProxyItem; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.content.into_iter() + } +} + +impl Extend for NoProxy { + fn extend>(&mut self, iter: T) { + self.content.extend(iter); + self.has_wildcard = self.content.contains(&NoProxyItem::Wildcard); + } +} + +impl NoProxy { + pub fn matches(&self, input: &str) -> bool { + if self.has_wildcard { + return true; + } + for item in self.content.iter() { + if item.matches(input) { + return true; + } + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn should_match(pattern: &str, value: &str) { + let no_proxy = NoProxy::from(pattern); + assert!( + no_proxy.matches(value), + "{} should match {}", + pattern, + value + ); + } + + fn shouldnt_match(pattern: &str, value: &str) { + let no_proxy = NoProxy::from(pattern); + assert!( + !no_proxy.matches(value), + "{} should not match {}", + pattern, + value + ); + } + + #[test] + fn wildcard() { + should_match("*", "www.wikipedia.org"); + should_match("*", "192.168.0.1"); + should_match("localhost , *", "wikipedia.org"); + } + + #[test] + fn cidr() { + should_match("21.19.35.40/24", "21.19.35.4"); + shouldnt_match("21.19.35.40/24", "127.0.0.1"); + } + + #[test] + fn leading_dot() { + should_match(".wikipedia.org", "fr.wikipedia.org"); + shouldnt_match(".wikipedia.org", "fr.wikipedia.co.uk"); + shouldnt_match(".wikipedia.org", "wikipedia.org"); + shouldnt_match(".wikipedia.org", "google.com"); + should_match(".168.0.1", "192.168.0.1"); + shouldnt_match(".168.0.1", "192.169.0.1"); + } + + #[test] + fn trailing_dot() { + should_match("fr.wikipedia.", "fr.wikipedia.com"); + should_match("fr.wikipedia.", "fr.wikipedia.org"); + should_match("fr.wikipedia.", "fr.wikipedia.somewhere.dangerous"); + shouldnt_match("fr.wikipedia.", "www.google.com"); + should_match("192.168.0.", "192.168.0.1"); + shouldnt_match("192.168.0.", "192.169.0.1"); + } + + #[test] + fn combination() { + let pattern = "127.0.0.1,localhost,.local,169.254.169.254,fileshare.company.com"; + should_match(pattern, "localhost"); + should_match(pattern, "somewhere.local"); + } + + #[test] + fn from_reqwest() { + let pattern = ".foo.bar,bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17"; + shouldnt_match(pattern, "hyper.rs"); + shouldnt_match(pattern, "foo.bar.baz"); + shouldnt_match(pattern, "10.43.1.1"); + shouldnt_match(pattern, "10.124.7.7"); + shouldnt_match(pattern, "[ffff:db8:a0b:12f0::1]"); + shouldnt_match(pattern, "[2005:db8:a0b:12f0::1]"); + + should_match(pattern, "hello.foo.bar"); + should_match(pattern, "bar.baz"); + should_match(pattern, "10.42.1.100"); + should_match(pattern, "[::1]"); + should_match(pattern, "[2001:db8:a0b:12f0::1]"); + should_match(pattern, "10.124.7.8"); + } +}