use crate::{
database::browser_profile::Platform,
server::{
dtos::{
browser_profile_dto::{BrowserProfileFullData, Mode},
start_dto::{self, ConnectionInfo, StartRequest},
},
error::Error,
},
};
use serde::{Deserialize, Serialize};
use super::{
consts::{
COUNTRY_LOCALE_MAP, LATEST_CHROME_VERSION, LINUX_ACCEPTABLE_LOCALES,
MACOS_ACCEPTABLE_LOCALES, WINDOWS_ACCEPTABLE_LOCALES,
},
FromStartRequest, Screen,
};
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct Connection {
pub downlink: f32,
pub effective_type: String,
pub rtt: i16,
pub save_data: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct Navigator {
pub app_code_name: String,
pub app_name: String,
pub connection: Connection,
pub device_memory: i32,
pub do_not_track: bool,
pub hardware_concurrency: i32,
pub app_locale: String,
pub locale: String,
pub languages: Vec<String>,
pub accept_languages: String,
pub platform: String,
pub product: String,
pub product_sub: String,
pub user_agent: String,
pub vendor: String,
pub vendor_sub: String,
pub timezone: String,
}
impl Navigator {
pub fn make_accept_languages(locale: String) -> String {
let mut split = locale.split('-');
let language = split.next().unwrap_or("en").to_lowercase();
let region = split.next().unwrap_or("US").to_uppercase();
if language == *"en" {
format!("en-{region},en;q=0.9")
} else {
format!("{language}-{region},{language};q=0.9;en-US,en;q=0.8")
}
}
pub fn make_languages(locale: String) -> Vec<String> {
let mut split = locale.split('-');
let language = split.next().unwrap_or("en").to_lowercase();
let region = split.next().unwrap_or("US").to_uppercase();
if language == *"en" {
vec![format!("{language}-{region}"), "en".into()]
} else {
vec![
format!("{language}-{region}"),
language,
"en-US".into(),
"en".into(),
]
}
}
pub fn detect_locale_from_country_code(country_code: String) -> String {
for locale in COUNTRY_LOCALE_MAP {
if locale.to_lowercase().contains(&country_code.to_lowercase()) {
return locale.to_string();
}
}
"en-US".into()
}
pub fn prepare_locale(
bp: &BrowserProfileFullData,
request: &StartRequest,
) -> Result<String, Error> {
match (bp.locale.mode, bp.locale.value.clone()) {
(Mode::Manual, Some(value)) => Ok(value),
_ => {
if !request.connection_info.country.is_empty() {
return Ok(Self::detect_locale_from_country_code(
request.connection_info.country.clone(),
));
}
Ok("en-US".to_string())
}
}
}
pub fn make_app_locale(locale: String, os: start_dto::Os) -> String {
use start_dto::Os::*;
let acceptable_list = match os {
MacOS => MACOS_ACCEPTABLE_LOCALES,
Windows => WINDOWS_ACCEPTABLE_LOCALES,
Linux => LINUX_ACCEPTABLE_LOCALES,
};
if acceptable_list.contains(&locale.as_str()) {
locale
} else if acceptable_list
.contains(&locale.chars().take(2).collect::<String>().as_str())
{
locale.chars().take(2).collect()
} else {
match os {
MacOS => "en".to_string(),
_ => "en-US".to_string(),
}
}
}
pub fn make_timezone(
bp: &BrowserProfileFullData,
connection_info: ConnectionInfo,
) -> String {
match (bp.timezone.mode, bp.timezone.value.clone()) {
(Mode::Manual, Some(value)) => value,
_ => connection_info.timezone.clone(),
}
}
}
impl FromStartRequest<Navigator> for Navigator {
fn from_start_request(
bp: &BrowserProfileFullData,
request: &StartRequest,
_navigator: &Navigator,
_screen: &Screen,
_token: &str,
) -> Result<Self, Error> {
let device_memory = match bp.memory.mode {
Mode::Manual => {
if bp.memory.value > 8 {
8
} else {
bp.memory.value as i32
}
}
_ => 0,
};
let hardware_concurrency = match bp.cpu.mode {
Mode::Manual => {
if bp.cpu.value > 16 {
16
} else {
bp.cpu.value as i32
}
}
_ => 0,
};
let locale = Self::prepare_locale(bp, request)?;
let languages = Self::make_languages(locale.clone());
let accept_languages = Self::make_accept_languages(locale.clone());
let app_locale = Self::make_app_locale(locale.clone(), request.os.clone());
let locale = locale.chars().take(2).collect();
let platform = match bp.platform {
Platform::Macos => "MacIntel".to_string(),
Platform::Windows => "Win32".to_string(),
Platform::Linux => "Linux".to_string(),
};
let user_agent = match bp.useragent.mode {
Mode::Manual => bp
.useragent
.value
.clone()
.unwrap_or(get_latest_user_agent(bp.platform.clone())),
_ => get_latest_user_agent(bp.platform.clone()),
};
let timezone = Self::make_timezone(bp, request.connection_info.clone());
Ok(Self {
app_code_name: "Mozilla".to_string(),
app_name: "Netscape".to_string(),
connection: Connection {
downlink: 10.2,
effective_type: "4g".to_string(),
rtt: 50,
save_data: false,
},
device_memory,
do_not_track: bp.do_not_track,
hardware_concurrency,
app_locale,
locale,
languages,
accept_languages,
platform,
product: "Gecko".to_string(),
product_sub: "20030107".to_string(),
user_agent: user_agent.clone(),
vendor: "Google Inc.".to_string(),
vendor_sub: "".to_string(),
timezone,
})
}
}
fn get_latest_user_agent(platform: Platform) -> String {
match platform {
Platform::Linux => format!("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36", LATEST_CHROME_VERSION),
Platform::Macos => format!("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36", LATEST_CHROME_VERSION),
Platform::Windows => format!("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36", LATEST_CHROME_VERSION)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::dtos::browser_profile_dto::{Locale, Timezone};
use crate::server::dtos::start_dto::Os;
use crate::server::services::browser_profile_services::config::consts::LINUX_MOCK_PROFILE;
fn create_base_profile() -> BrowserProfileFullData {
BrowserProfileFullData {
locale: Locale {
mode: Mode::Auto,
value: None,
},
timezone: Timezone {
mode: Mode::Auto,
value: None,
},
..LINUX_MOCK_PROFILE.clone()
}
}
#[test]
fn test_make_accept_languages_english() {
let result = Navigator::make_accept_languages("en-US".to_string());
assert_eq!(result, "en-US,en;q=0.9");
let result = Navigator::make_accept_languages("en-GB".to_string());
assert_eq!(result, "en-GB,en;q=0.9");
}
#[test]
fn test_make_accept_languages_other() {
let result = Navigator::make_accept_languages("fr-FR".to_string());
assert_eq!(result, "fr-FR,fr;q=0.9;en-US,en;q=0.8");
let result = Navigator::make_accept_languages("de-DE".to_string());
assert_eq!(result, "de-DE,de;q=0.9;en-US,en;q=0.8");
}
#[test]
fn test_make_languages_english() {
let result = Navigator::make_languages("en-US".to_string());
assert_eq!(result, vec!["en-US", "en"]);
let result = Navigator::make_languages("en-GB".to_string());
assert_eq!(result, vec!["en-GB", "en"]);
}
#[test]
fn test_make_languages_other() {
let result = Navigator::make_languages("fr-FR".to_string());
assert_eq!(result, vec!["fr-FR", "fr", "en-US", "en"]);
let result = Navigator::make_languages("de-DE".to_string());
assert_eq!(result, vec!["de-DE", "de", "en-US", "en"]);
}
#[test]
#[ignore = "Current implementation is not reliable and returns fr-BE for country code FR, so the second assert fails"]
fn test_detect_locale_from_country_code() {
let result = Navigator::detect_locale_from_country_code("US".to_string());
assert_eq!(result, "en-US");
let result = Navigator::detect_locale_from_country_code("FR".to_string());
assert_eq!(result, "fr-FR");
let result = Navigator::detect_locale_from_country_code("XX".to_string());
assert_eq!(result, "en-US");
}
#[test]
fn test_prepare_locale_manual() -> Result<(), Error> {
let mut profile = create_base_profile();
profile.locale.mode = Mode::Manual;
profile.locale.value = Some("fr-FR".to_string());
let request = StartRequest::get_mock();
let result = Navigator::prepare_locale(&profile, &request)?;
assert_eq!(result, "fr-FR");
Ok(())
}
#[test]
#[ignore = "Current implementation is not reliable and returns fr-BE for country code FR, so the assert fails"]
fn test_prepare_locale_auto() -> Result<(), Error> {
let profile = create_base_profile();
let mut request = StartRequest::get_mock();
request.connection_info = ConnectionInfo {
country: "FR".to_string(),
..ConnectionInfo::get_mock()
};
let result = Navigator::prepare_locale(&profile, &request)?;
assert_eq!(result, "fr-FR");
Ok(())
}
#[test]
fn test_make_app_locale_macos() {
let result = Navigator::make_app_locale("en-US".to_string(), Os::MacOS);
assert_eq!(result, "en");
let result = Navigator::make_app_locale("fr-FR".to_string(), Os::MacOS);
assert_eq!(result, "fr");
}
#[test]
fn test_make_app_locale_other_os() {
let result = Navigator::make_app_locale("en-US".to_string(), Os::Windows);
assert_eq!(result, "en");
let result = Navigator::make_app_locale("fr-FR".to_string(), Os::Linux);
assert_eq!(result, "fr");
}
#[test]
fn test_make_timezone_manual() {
let mut profile = create_base_profile();
profile.timezone.mode = Mode::Manual;
profile.timezone.value = Some("Europe/Paris".to_string());
let result = Navigator::make_timezone(&profile, ConnectionInfo::get_mock());
assert_eq!(result, "Europe/Paris");
}
#[test]
fn test_make_timezone_auto() {
let profile = create_base_profile();
let connection_info = ConnectionInfo {
timezone: "America/New_York".to_string(),
..ConnectionInfo::get_mock()
};
let result = Navigator::make_timezone(&profile, connection_info);
assert_eq!(result, "America/New_York");
}
#[test]
fn test_make_timezone_empty() {
let profile = create_base_profile();
let result = Navigator::make_timezone(&profile, ConnectionInfo::get_mock());
assert_eq!(result, "");
}
#[test]
fn test_from_start_request() -> Result<(), Error> {
let mut profile = create_base_profile();
profile.memory.mode = Mode::Manual;
profile.memory.value = 8;
profile.cpu.mode = Mode::Manual;
profile.cpu.value = 8;
profile.platform = Platform::Windows;
profile.useragent.mode = Mode::Manual;
profile.useragent.value = Some("Custom UA".to_string());
profile.do_not_track = true;
let request = StartRequest::get_mock();
let navigator = Navigator::default();
let screen = Screen::default();
let token = String::new();
let result = Navigator::from_start_request(
&profile, &request, &navigator, &screen, &token,
)?;
assert_eq!(result.device_memory, 8);
assert_eq!(result.hardware_concurrency, 8);
assert_eq!(result.platform, "Win32");
assert_eq!(result.user_agent, "Custom UA");
assert!(result.do_not_track);
assert_eq!(result.app_code_name, "Mozilla");
assert_eq!(result.app_name, "Netscape");
assert_eq!(result.product, "Gecko");
assert_eq!(result.product_sub, "20030107");
assert_eq!(result.vendor, "Google Inc.");
assert_eq!(result.vendor_sub, "");
Ok(())
}
#[test]
fn test_memory_and_cpu_limits() -> Result<(), Error> {
let mut profile = create_base_profile();
profile.memory.mode = Mode::Manual;
profile.memory.value = 16; profile.cpu.mode = Mode::Manual;
profile.cpu.value = 32; let request = StartRequest::get_mock();
let navigator = Navigator::default();
let screen = Screen::default();
let token = String::new();
let result = Navigator::from_start_request(
&profile, &request, &navigator, &screen, &token,
)?;
assert_eq!(result.device_memory, 8);
assert_eq!(result.hardware_concurrency, 16);
Ok(())
}
}