use stygian_browser::tls::{
CHROME_131, EDGE_131, FIREFOX_133, SAFARI_18, TlsControl, TlsProfile,
build_profiled_client_preset, build_profiled_client_with_control,
};
use thiserror::Error;
pub use crate::types::ProfiledRequestMode;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProfiledRequesterError {
#[error("failed to build TLS-profiled client: {0}")]
Build(#[from] stygian_browser::tls::TlsClientError),
}
#[derive(Clone, Debug)]
pub struct ProfiledRequester {
client: reqwest::Client,
profile: &'static TlsProfile,
}
impl ProfiledRequester {
pub fn from_profile(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
) -> Result<Self, ProfiledRequesterError> {
let client = build_profiled_client_preset(profile, proxy_url)?;
Ok(Self { client, profile })
}
pub fn from_profile_mode(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
mode: ProfiledRequestMode,
) -> Result<Self, ProfiledRequesterError> {
let client = match mode {
ProfiledRequestMode::Compatible => {
build_profiled_client_with_control(profile, proxy_url, TlsControl::compatible())?
}
ProfiledRequestMode::Preset => build_profiled_client_preset(profile, proxy_url)?,
ProfiledRequestMode::Strict => {
build_profiled_client_with_control(profile, proxy_url, TlsControl::strict())?
}
ProfiledRequestMode::StrictAll => {
build_profiled_client_with_control(profile, proxy_url, TlsControl::strict_all())?
}
};
Ok(Self { client, profile })
}
pub fn from_profile_with_control(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
control: TlsControl,
) -> Result<Self, ProfiledRequesterError> {
let client = build_profiled_client_with_control(profile, proxy_url, control)?;
Ok(Self { client, profile })
}
pub fn from_profile_compatible(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(profile, proxy_url, ProfiledRequestMode::Compatible)
}
pub fn from_profile_strict(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(profile, proxy_url, ProfiledRequestMode::Strict)
}
pub fn from_profile_strict_all(
profile: &'static TlsProfile,
proxy_url: Option<&str>,
) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(profile, proxy_url, ProfiledRequestMode::StrictAll)
}
pub fn chrome() -> Result<Self, ProfiledRequesterError> {
Self::from_profile(&CHROME_131, None)
}
pub fn chrome_mode(mode: ProfiledRequestMode) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(&CHROME_131, None, mode)
}
pub fn firefox() -> Result<Self, ProfiledRequesterError> {
Self::from_profile(&FIREFOX_133, None)
}
pub fn firefox_mode(mode: ProfiledRequestMode) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(&FIREFOX_133, None, mode)
}
pub fn safari() -> Result<Self, ProfiledRequesterError> {
Self::from_profile(&SAFARI_18, None)
}
pub fn safari_mode(mode: ProfiledRequestMode) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(&SAFARI_18, None, mode)
}
pub fn edge() -> Result<Self, ProfiledRequesterError> {
Self::from_profile(&EDGE_131, None)
}
pub fn edge_mode(mode: ProfiledRequestMode) -> Result<Self, ProfiledRequesterError> {
Self::from_profile_mode(&EDGE_131, None, mode)
}
pub fn random_weighted(seed: u64) -> Result<Self, ProfiledRequesterError> {
let profile = TlsProfile::random_weighted(seed);
let client = build_profiled_client_preset(profile, None)?;
Ok(Self { client, profile })
}
pub fn random_weighted_mode(
seed: u64,
mode: ProfiledRequestMode,
) -> Result<Self, ProfiledRequesterError> {
let profile = TlsProfile::random_weighted(seed);
Self::from_profile_mode(profile, None, mode)
}
#[must_use]
pub const fn client(&self) -> &reqwest::Client {
&self.client
}
#[must_use]
pub fn into_client(self) -> reqwest::Client {
self.client
}
#[must_use]
pub const fn profile(&self) -> &'static TlsProfile {
self.profile
}
#[must_use]
pub fn profile_name(&self) -> &str {
&self.profile.name
}
#[must_use]
pub fn supports_h2(&self) -> bool {
!self.profile.name.contains("HTTP/1.1-only")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chrome_requester_builds() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::chrome()?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn chrome_mode_preset_builds() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::chrome_mode(ProfiledRequestMode::Preset)?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn firefox_requester_builds() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::firefox()?;
assert_eq!(r.profile_name(), "Firefox 133");
Ok(())
}
#[test]
fn safari_requester_builds() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::safari()?;
assert_eq!(r.profile_name(), "Safari 18");
Ok(())
}
#[test]
fn edge_requester_builds() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::edge()?;
assert_eq!(r.profile_name(), "Edge 131");
Ok(())
}
#[test]
fn random_weighted_requester_varies() -> std::result::Result<(), Box<dyn std::error::Error>> {
let a = ProfiledRequester::random_weighted(1)?;
let b = ProfiledRequester::random_weighted(999_999)?;
let _ = (a.profile_name(), b.profile_name()); Ok(())
}
#[test]
fn random_weighted_mode_compatible_builds()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::random_weighted_mode(42, ProfiledRequestMode::Compatible)?;
assert!(!r.profile_name().is_empty());
Ok(())
}
#[test]
fn from_profile_with_custom_gives_correct_name()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::from_profile(&CHROME_131, None)?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn from_profile_with_control_gives_correct_name()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::from_profile_with_control(
&CHROME_131,
None,
TlsControl::compatible(),
)?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn from_profile_mode_preset_gives_correct_name()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let r =
ProfiledRequester::from_profile_mode(&CHROME_131, None, ProfiledRequestMode::Preset)?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn from_profile_mode_compatible_gives_correct_name()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::from_profile_mode(
&CHROME_131,
None,
ProfiledRequestMode::Compatible,
)?;
assert_eq!(r.profile_name(), "Chrome 131");
Ok(())
}
#[test]
fn strict_all_constructor_reports_unsupported_group_for_chrome()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let err = ProfiledRequester::from_profile_strict_all(&CHROME_131, None)
.err()
.ok_or_else(|| {
std::io::Error::other("strict_all should fail if a profile group is unsupported")
})?;
let msg = err.to_string();
assert!(msg.contains("unsupported supported_group"), "{msg}");
Ok(())
}
#[test]
fn clone_is_shallow() -> std::result::Result<(), Box<dyn std::error::Error>> {
let r = ProfiledRequester::chrome()?;
let r2 = r.clone();
assert_eq!(r.profile_name(), r2.profile_name());
Ok(())
}
}