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}