tuwunel_core/config/
proxy.rs1use reqwest::{Proxy, Url};
2use serde::Deserialize;
3
4use crate::Result;
5
6#[derive(Clone, Default, Debug, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ProxyConfig {
34 #[default]
35 None,
36 Global {
37 #[serde(deserialize_with = "crate::utils::deserialize_from_str")]
38 url: Url,
39 },
40 ByDomain(Vec<PartialProxyConfig>),
41}
42impl ProxyConfig {
43 pub fn to_proxy(&self) -> Result<Option<Proxy>> {
44 Ok(match self.clone() {
45 | Self::None => None,
46 | Self::Global { url } => Some(Proxy::all(url)?),
47 | Self::ByDomain(proxies) => Some(Proxy::custom(move |url| {
48 proxies
50 .iter()
51 .find_map(|proxy| proxy.for_url(url))
52 .cloned()
53 })),
54 })
55 }
56}
57
58#[derive(Clone, Debug, Deserialize)]
59pub struct PartialProxyConfig {
60 #[serde(deserialize_with = "crate::utils::deserialize_from_str")]
61 url: Url,
62 #[serde(default)]
63 include: Vec<WildCardedDomain>,
64 #[serde(default)]
65 exclude: Vec<WildCardedDomain>,
66}
67impl PartialProxyConfig {
68 #[must_use]
69 pub fn for_url(&self, url: &Url) -> Option<&Url> {
70 let domain = url.domain()?;
71 let mut included_because = None; let mut excluded_because = None; if self.include.is_empty() {
74 included_because = Some(&WildCardedDomain::WildCard);
76 }
77 for wc_domain in &self.include {
78 if wc_domain.matches(domain) {
79 match included_because {
80 | Some(prev) if !wc_domain.more_specific_than(prev) => (),
81 | _ => included_because = Some(wc_domain),
82 }
83 }
84 }
85 for wc_domain in &self.exclude {
86 if wc_domain.matches(domain) {
87 match excluded_because {
88 | Some(prev) if !wc_domain.more_specific_than(prev) => (),
89 | _ => excluded_because = Some(wc_domain),
90 }
91 }
92 }
93 match (included_because, excluded_because) {
94 | (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url),
95 | (Some(_), None) => Some(&self.url),
96 | _ => None,
97 }
98 }
99}
100
101#[derive(Clone, Debug)]
103enum WildCardedDomain {
104 WildCard,
105 WildCarded(String),
106 Exact(String),
107}
108impl WildCardedDomain {
109 fn matches(&self, domain: &str) -> bool {
110 match self {
111 | Self::WildCard => true,
112 | Self::WildCarded(d) => domain.ends_with(d),
113 | Self::Exact(d) => domain == d,
114 }
115 }
116
117 fn more_specific_than(&self, other: &Self) -> bool {
118 match (self, other) {
119 | (Self::WildCard, Self::WildCard) => false,
120 | (_, Self::WildCard) => true,
121 | (Self::Exact(a), Self::WildCarded(_)) => other.matches(a),
122 | (Self::WildCarded(a), Self::WildCarded(b)) => a != b && a.ends_with(b),
123 | _ => false,
124 }
125 }
126}
127impl std::str::FromStr for WildCardedDomain {
128 type Err = std::convert::Infallible;
129
130 #[expect(clippy::string_slice)]
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
132 Ok(if s.starts_with("*.") {
134 Self::WildCarded(s[1..].to_owned())
135 } else if s == "*" {
136 Self::WildCarded(String::new())
137 } else {
138 Self::Exact(s.to_owned())
139 })
140 }
141}
142impl<'de> Deserialize<'de> for WildCardedDomain {
143 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144 where
145 D: serde::de::Deserializer<'de>,
146 {
147 crate::utils::deserialize_from_str(deserializer)
148 }
149}