use std::sync::Arc;
use async_trait::async_trait;
use crate::error::ProxyResult;
use crate::manager::{ProxyHandle, ProxyManager};
#[async_trait]
pub trait BrowserProxySource: Send + Sync + 'static {
async fn bind_proxy(&self) -> ProxyResult<(String, ProxyHandle)>;
}
pub struct ProxyManagerBridge {
manager: Arc<ProxyManager>,
}
impl ProxyManagerBridge {
#[must_use]
pub const fn new(manager: Arc<ProxyManager>) -> Self {
Self { manager }
}
}
impl std::fmt::Debug for ProxyManagerBridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProxyManagerBridge").finish_non_exhaustive()
}
}
#[async_trait]
impl BrowserProxySource for ProxyManagerBridge {
async fn bind_proxy(&self) -> ProxyResult<(String, ProxyHandle)> {
let handle = self.manager.acquire_proxy().await?;
let url = handle.proxy_url.clone();
Ok((url, handle))
}
}
struct ProxyLeaseAdapter(ProxyHandle);
impl stygian_browser::proxy::ProxyLease for ProxyLeaseAdapter {
fn mark_success(&self) {
self.0.mark_success();
}
}
#[async_trait]
impl stygian_browser::proxy::ProxySource for ProxyManagerBridge {
async fn bind_proxy(
&self,
) -> stygian_browser::error::Result<(String, Box<dyn stygian_browser::proxy::ProxyLease>)> {
let handle = self.manager.acquire_proxy().await.map_err(|e| {
stygian_browser::error::BrowserError::ProxyUnavailable {
reason: e.to_string(),
}
})?;
let url = handle.proxy_url.clone();
Ok((url, Box::new(ProxyLeaseAdapter(handle))))
}
async fn bind_proxy_with_tls_profile(
&self,
profile: Option<&str>,
) -> stygian_browser::error::Result<(String, Box<dyn stygian_browser::proxy::ProxyLease>)> {
let Some(profile) = profile else {
return stygian_browser::proxy::ProxySource::bind_proxy(self).await;
};
let req = crate::types::CapabilityRequirement {
require_tls_profile: Some(profile.to_string()),
..Default::default()
};
let handle = self
.manager
.acquire_with_capabilities(&req)
.await
.map_err(|e| stygian_browser::error::BrowserError::ProxyUnavailable {
reason: e.to_string(),
})?;
let url = handle.proxy_url.clone();
Ok((url, Box::new(ProxyLeaseAdapter(handle))))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::storage::MemoryProxyStore;
use crate::types::{Proxy, ProxyConfig, ProxyType};
fn make_proxy(url: &str) -> Proxy {
Proxy {
url: url.into(),
proxy_type: ProxyType::Http,
username: None,
password: None,
weight: 1,
tags: vec![],
capabilities: crate::types::ProxyCapabilities::default(),
ip_class: crate::types::IpClass::Unknown,
target_compatibility: crate::types::TargetVendorCompatibility::default(),
}
}
#[tokio::test]
async fn bridge_returns_proxy_url_and_handle() -> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryProxyStore::default());
let mgr = Arc::new(ProxyManager::with_round_robin(
storage.clone(),
ProxyConfig::default(),
)?);
mgr.add_proxy(make_proxy("http://p.test:8080")).await?;
let bridge = ProxyManagerBridge::new(mgr);
let (url, handle) = bridge.bind_proxy().await?;
assert_eq!(url, "http://p.test:8080");
handle.mark_success();
Ok(())
}
#[tokio::test]
async fn crash_records_failure() -> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryProxyStore::default());
let mgr = Arc::new(ProxyManager::with_round_robin(
storage.clone(),
ProxyConfig {
circuit_open_threshold: 1,
..ProxyConfig::default()
},
)?);
mgr.add_proxy(make_proxy("http://q.test:8080")).await?;
let bridge = ProxyManagerBridge::new(Arc::clone(&mgr));
{
let (_url, _handle) = bridge.bind_proxy().await?;
}
let stats = mgr.pool_stats().await?;
assert_eq!(
stats.open, 1,
"circuit should open after crash (open = {})",
stats.open
);
Ok(())
}
#[test]
fn direct_handle_is_valid_noop_binding() {
let handle = ProxyHandle::direct();
assert!(handle.proxy_url.is_empty());
handle.mark_success();
}
}