use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::Value;
use crate::network::{BayesianNetwork, fingerprint_network};
use crate::{
Error, HeaderGenerator, HeaderOptions, HeaderOverrides, Headers, OrderedMap, Restrictions,
Result, Rng, get_user_agent,
};
pub const STRINGIFIED: &str = "*STRINGIFIED*";
const MISSING_VALUE: &str = crate::headers::MISSING_VALUE;
const DEFAULT_MAX_DIMENSION: u32 = 100_000;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Screen {
pub min_width: Option<u32>,
pub max_width: Option<u32>,
pub min_height: Option<u32>,
pub max_height: Option<u32>,
}
impl Screen {
pub fn validate(&self) -> Result<()> {
let inverted =
|min: Option<u32>, max: Option<u32>| min.zip(max).is_some_and(|(a, b)| a > b);
if inverted(self.min_width, self.max_width) || inverted(self.min_height, self.max_height) {
return Err(Error::InvalidOptions(
"Invalid screen constraints: min values cannot be greater than max values"
.to_owned(),
));
}
Ok(())
}
#[must_use]
pub fn is_set(&self) -> bool {
self.min_width.is_some()
|| self.max_width.is_some()
|| self.min_height.is_some()
|| self.max_height.is_some()
}
#[must_use]
pub fn admits(&self, screen_value: &str) -> bool {
let Some(json) = screen_value.strip_prefix(STRINGIFIED) else {
return false;
};
let Ok(screen) = serde_json::from_str::<Value>(json) else {
return false;
};
let within = |key: &str, min: Option<u32>, max: Option<u32>| {
screen.get(key).and_then(Value::as_f64).is_some_and(|size| {
size >= f64::from(min.unwrap_or(0))
&& size <= f64::from(max.unwrap_or(DEFAULT_MAX_DIMENSION))
})
};
within("width", self.min_width, self.max_width)
&& within("height", self.min_height, self.max_height)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FingerprintOptions {
pub screen: Option<Screen>,
pub strict: bool,
pub mock_webrtc: bool,
pub slim: bool,
pub header: HeaderOptions,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FingerprintOverrides {
pub screen: Option<Screen>,
pub strict: Option<bool>,
pub mock_webrtc: Option<bool>,
pub slim: Option<bool>,
pub header: HeaderOverrides,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenFingerprint {
pub avail_top: i64,
pub avail_left: i64,
pub page_x_offset: i64,
pub page_y_offset: i64,
pub screen_x: i64,
#[serde(rename = "hasHDR")]
pub has_hdr: bool,
pub width: i64,
pub height: i64,
pub avail_width: i64,
pub avail_height: i64,
pub client_width: i64,
pub client_height: i64,
pub inner_width: i64,
pub inner_height: i64,
pub outer_width: i64,
pub outer_height: i64,
pub color_depth: i64,
pub pixel_depth: i64,
pub device_pixel_ratio: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigatorFingerprint {
pub user_agent: String,
pub user_agent_data: Value,
pub do_not_track: Value,
pub app_code_name: String,
pub app_name: String,
pub app_version: String,
pub oscpu: Value,
pub webdriver: Value,
pub language: String,
pub languages: Vec<String>,
pub platform: String,
pub device_memory: Value,
pub hardware_concurrency: i64,
pub product: String,
pub product_sub: String,
pub vendor: Value,
pub vendor_sub: Value,
pub max_touch_points: i64,
pub extra_properties: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VideoCard {
pub renderer: String,
pub vendor: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Fingerprint {
pub screen: ScreenFingerprint,
pub navigator: NavigatorFingerprint,
#[serde(serialize_with = "serialize_headers")]
pub headers: Headers,
pub video_codecs: Value,
pub audio_codecs: Value,
pub plugins_data: Value,
pub battery: Value,
pub video_card: Option<VideoCard>,
pub multimedia_devices: Value,
pub fonts: Value,
#[serde(rename = "mockWebRTC")]
pub mock_webrtc: bool,
pub slim: bool,
}
impl Fingerprint {
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| Error::Data(e.to_string()))
}
}
fn serialize_headers<S: Serializer>(
headers: &Headers,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.collect_map(headers.iter())
}
#[derive(Debug, Clone)]
pub struct FingerprintGenerator {
options: FingerprintOptions,
header_generator: HeaderGenerator,
}
impl FingerprintGenerator {
pub fn new(options: FingerprintOptions) -> Result<Self> {
if let Some(screen) = &options.screen {
screen.validate()?;
}
let header_generator = HeaderGenerator::new(options.header.clone())?;
Ok(Self {
options,
header_generator,
})
}
#[must_use]
pub fn options(&self) -> &FingerprintOptions {
&self.options
}
pub fn generate(&self, rng: &mut Rng, overrides: &FingerprintOverrides) -> Result<Fingerprint> {
let screen = overrides
.screen
.or(self.options.screen)
.filter(Screen::is_set);
if let Some(screen) = &screen {
screen.validate()?;
}
let strict = overrides.strict.unwrap_or(self.options.strict);
let net = fingerprint_network()?;
let mut header_overrides = overrides.header.clone();
let screen_values = match screen {
Some(screen) => {
let constrained = screen_constraint(net, &screen, &header_overrides.user_agent)?;
match constrained {
Some((values, user_agents)) => {
header_overrides.user_agent = user_agents;
Some(values)
}
None if strict => return Err(Error::NoFingerprint),
None => None,
}
}
None => None,
};
let headers = self.header_generator.generate(rng, &header_overrides)?;
let user_agent = get_user_agent(&headers)
.map(str::to_owned)
.ok_or(Error::MissingUserAgent)?;
let mut restrictions = Restrictions::new();
if let Some(values) = screen_values {
restrictions.insert("screen", values);
}
restrictions.insert("userAgent", vec![user_agent]);
let mut sample = net.consistent_sample(rng, &restrictions);
if sample.is_none() && !strict && restrictions.remove("screen").is_some() {
sample = net.consistent_sample(rng, &restrictions);
}
let sample = sample.ok_or(Error::NoFingerprint)?;
let mut attributes: OrderedMap<Value> = OrderedMap::new();
for (name, value) in sample {
attributes.insert(name, unpack(&value)?);
}
Ok(Fingerprint {
screen: typed(&mut attributes, "screen")?,
navigator: navigator(&mut attributes, &headers)?,
video_codecs: take(&mut attributes, "videoCodecs"),
audio_codecs: take(&mut attributes, "audioCodecs"),
plugins_data: take(&mut attributes, "pluginsData"),
battery: take(&mut attributes, "battery"),
video_card: video_card(take(&mut attributes, "videoCard"))?,
multimedia_devices: take(&mut attributes, "multimediaDevices"),
fonts: take(&mut attributes, "fonts"),
mock_webrtc: overrides.mock_webrtc.unwrap_or(self.options.mock_webrtc),
slim: overrides.slim.unwrap_or(self.options.slim),
headers,
})
}
}
fn screen_constraint(
net: &BayesianNetwork,
screen: &Screen,
user_agents: &[String],
) -> Result<Option<(Vec<String>, Vec<String>)>> {
let values: Vec<String> = node_values(net, "screen")?
.into_iter()
.filter(|value| screen.admits(value))
.collect();
let mut restriction = Restrictions::new();
restriction.insert("screen", values.clone());
let closure = match net.possible_values(&restriction) {
Ok(closure) => closure,
Err(Error::TooRestrictive) => return Ok(None),
Err(e) => return Err(e),
};
let mut compatible = closure.get("userAgent").cloned().unwrap_or_default();
if !user_agents.is_empty() {
compatible.retain(|ua| user_agents.contains(ua));
}
Ok((!compatible.is_empty()).then_some((values, compatible)))
}
fn node_values(net: &BayesianNetwork, name: &str) -> Result<Vec<String>> {
let node = net
.node(name)
.ok_or_else(|| Error::UnknownNode(name.to_owned()))?;
Ok(node.possible_values().map(str::to_owned).collect())
}
fn unpack(value: &str) -> Result<Value> {
if value == MISSING_VALUE {
return Ok(Value::Null);
}
match value.strip_prefix(STRINGIFIED) {
Some(json) => serde_json::from_str(json)
.map_err(|e| Error::Data(format!("malformed stringified value {json:?}: {e}"))),
None => Ok(Value::String(value.to_owned())),
}
}
fn take(attributes: &mut OrderedMap<Value>, name: &str) -> Value {
attributes.remove(name).unwrap_or(Value::Null)
}
fn typed<T: DeserializeOwned>(attributes: &mut OrderedMap<Value>, name: &str) -> Result<T> {
serde_json::from_value(take(attributes, name))
.map_err(|e| Error::Data(format!("fingerprint attribute {name}: {e}")))
}
fn video_card(value: Value) -> Result<Option<VideoCard>> {
let empty = match &value {
Value::Null => true,
Value::Object(map) => map.is_empty(),
_ => false,
};
if empty {
return Ok(None);
}
serde_json::from_value(value)
.map(Some)
.map_err(|e| Error::Data(format!("fingerprint attribute videoCard: {e}")))
}
fn languages(headers: &Headers) -> Vec<String> {
let accept_language = headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case("accept-language"))
.map_or("", |(_, value)| value.as_str());
accept_language
.split(',')
.map(|locale| {
locale
.split_once(';')
.map_or(locale, |(tag, _)| tag)
.trim()
.to_owned()
})
.collect()
}
fn navigator(
attributes: &mut OrderedMap<Value>,
headers: &Headers,
) -> Result<NavigatorFingerprint> {
let languages = languages(headers);
let max_touch_points = match take(attributes, "maxTouchPoints") {
Value::Null => 0,
value => serde_json::from_value(value)
.map_err(|e| Error::Data(format!("fingerprint attribute maxTouchPoints: {e}")))?,
};
Ok(NavigatorFingerprint {
user_agent: typed(attributes, "userAgent")?,
user_agent_data: take(attributes, "userAgentData"),
do_not_track: take(attributes, "doNotTrack"),
app_code_name: typed(attributes, "appCodeName")?,
app_name: typed(attributes, "appName")?,
app_version: typed(attributes, "appVersion")?,
oscpu: take(attributes, "oscpu"),
webdriver: take(attributes, "webdriver"),
language: languages.first().cloned().unwrap_or_default(),
languages,
platform: typed(attributes, "platform")?,
device_memory: take(attributes, "deviceMemory"),
hardware_concurrency: typed(attributes, "hardwareConcurrency")?,
product: typed(attributes, "product")?,
product_sub: typed(attributes, "productSub")?,
vendor: take(attributes, "vendor"),
vendor_sub: take(attributes, "vendorSub"),
max_touch_points,
extra_properties: take(attributes, "extraProperties"),
})
}
fn rng_for(seed: Option<&str>) -> Result<Rng> {
seed.map_or_else(Rng::from_entropy, |seed| Ok(Rng::from_seed(seed)))
}
pub fn generate_fingerprint(
seed: Option<&str>,
options: &FingerprintOptions,
) -> Result<Fingerprint> {
let mut rng = rng_for(seed)?;
FingerprintGenerator::new(options.clone())?.generate(&mut rng, &FingerprintOverrides::default())
}
pub fn generate_headers(seed: Option<&str>, options: &HeaderOptions) -> Result<Headers> {
let mut rng = rng_for(seed)?;
HeaderGenerator::new(options.clone())?.generate(&mut rng, &HeaderOverrides::default())
}