use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProxyType {
Http,
Https,
#[cfg(feature = "socks")]
Socks4,
#[cfg(feature = "socks")]
Socks5,
CdnEdge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfiledRequestMode {
Compatible,
Preset,
Strict,
StrictAll,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ProxyCapabilities {
#[serde(default)]
pub supports_https_connect: bool,
#[serde(default)]
pub supports_socks5_udp: bool,
#[serde(default)]
pub supports_http3_tunnel: bool,
#[serde(default)]
pub geo_country: Option<String>,
#[serde(default)]
pub geo_confidence: Option<f32>,
#[serde(default)]
pub is_cdn_edge: bool,
#[serde(default)]
pub cdn_provider: Option<String>,
#[serde(default)]
pub tls_profile: Option<String>,
}
impl ProxyCapabilities {
pub fn satisfies(&self, req: &CapabilityRequirement) -> bool {
if req.require_https_connect && !self.supports_https_connect {
return false;
}
if req.require_socks5_udp && !self.supports_socks5_udp {
return false;
}
if req.require_http3_tunnel && !self.supports_http3_tunnel {
return false;
}
if let Some(ref required_country) = req.require_geo_country
&& self.geo_country.as_deref() != Some(required_country.as_str())
{
return false;
}
if req.require_cdn_edge && !self.is_cdn_edge {
return false;
}
if let Some(ref required_profile) = req.require_tls_profile
&& self.tls_profile.as_deref() != Some(required_profile.as_str())
{
return false;
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CapabilityRequirement {
#[serde(default)]
pub require_https_connect: bool,
#[serde(default)]
pub require_socks5_udp: bool,
#[serde(default)]
pub require_http3_tunnel: bool,
#[serde(default)]
pub require_geo_country: Option<String>,
#[serde(default)]
pub require_cdn_edge: bool,
#[serde(default)]
pub require_tls_profile: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingPath {
H1H2OverTcp,
H3OverUdp,
PersistentTcp,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Proxy {
pub url: String,
pub proxy_type: ProxyType,
pub username: Option<String>,
pub password: Option<String>,
pub weight: u32,
pub tags: Vec<String>,
#[serde(default)]
pub capabilities: ProxyCapabilities,
}
impl std::fmt::Debug for Proxy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Proxy")
.field("url", &self.url)
.field("proxy_type", &self.proxy_type)
.field("username", &self.username)
.field("password", &self.password.as_deref().map(|_| "***"))
.field("weight", &self.weight)
.field("tags", &self.tags)
.field("capabilities", &self.capabilities)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ProxyRecord {
pub id: Uuid,
pub proxy: Proxy,
#[serde(skip, default = "Instant::now")]
pub added_at: Instant,
}
impl ProxyRecord {
pub fn new(proxy: Proxy) -> Self {
Self {
id: Uuid::new_v4(),
proxy,
added_at: Instant::now(),
}
}
}
#[derive(Debug, Default)]
pub struct ProxyMetrics {
pub requests_total: AtomicU64,
pub successes: AtomicU64,
pub failures: AtomicU64,
pub total_latency_ms: AtomicU64,
}
impl ProxyMetrics {
#[allow(clippy::cast_precision_loss)]
const fn u64_as_f64(value: u64) -> f64 {
value as f64
}
pub fn success_rate(&self) -> f64 {
let total = self.requests_total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
Self::u64_as_f64(self.successes.load(Ordering::Relaxed)) / Self::u64_as_f64(total)
}
pub fn avg_latency_ms(&self) -> f64 {
let total = self.requests_total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
Self::u64_as_f64(self.total_latency_ms.load(Ordering::Relaxed)) / Self::u64_as_f64(total)
}
}
mod serde_duration_secs {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
d.as_secs().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
Ok(Duration::from_secs(u64::deserialize(d)?))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ProxyConfig {
pub health_check_url: String,
#[serde(with = "serde_duration_secs")]
pub health_check_interval: Duration,
#[serde(with = "serde_duration_secs")]
pub health_check_timeout: Duration,
#[serde(default = "default_health_check_jitter_pct")]
pub health_check_jitter_pct: f32,
pub circuit_open_threshold: u32,
#[serde(with = "serde_duration_secs")]
pub circuit_half_open_after: Duration,
#[serde(default)]
pub sticky_policy: crate::session::StickyPolicy,
#[serde(default)]
pub profiled_request_mode: Option<ProfiledRequestMode>,
#[serde(default)]
pub max_requests_per_connection: Option<u32>,
#[serde(default)]
pub connection_max_age_secs: Option<u64>,
}
const fn default_health_check_jitter_pct() -> f32 {
0.20
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
health_check_url: "https://httpbin.org/ip".into(),
health_check_interval: Duration::from_mins(1),
health_check_timeout: Duration::from_secs(5),
health_check_jitter_pct: 0.20,
circuit_open_threshold: 5,
circuit_half_open_after: Duration::from_secs(30),
sticky_policy: crate::session::StickyPolicy::default(),
profiled_request_mode: None,
max_requests_per_connection: None,
connection_max_age_secs: None,
}
}
}