Skip to main content

stygian_proxy/
types.rs

1//! Core domain types for proxy management.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{Duration, Instant};
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// The protocol variant of a proxy endpoint.
10///
11/// # Example
12/// ```
13/// use stygian_proxy::types::ProxyType;
14/// assert_eq!(ProxyType::Http, ProxyType::Http);
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ProxyType {
19    /// Plain HTTP proxy (CONNECT / forwarding).
20    Http,
21    /// HTTPS proxy over TLS.
22    Https,
23    #[cfg(feature = "socks")]
24    /// SOCKS4 proxy (requires the `socks` feature).
25    Socks4,
26    #[cfg(feature = "socks")]
27    /// SOCKS5 proxy (requires the `socks` feature).
28    Socks5,
29}
30
31/// TLS-profiled request mode for proxy-side HTTP operations.
32///
33/// Used by `tls-profiled` integrations to decide how strictly browser TLS
34/// profiles should be mapped onto rustls.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum ProfiledRequestMode {
38    /// Broad compatibility: skip unknown entries and use safe fallbacks.
39    Compatible,
40    /// Profile-aware preset selected from the profile name.
41    Preset,
42    /// Strict cipher-suite mapping with compatibility group fallback.
43    Strict,
44    /// Strict cipher-suite + group mapping without fallback.
45    StrictAll,
46}
47
48/// A proxy endpoint with optional authentication credentials.
49///
50/// `Debug` output masks `password` to prevent accidental credential logging.
51///
52/// # Example
53/// ```
54/// use stygian_proxy::types::{Proxy, ProxyType};
55/// let p = Proxy {
56///     url: "http://proxy.example.com:8080".into(),
57///     proxy_type: ProxyType::Http,
58///     username: Some("alice".into()),
59///     password: Some("secret".into()),
60///     weight: 1,
61///     tags: vec!["prod".into()],
62/// };
63/// let debug = format!("{p:?}");
64/// assert!(debug.contains("***"), "password must be masked in Debug output");
65/// ```
66#[derive(Clone, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub struct Proxy {
69    /// The proxy URL, e.g. `http://proxy.example.com:8080`.
70    pub url: String,
71    pub proxy_type: ProxyType,
72    pub username: Option<String>,
73    pub password: Option<String>,
74    /// Relative selection weight for weighted rotation (default: `1`).
75    pub weight: u32,
76    /// User-defined tags for filtering and grouping.
77    pub tags: Vec<String>,
78}
79
80impl std::fmt::Debug for Proxy {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("Proxy")
83            .field("url", &self.url)
84            .field("proxy_type", &self.proxy_type)
85            .field("username", &self.username)
86            .field("password", &self.password.as_deref().map(|_| "***"))
87            .field("weight", &self.weight)
88            .field("tags", &self.tags)
89            .finish()
90    }
91}
92
93/// A [`Proxy`] with a stable identity and insertion timestamp.
94///
95/// # Example
96/// ```
97/// use stygian_proxy::types::{Proxy, ProxyType, ProxyRecord};
98/// let proxy = Proxy {
99///     url: "http://proxy.example.com:8080".into(),
100///     proxy_type: ProxyType::Http,
101///     username: None,
102///     password: None,
103///     weight: 1,
104///     tags: vec![],
105/// };
106/// let record = ProxyRecord::new(proxy);
107/// assert!(!record.id.is_nil());
108/// ```
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub struct ProxyRecord {
112    pub id: Uuid,
113    pub proxy: Proxy,
114    /// Wall-clock time the proxy was added. Not serialized — `Instant` is
115    /// not meaningfully portable; defaults to `Instant::now()` on deserialization.
116    #[serde(skip, default = "Instant::now")]
117    pub added_at: Instant,
118}
119
120impl ProxyRecord {
121    /// Create a new [`ProxyRecord`] wrapping `proxy` with a freshly generated UUID.
122    pub fn new(proxy: Proxy) -> Self {
123        Self {
124            id: Uuid::new_v4(),
125            proxy,
126            added_at: Instant::now(),
127        }
128    }
129}
130
131/// Per-proxy runtime metrics using lock-free atomic counters.
132///
133/// Intended to be shared via `Arc<ProxyMetrics>`.
134///
135/// # Example
136/// ```
137/// use stygian_proxy::types::ProxyMetrics;
138/// let m = ProxyMetrics::default();
139/// assert_eq!(m.success_rate(), 0.0);
140/// assert_eq!(m.avg_latency_ms(), 0.0);
141/// ```
142#[derive(Debug, Default)]
143pub struct ProxyMetrics {
144    pub requests_total: AtomicU64,
145    pub successes: AtomicU64,
146    pub failures: AtomicU64,
147    pub total_latency_ms: AtomicU64,
148}
149
150impl ProxyMetrics {
151    /// Cast a `u64` counter to `f64` for ratio computation.
152    ///
153    /// `u64` can represent values up to ~1.8 × 10¹⁹; `f64` has 53-bit
154    /// mantissa, so precision loss begins around 9 × 10¹⁵.  For long-running
155    /// proxies that number is never reached in practice, and direct casting
156    /// preserves ratios correctly (unlike saturating to `u32::MAX`).
157    #[allow(clippy::cast_precision_loss)]
158    const fn u64_as_f64(value: u64) -> f64 {
159        value as f64
160    }
161
162    /// Returns the fraction of requests that succeeded, in `[0.0, 1.0]`.
163    ///
164    /// Returns `0.0` when no requests have been recorded.
165    ///
166    /// # Example
167    /// ```
168    /// use stygian_proxy::types::ProxyMetrics;
169    /// use std::sync::atomic::Ordering;
170    /// let m = ProxyMetrics::default();
171    /// m.requests_total.store(10, Ordering::Relaxed);
172    /// m.successes.store(8, Ordering::Relaxed);
173    /// assert!((m.success_rate() - 0.8).abs() < f64::EPSILON);
174    /// ```
175    pub fn success_rate(&self) -> f64 {
176        let total = self.requests_total.load(Ordering::Relaxed);
177        if total == 0 {
178            return 0.0;
179        }
180        Self::u64_as_f64(self.successes.load(Ordering::Relaxed)) / Self::u64_as_f64(total)
181    }
182
183    /// Returns the average request latency in milliseconds.
184    ///
185    /// Returns `0.0` when no requests have been recorded.
186    ///
187    /// # Example
188    /// ```
189    /// use stygian_proxy::types::ProxyMetrics;
190    /// use std::sync::atomic::Ordering;
191    /// let m = ProxyMetrics::default();
192    /// m.requests_total.store(4, Ordering::Relaxed);
193    /// m.total_latency_ms.store(400, Ordering::Relaxed);
194    /// assert!((m.avg_latency_ms() - 100.0).abs() < f64::EPSILON);
195    /// ```
196    pub fn avg_latency_ms(&self) -> f64 {
197        let total = self.requests_total.load(Ordering::Relaxed);
198        if total == 0 {
199            return 0.0;
200        }
201        Self::u64_as_f64(self.total_latency_ms.load(Ordering::Relaxed)) / Self::u64_as_f64(total)
202    }
203}
204
205mod serde_duration_secs {
206    use serde::{Deserialize, Deserializer, Serialize, Serializer};
207    use std::time::Duration;
208
209    pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
210        d.as_secs().serialize(s)
211    }
212
213    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
214        Ok(Duration::from_secs(u64::deserialize(d)?))
215    }
216}
217
218/// Configuration governing health checking and circuit-breaker behaviour.
219///
220/// Duration fields serialize as integer seconds for TOML/JSON compatibility.
221///
222/// # Example
223/// ```
224/// use stygian_proxy::types::ProxyConfig;
225/// use std::time::Duration;
226/// let cfg = ProxyConfig::default();
227/// assert_eq!(cfg.health_check_url, "https://httpbin.org/ip");
228/// assert_eq!(cfg.health_check_interval, Duration::from_secs(60));
229/// assert_eq!(cfg.health_check_timeout, Duration::from_secs(5));
230/// assert_eq!(cfg.circuit_open_threshold, 5);
231/// assert_eq!(cfg.circuit_half_open_after, Duration::from_secs(30));
232/// assert!(cfg.profiled_request_mode.is_none());
233/// ```
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236pub struct ProxyConfig {
237    /// URL called during health checks to verify proxy liveness.
238    pub health_check_url: String,
239    /// How often to run health checks (seconds).
240    #[serde(with = "serde_duration_secs")]
241    pub health_check_interval: Duration,
242    /// Per-probe HTTP timeout (seconds).
243    #[serde(with = "serde_duration_secs")]
244    pub health_check_timeout: Duration,
245    /// Consecutive failures before the circuit trips to OPEN.
246    pub circuit_open_threshold: u32,
247    /// How long to wait in OPEN before transitioning to HALF-OPEN (seconds).
248    #[serde(with = "serde_duration_secs")]
249    pub circuit_half_open_after: Duration,
250    /// Sticky-session policy for domain→proxy binding.
251    #[serde(default)]
252    pub sticky_policy: crate::session::StickyPolicy,
253    /// Optional default mode for TLS-profiled helper clients.
254    ///
255    /// When set and `tls-profiled` is enabled, `ProxyManager` initializes its
256    /// `HealthChecker` with a Chrome-profiled requester using this mode.
257    ///
258    /// Ignored when `tls-profiled` is disabled.
259    #[serde(default)]
260    pub profiled_request_mode: Option<ProfiledRequestMode>,
261}
262
263impl Default for ProxyConfig {
264    fn default() -> Self {
265        Self {
266            health_check_url: "https://httpbin.org/ip".into(),
267            health_check_interval: Duration::from_secs(60),
268            health_check_timeout: Duration::from_secs(5),
269            circuit_open_threshold: 5,
270            circuit_half_open_after: Duration::from_secs(30),
271            sticky_policy: crate::session::StickyPolicy::default(),
272            profiled_request_mode: None,
273        }
274    }
275}