use std::net::IpAddr;
pub use tropel_sdk::types::{ProxyConfig, ProxyMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BypassRule {
All,
Host(String),
Subdomains(String),
Ip(IpAddr),
Cidr(IpAddr, u8),
}
pub fn parse_bypass(entry: &str) -> Result<BypassRule, String> {
let raw = entry.trim();
if raw.is_empty() {
return Err("an empty bypass entry matches nothing — remove it".to_string());
}
if raw == "*" {
return Ok(BypassRule::All);
}
if let Some(suffix) = raw.strip_prefix("*.") {
if suffix.is_empty() || suffix.contains('*') {
return Err(format!(
"bypass entry {raw:?}: \"*.\" must be followed by a plain suffix"
));
}
return Ok(BypassRule::Subdomains(suffix.to_ascii_lowercase()));
}
if raw.contains('*') {
return Err(format!(
"bypass entry {raw:?}: a wildcard is only allowed as a leading \"*.\" \
(subdomains) or a bare \"*\" (everything)"
));
}
if let Some((net, prefix)) = raw.split_once('/') {
let addr: IpAddr = net
.parse()
.map_err(|_| format!("bypass entry {raw:?}: {net:?} is not an IP address"))?;
let bits: u8 = prefix
.parse()
.map_err(|_| format!("bypass entry {raw:?}: {prefix:?} is not a prefix length"))?;
let max = if addr.is_ipv4() { 32 } else { 128 };
if bits > max {
return Err(format!("bypass entry {raw:?}: /{bits} exceeds /{max}"));
}
return Ok(BypassRule::Cidr(addr, bits));
}
if let Ok(addr) = raw.parse::<IpAddr>() {
return Ok(BypassRule::Ip(addr));
}
Ok(BypassRule::Host(raw.to_ascii_lowercase()))
}
impl BypassRule {
pub fn matches(&self, host: &str) -> bool {
let host = host.trim().to_ascii_lowercase();
match self {
BypassRule::All => true,
BypassRule::Host(want) => &host == want,
BypassRule::Subdomains(suffix) => {
host.len() > suffix.len() + 1
&& host.ends_with(suffix.as_str())
&& host.as_bytes()[host.len() - suffix.len() - 1] == b'.'
}
BypassRule::Ip(want) => host.parse::<IpAddr>().map(|a| a == *want).unwrap_or(false),
BypassRule::Cidr(net, bits) => host
.parse::<IpAddr>()
.map(|addr| in_cidr(addr, *net, *bits))
.unwrap_or(false),
}
}
}
fn in_cidr(addr: IpAddr, net: IpAddr, bits: u8) -> bool {
match (addr, net) {
(IpAddr::V4(a), IpAddr::V4(n)) => prefix_eq(&a.octets(), &n.octets(), bits),
(IpAddr::V6(a), IpAddr::V6(n)) => prefix_eq(&a.octets(), &n.octets(), bits),
_ => false,
}
}
fn prefix_eq(a: &[u8], b: &[u8], bits: u8) -> bool {
let full = (bits / 8) as usize;
if a[..full] != b[..full] {
return false;
}
let rest = bits % 8;
if rest == 0 {
return true;
}
let mask = 0xffu8 << (8 - rest);
a[full] & mask == b[full] & mask
}
pub fn parse_bypass_list(entries: &[String]) -> Result<Vec<BypassRule>, String> {
let mut rules = Vec::new();
let mut errors = Vec::new();
for entry in entries {
match parse_bypass(entry) {
Ok(rule) => rules.push(rule),
Err(e) => errors.push(e),
}
}
if errors.is_empty() {
Ok(rules)
} else {
Err(errors.join("; "))
}
}
pub fn bypasses(rules: &[BypassRule], host: &str) -> bool {
rules.iter().any(|r| r.matches(host))
}
pub fn system_proxy_url() -> Option<String> {
for key in ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] {
if let Ok(value) = std::env::var(key) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
pub fn system_no_proxy() -> Vec<String> {
for key in ["NO_PROXY", "no_proxy"] {
if let Ok(value) = std::env::var(key) {
let entries: Vec<String> = value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !entries.is_empty() {
return entries;
}
}
}
Vec::new()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PacDirective {
Direct,
Proxy(String),
Socks(String),
}
impl PacDirective {
pub fn proxy_url(&self) -> Option<String> {
match self {
PacDirective::Direct => None,
PacDirective::Proxy(target) => Some(format!("http://{target}")),
PacDirective::Socks(target) => Some(format!("socks5://{target}")),
}
}
}
pub fn parse_pac_result(result: &str) -> Vec<PacDirective> {
result
.split(';')
.filter_map(|candidate| parse_pac_directive(candidate.trim()))
.collect()
}
fn parse_pac_directive(directive: &str) -> Option<PacDirective> {
if directive.is_empty() {
return None;
}
let mut parts = directive.split_whitespace();
let keyword = parts.next()?.to_ascii_uppercase();
let target = parts.next();
match (keyword.as_str(), target) {
("DIRECT", _) => Some(PacDirective::Direct),
("PROXY", Some(t)) | ("HTTP", Some(t)) => Some(PacDirective::Proxy(t.to_string())),
("HTTPS", Some(t)) => Some(PacDirective::Proxy(t.to_string())),
("SOCKS", Some(t)) | ("SOCKS4", Some(t)) | ("SOCKS5", Some(t)) => {
Some(PacDirective::Socks(t.to_string()))
}
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct PacDecision {
pub candidates: Vec<PacDirective>,
pub current: usize,
pub decided_at: std::time::Instant,
}
impl PacDecision {
pub fn new(candidates: Vec<PacDirective>) -> Self {
Self {
candidates,
current: 0,
decided_at: std::time::Instant::now(),
}
}
pub fn candidate(&self) -> Option<&PacDirective> {
self.candidates.get(self.current)
}
pub fn advance(&mut self) -> Option<&PacDirective> {
self.current += 1;
self.candidate()
}
pub fn exhausted(&self) -> bool {
self.current >= self.candidates.len()
}
pub fn is_stale(&self, ttl: std::time::Duration) -> bool {
self.decided_at.elapsed() >= ttl
}
}
pub const DEFAULT_PAC_TTL: std::time::Duration = std::time::Duration::from_secs(300);