rustenium-identity 0.1.11

A versatile stealth overlay for rustenium
Documentation
use crate::error::IdentityError;

/// What the exit IP says about where the browser is sitting.
///
/// Timezone and language are checked *against the IP*, not just for internal
/// consistency — a Dutch exit node reporting `en-US` and `America/New_York` is the
/// mismatch these suites exist to find. Both are therefore derived from the same
/// lookup rather than carried in the persona.
pub struct Geo {
    pub timezone: String,
    pub country_code: Option<String>,
}

/// Resolve timezone and country: use the explicit timezone override if provided,
/// otherwise query ip-api.com for the (possibly proxied) exit IP.
pub async fn resolve_geo(
    explicit_tz: Option<&str>,
    proxy: Option<&str>,
) -> Result<Geo, IdentityError> {
    let fetched = fetch_from_ipapi(proxy).await;

    match (explicit_tz, fetched) {
        // An explicit timezone still benefits from the country lookup, but must not
        // fail the launch if that lookup is unavailable.
        (Some(tz), Ok(geo)) => Ok(Geo {
            timezone: tz.to_string(),
            country_code: geo.country_code,
        }),
        (Some(tz), Err(_)) => Ok(Geo {
            timezone: tz.to_string(),
            country_code: None,
        }),
        (None, result) => result,
    }
}

/// Query ip-api.com for the IANA timezone and ISO country of the current IP
/// (optionally routed through a proxy).
async fn fetch_from_ipapi(proxy: Option<&str>) -> Result<Geo, IdentityError> {
    let mut builder = reqwest::Client::builder();
    if let Some(proxy_url) = proxy {
        builder = builder.proxy(
            reqwest::Proxy::all(proxy_url)
                .map_err(|e| IdentityError::TimezoneError(format!("invalid proxy: {e}")))?,
        );
    }
    let client = builder
        .build()
        .map_err(|e| IdentityError::TimezoneError(format!("http client error: {e}")))?;

    let resp: serde_json::Value = client
        .get("http://ip-api.com/json/?fields=timezone,countryCode")
        .send()
        .await
        .map_err(|e| IdentityError::TimezoneError(format!("ip-api request failed: {e}")))?
        .json()
        .await
        .map_err(|e| IdentityError::TimezoneError(format!("ip-api parse failed: {e}")))?;

    let timezone = resp
        .get("timezone")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| IdentityError::TimezoneError("ip-api returned no timezone".into()))?;

    Ok(Geo {
        timezone,
        country_code: resp
            .get("countryCode")
            .and_then(|v| v.as_str())
            .map(|s| s.to_uppercase()),
    })
}