mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
308 lines
11 KiB
Rust
308 lines
11 KiB
Rust
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct WindowSurfaceProfile {
|
|
pub user_agent: &'static str,
|
|
pub platform: &'static str,
|
|
pub language: &'static str,
|
|
pub hardware_concurrency: f64,
|
|
pub max_touch_points: f64,
|
|
pub inner_width: f64,
|
|
pub inner_height: f64,
|
|
pub device_pixel_ratio: f64,
|
|
pub screen_width: f64,
|
|
pub screen_height: f64,
|
|
pub screen_avail_width: f64,
|
|
pub screen_avail_height: f64,
|
|
pub color_depth: f64,
|
|
pub pixel_depth: f64,
|
|
pub orientation_angle: f64,
|
|
pub orientation_type: &'static str,
|
|
pub visual_viewport_scale: f64,
|
|
}
|
|
|
|
/// Chromium-compatible product token exposed through `Browser.getVersion`.
|
|
///
|
|
/// Keep this token in the default user agent as well. CDP clients commonly use
|
|
/// `product` for Chromium feature detection, while Moli's own product
|
|
/// identity remains available through its binary/package metadata.
|
|
pub const DEFAULT_CDP_PRODUCT: &str = "Chrome/145.0.0.0";
|
|
pub const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
|
|
pub const DEFAULT_ACCEPT_LANGUAGE: &str = "en-US,en;q=0.9";
|
|
pub const DEFAULT_SEC_CH_UA_PLATFORM: &str = "\"Windows\"";
|
|
pub const DEFAULT_SEC_CH_UA_PLATFORM_VERSION: &str = "\"19.0.0\"";
|
|
pub const DEFAULT_SEC_CH_UA_ARCH: &str = "\"x86\"";
|
|
pub const DEFAULT_SEC_CH_UA_BITNESS: &str = "\"64\"";
|
|
pub const DEFAULT_SEC_CH_UA_MODEL: &str = "\"\"";
|
|
pub const DEFAULT_SEC_CH_UA_WOW64: &str = "?0";
|
|
pub const DEFAULT_SEC_CH_UA_FORM_FACTORS: &str = "\"Desktop\"";
|
|
pub const DEFAULT_NAVIGATOR_PLATFORM: &str = "Win32";
|
|
pub const DEFAULT_NAVIGATOR_APP_CODE_NAME: &str = "Mozilla";
|
|
pub const DEFAULT_NAVIGATOR_APP_NAME: &str = "Netscape";
|
|
pub const DEFAULT_NAVIGATOR_VENDOR: &str = "Google Inc.";
|
|
pub const DEFAULT_NAVIGATOR_VENDOR_SUB: &str = "";
|
|
pub const DEFAULT_NAVIGATOR_PRODUCT: &str = "Gecko";
|
|
pub const DEFAULT_NAVIGATOR_PRODUCT_SUB: &str = "20030107";
|
|
pub const DEFAULT_NAVIGATOR_ONLINE: bool = true;
|
|
pub const DEFAULT_NAVIGATOR_WEBDRIVER: bool = false;
|
|
pub const DEFAULT_NAVIGATOR_PDF_VIEWER_ENABLED: bool = true;
|
|
pub const DEFAULT_NAVIGATOR_DEVICE_MEMORY: f64 = 8.0;
|
|
pub const DEFAULT_CONNECTION_TYPE: &str = "unknown";
|
|
pub const DEFAULT_CONNECTION_DOWNLINK_MAX: f64 = f64::INFINITY;
|
|
pub const DEFAULT_CONNECTION_EFFECTIVE_TYPE: &str = "4g";
|
|
pub const DEFAULT_CONNECTION_DOWNLINK: f64 = 10.0;
|
|
pub const DEFAULT_CONNECTION_RTT: f64 = 50.0;
|
|
pub const DEFAULT_CONNECTION_SAVE_DATA: bool = false;
|
|
|
|
pub const DEFAULT_WINDOW_SURFACE_PROFILE: WindowSurfaceProfile = WindowSurfaceProfile {
|
|
user_agent: DEFAULT_USER_AGENT,
|
|
platform: DEFAULT_NAVIGATOR_PLATFORM,
|
|
language: "en-US",
|
|
hardware_concurrency: 4.0,
|
|
max_touch_points: 0.0,
|
|
inner_width: 1920.0,
|
|
inner_height: 1080.0,
|
|
device_pixel_ratio: 1.0,
|
|
// Keep the stable desktop profile internally consistent: inner viewport
|
|
// should not exceed the reported screen bounds. This stays close to the
|
|
// Chromium-derived Zhihu-clearing profile without carrying a textbook
|
|
// `innerWidth > screen.width` fingerprint mismatch.
|
|
screen_width: 1920.0,
|
|
screen_height: 1080.0,
|
|
screen_avail_width: 1920.0,
|
|
screen_avail_height: 1080.0,
|
|
color_depth: 24.0,
|
|
pixel_depth: 24.0,
|
|
orientation_angle: 0.0,
|
|
orientation_type: "landscape-primary",
|
|
visual_viewport_scale: 1.0,
|
|
};
|
|
|
|
pub fn navigator_app_version(user_agent: &str) -> &str {
|
|
user_agent.strip_prefix("Mozilla/").unwrap_or(user_agent)
|
|
}
|
|
|
|
pub fn chromium_major_version(user_agent: &str) -> Option<&str> {
|
|
let (_, tail) = user_agent
|
|
.split_once("HeadlessChrome/")
|
|
.or_else(|| user_agent.split_once("Chrome/"))?;
|
|
let major_end = tail.find('.').unwrap_or(tail.len());
|
|
let major = &tail[..major_end];
|
|
(!major.is_empty() && major.chars().all(|ch| ch.is_ascii_digit())).then_some(major)
|
|
}
|
|
|
|
pub fn chromium_full_version(user_agent: &str) -> Option<&str> {
|
|
let (_, tail) = user_agent
|
|
.split_once("HeadlessChrome/")
|
|
.or_else(|| user_agent.split_once("Chrome/"))?;
|
|
let version_end = tail
|
|
.find(|character: char| character.is_ascii_whitespace())
|
|
.unwrap_or(tail.len());
|
|
let version = &tail[..version_end];
|
|
(!version.is_empty()
|
|
&& version.split('.').all(|component| {
|
|
!component.is_empty() && component.chars().all(|ch| ch.is_ascii_digit())
|
|
}))
|
|
.then_some(version)
|
|
}
|
|
|
|
pub fn chromium_product_brand(user_agent: &str) -> Option<&'static str> {
|
|
if user_agent.contains("HeadlessChrome/") {
|
|
Some("HeadlessChrome")
|
|
} else if user_agent.contains("Chrome/") {
|
|
Some("Google Chrome")
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn chromium_greased_brand_version(seed: usize) -> (String, String) {
|
|
const GREASE_CHARS: [&str; 11] = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
|
|
const GREASE_VERSIONS: [&str; 3] = ["8", "99", "24"];
|
|
(
|
|
format!(
|
|
"Not{}A{}Brand",
|
|
GREASE_CHARS[seed % GREASE_CHARS.len()],
|
|
GREASE_CHARS[(seed + 1) % GREASE_CHARS.len()],
|
|
),
|
|
GREASE_VERSIONS[seed % GREASE_VERSIONS.len()].to_owned(),
|
|
)
|
|
}
|
|
|
|
pub fn chromium_brand_list_order(seed: usize, size: usize) -> Option<Vec<usize>> {
|
|
match size {
|
|
2 => Some(vec![seed % size, (seed + 1) % size]),
|
|
3 => {
|
|
const ORDERS: [[usize; 3]; 6] = [
|
|
[0, 1, 2],
|
|
[0, 2, 1],
|
|
[1, 0, 2],
|
|
[1, 2, 0],
|
|
[2, 0, 1],
|
|
[2, 1, 0],
|
|
];
|
|
Some(ORDERS[seed % ORDERS.len()].to_vec())
|
|
}
|
|
4 => {
|
|
const ORDERS: [[usize; 4]; 24] = [
|
|
[0, 1, 2, 3],
|
|
[0, 1, 3, 2],
|
|
[0, 2, 1, 3],
|
|
[0, 2, 3, 1],
|
|
[0, 3, 1, 2],
|
|
[0, 3, 2, 1],
|
|
[1, 0, 2, 3],
|
|
[1, 0, 3, 2],
|
|
[1, 2, 0, 3],
|
|
[1, 2, 3, 0],
|
|
[1, 3, 0, 2],
|
|
[1, 3, 2, 0],
|
|
[2, 0, 1, 3],
|
|
[2, 0, 3, 1],
|
|
[2, 1, 0, 3],
|
|
[2, 1, 3, 0],
|
|
[2, 3, 0, 1],
|
|
[2, 3, 1, 0],
|
|
[3, 0, 1, 2],
|
|
[3, 0, 2, 1],
|
|
[3, 1, 0, 2],
|
|
[3, 1, 2, 0],
|
|
[3, 2, 0, 1],
|
|
[3, 2, 1, 0],
|
|
];
|
|
Some(ORDERS[seed % ORDERS.len()].to_vec())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn chromium_sec_ch_ua_value(user_agent: &str) -> Option<String> {
|
|
chromium_ua_brand_versions(user_agent, false).map(format_sec_ch_ua_brand_versions)
|
|
}
|
|
|
|
pub fn chromium_sec_ch_ua_full_version_list_value(user_agent: &str) -> Option<String> {
|
|
chromium_ua_brand_versions(user_agent, true).map(format_sec_ch_ua_brand_versions)
|
|
}
|
|
|
|
pub fn chromium_ua_brand_versions(
|
|
user_agent: &str,
|
|
full_versions: bool,
|
|
) -> Option<Vec<(String, String)>> {
|
|
let ua_major = chromium_major_version(user_agent)?;
|
|
let ua_full = chromium_full_version(user_agent)?;
|
|
let seed = ua_major.parse::<usize>().ok()?;
|
|
let (greased_brand, greased_version) = chromium_greased_brand_version(seed);
|
|
let greased_version = if full_versions {
|
|
format!("{greased_version}.0.0.0")
|
|
} else {
|
|
greased_version
|
|
};
|
|
let product_version = if full_versions { ua_full } else { ua_major };
|
|
|
|
let mut brand_version_list = vec![
|
|
(greased_brand, greased_version),
|
|
("Chromium".to_owned(), product_version.to_owned()),
|
|
];
|
|
if let Some(brand) = chromium_product_brand(user_agent) {
|
|
brand_version_list.push((brand.to_owned(), product_version.to_owned()));
|
|
}
|
|
|
|
let order = chromium_brand_list_order(seed, brand_version_list.len())?;
|
|
let mut shuffled = vec![("".to_owned(), "".to_owned()); brand_version_list.len()];
|
|
for (index, shuffled_index) in order.iter().copied().enumerate() {
|
|
shuffled[shuffled_index] = brand_version_list[index].clone();
|
|
}
|
|
Some(shuffled)
|
|
}
|
|
|
|
fn format_sec_ch_ua_brand_versions(brand_versions: Vec<(String, String)>) -> String {
|
|
brand_versions
|
|
.into_iter()
|
|
.map(|(brand, version)| format!("\"{brand}\";v=\"{version}\""))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_window_surface_profile_matches_stable_moli_baseline() {
|
|
assert_eq!(
|
|
DEFAULT_WINDOW_SURFACE_PROFILE.user_agent,
|
|
DEFAULT_USER_AGENT
|
|
);
|
|
assert_eq!(
|
|
DEFAULT_WINDOW_SURFACE_PROFILE.platform,
|
|
DEFAULT_NAVIGATOR_PLATFORM
|
|
);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.language, "en-US");
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.hardware_concurrency, 4.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.max_touch_points, 0.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.inner_width, 1920.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.inner_height, 1080.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.screen_width, 1920.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.screen_height, 1080.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.screen_avail_width, 1920.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.screen_avail_height, 1080.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.color_depth, 24.0);
|
|
assert_eq!(DEFAULT_WINDOW_SURFACE_PROFILE.pixel_depth, 24.0);
|
|
assert_eq!(
|
|
DEFAULT_WINDOW_SURFACE_PROFILE.orientation_type,
|
|
"landscape-primary"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn navigator_app_version_strips_mozilla_prefix() {
|
|
assert_eq!(
|
|
navigator_app_version(DEFAULT_USER_AGENT),
|
|
DEFAULT_USER_AGENT.strip_prefix("Mozilla/").unwrap()
|
|
);
|
|
assert_eq!(navigator_app_version("CustomAgent/1.0"), "CustomAgent/1.0");
|
|
}
|
|
|
|
#[test]
|
|
fn sec_ch_ua_value_matches_chromium_seeded_order_for_chrome() {
|
|
assert_eq!(
|
|
chromium_sec_ch_ua_value(DEFAULT_USER_AGENT).as_deref(),
|
|
Some("\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sec_ch_ua_full_version_list_uses_the_same_brand_order() {
|
|
assert_eq!(
|
|
chromium_sec_ch_ua_full_version_list_value(DEFAULT_USER_AGENT).as_deref(),
|
|
Some(
|
|
"\"Not:A-Brand\";v=\"99.0.0.0\", \"Google Chrome\";v=\"145.0.0.0\", \"Chromium\";v=\"145.0.0.0\""
|
|
)
|
|
);
|
|
assert_eq!(chromium_full_version(DEFAULT_USER_AGENT), Some("145.0.0.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn sec_ch_ua_value_matches_chromium_seeded_order_for_headless() {
|
|
let user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/145.0.0.0 Safari/537.36";
|
|
assert_eq!(
|
|
chromium_sec_ch_ua_value(user_agent).as_deref(),
|
|
Some("\"Not:A-Brand\";v=\"99\", \"HeadlessChrome\";v=\"145\", \"Chromium\";v=\"145\"")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_major_version_prefers_headless_prefix_without_dead_fallback() {
|
|
assert_eq!(
|
|
chromium_major_version(
|
|
"Mozilla/5.0 HeadlessChrome/145.0.0.0 Safari/537.36 Chrome/999.0.0.0"
|
|
),
|
|
Some("145")
|
|
);
|
|
assert_eq!(chromium_major_version(DEFAULT_USER_AGENT), Some("145"));
|
|
assert!(
|
|
DEFAULT_USER_AGENT.contains(DEFAULT_CDP_PRODUCT),
|
|
"CDP product and default user agent must advertise the same Chromium compatibility version"
|
|
);
|
|
assert_eq!(chromium_major_version("Mozilla/5.0 Safari/537.36"), None);
|
|
}
|
|
}
|