pub use bao_stealth::StealthProfile;
use bao_stealth::{Http2Fingerprint, PriorityFrameMode, TlsFingerprint, TlsFingerprintConfig};
use bun_http::Method;
use bun_http::ssl_config::SSLConfig;
use bytes::Bytes;
use compact_str::CompactString;
use smallvec::SmallVec;
pub struct StealthRequestConfig {
pub method: Method,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
pub user_agent: Option<String>,
}
pub fn create_stealth_request(
profile: &Option<StealthProfile>,
method: Method,
url: &str,
headers: &[(String, String)],
body: Option<&[u8]>,
) -> StealthRequestConfig {
let ordered = ordered_headers(profile, headers);
let mut final_headers: Vec<(String, String)> = ordered
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let user_agent = profile.as_ref().map(|p| {
let ua = p.navigator.user_agent.clone();
final_headers.push(("user-agent".to_string(), ua.clone()));
ua
});
StealthRequestConfig {
method,
url: url.to_string(),
headers: final_headers,
body: body.map(|b| b.to_vec()),
user_agent,
}
}
pub struct StealthSyncResult {
pub status_code: u32,
pub status_text: CompactString,
pub headers: SmallVec<[(CompactString, CompactString); 8]>,
pub body: Bytes,
}
pub fn stealth_http_request(
profile: &Option<StealthProfile>,
method: Method,
url: &str,
headers: &[(String, String)],
body: Option<&[u8]>,
) -> Result<StealthSyncResult, String> {
let config = create_stealth_request(profile, method, url, headers, body);
let result = crate::http_client::http_request(
config.method,
&config.url,
&config.headers,
config.body.as_deref(),
)?;
Ok(StealthSyncResult {
status_code: result.status_code,
status_text: result.status_text,
headers: result.headers,
body: result.body,
})
}
pub fn stealth_profile_to_ssl_config(profile: &Option<StealthProfile>) -> SSLConfig {
let mut config = SSLConfig::default();
if let Some(p) = profile {
let tls_cfg = TlsFingerprintConfig::from_fingerprint(&p.tls);
config.tls12_cipher_list = bun_core::dupe_z(tls_cfg.tls12_cipher_list.as_bytes());
config.tls13_cipher_suites = bun_core::dupe_z(tls_cfg.tls13_cipher_suites.as_bytes());
config.tls_curves_list = bun_core::dupe_z(tls_cfg.curves_list.as_bytes());
config.tls_sigalgs_list = bun_core::dupe_z(tls_cfg.sigalgs_list.as_bytes());
config.h2_settings_payload = Some(h2_settings_wire_format(&p.http2).into_boxed_slice());
config.h2_initial_window_size = p.http2.initial_window_size;
config.h2_pseudo_header_order = Some(
p.http2
.pseudo_header_order
.iter()
.map(|name| name.to_string().into_boxed_str())
.collect(),
);
config.h2_priority_frames = Some(
p.http2
.priority_frames
.iter()
.map(|f| bun_http::ssl_config::H2PriorityFrame {
stream_id: f.stream_id,
stream_dependency: f.stream_dependency,
exclusive: f.exclusive,
weight: f.weight,
})
.collect(),
);
}
config
}
#[allow(dead_code)]
fn alpn_wire_format(fp: &TlsFingerprint) -> Vec<u8> {
let mut wire = Vec::new();
for proto in &fp.alpn_protocols {
let len = proto.len().min(255) as u8;
wire.push(len);
wire.extend_from_slice(&proto[..len as usize]);
}
wire
}
pub fn h2_alpn_offer(fp: &Http2Fingerprint) -> &'static str {
if fp.pseudo_header_order.is_empty() {
"http/1.1"
} else {
"h2,http/1.1"
}
}
#[allow(dead_code)]
fn h2_settings_wire_format(fp: &Http2Fingerprint) -> Vec<u8> {
let settings = fp.settings_frame_payload();
let mut wire = Vec::with_capacity(settings.len() * 6);
for (id, value) in &settings {
wire.extend_from_slice(&id.to_be_bytes());
wire.extend_from_slice(&value.to_be_bytes());
}
wire
}
pub fn ordered_headers<'a>(
profile: &Option<StealthProfile>,
headers: &'a [(String, String)],
) -> Vec<(&'a str, &'a str)> {
let refs: Vec<(&'a str, &'a str)> = headers
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
match profile {
Some(p) => p.http2.ordered_headers(&refs),
None => refs,
}
}
pub fn ja3_hash(profile: &Option<StealthProfile>) -> Option<String> {
profile.as_ref().map(|p| p.tls.compute_ja3())
}
pub fn akamai_fingerprint(profile: &Option<StealthProfile>) -> Option<String> {
profile.as_ref().map(|p| p.http2.akamai_fingerprint())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_stealth_request_no_profile() {
let config = create_stealth_request(&None, Method::GET, "https://example.com", &[], None);
assert_eq!(config.method.as_str(), "GET");
assert!(config.user_agent.is_none());
}
#[test]
fn test_create_stealth_request_firefox() {
let profile = StealthProfile::firefox_default();
let config = create_stealth_request(
&Some(profile),
Method::POST,
"https://example.com",
&[],
Some(b"test"),
);
assert_eq!(config.method.as_str(), "POST");
assert!(config.user_agent.is_some());
}
#[test]
fn test_create_stealth_request_chrome() {
let profile = StealthProfile::chrome_default();
let config = create_stealth_request(
&Some(profile),
Method::GET,
"https://example.com",
&[],
None,
);
assert!(config.user_agent.is_some());
}
#[test]
fn test_ordered_headers_no_profile() {
let headers = vec![
("content-type".to_string(), "text/html".to_string()),
(":method".to_string(), "GET".to_string()),
];
let ordered = ordered_headers(&None, &headers);
assert_eq!(ordered.len(), 2);
assert_eq!(ordered[0].0, "content-type");
}
#[test]
fn test_ordered_headers_firefox_pseudo_first() {
let profile = StealthProfile::firefox_default();
let headers = vec![
("content-length".to_string(), "100".to_string()),
(":method".to_string(), "GET".to_string()),
(":path".to_string(), "/".to_string()),
(":authority".to_string(), "example.com".to_string()),
(":scheme".to_string(), "https".to_string()),
];
let ordered = ordered_headers(&Some(profile), &headers);
assert!(ordered[0].0.starts_with(':'));
assert!(ordered[1].0.starts_with(':'));
}
#[test]
fn test_ordered_headers_chrome_order() {
let profile = StealthProfile::chrome_default();
let headers = vec![
("accept".to_string(), "*/*".to_string()),
(":method".to_string(), "GET".to_string()),
(":authority".to_string(), "example.com".to_string()),
(":scheme".to_string(), "https".to_string()),
(":path".to_string(), "/".to_string()),
];
let ordered = ordered_headers(&Some(profile), &headers);
assert_eq!(ordered[0].0, ":method");
assert_eq!(ordered[1].0, ":authority");
assert_eq!(ordered[2].0, ":scheme");
assert_eq!(ordered[3].0, ":path");
}
#[test]
fn test_ja3_hash_none() {
assert!(ja3_hash(&None).is_none());
}
#[test]
fn test_ja3_hash_firefox() {
let profile = StealthProfile::firefox_default();
let hash = ja3_hash(&Some(profile)).unwrap();
assert!(hash.starts_with("771,"));
}
#[test]
fn test_ja3_hash_chrome() {
let profile = StealthProfile::chrome_default();
let hash = ja3_hash(&Some(profile)).unwrap();
assert!(hash.starts_with("771,"));
}
#[test]
fn test_akamai_fingerprint_none() {
assert!(akamai_fingerprint(&None).is_none());
}
#[test]
fn test_akamai_fingerprint_firefox() {
let profile = StealthProfile::firefox_default();
let fp = akamai_fingerprint(&Some(profile)).unwrap();
let parts: Vec<&str> = fp.split(':').collect();
assert_eq!(parts.len(), 6);
}
#[test]
fn test_akamai_fingerprint_chrome() {
let profile = StealthProfile::chrome_default();
let fp = akamai_fingerprint(&Some(profile)).unwrap();
let parts: Vec<&str> = fp.split(':').collect();
assert_eq!(parts.len(), 6);
}
#[test]
fn test_profiles_different_ja3() {
let ff = StealthProfile::firefox_default();
let ch = StealthProfile::chrome_default();
assert_ne!(ja3_hash(&Some(ff)).unwrap(), ja3_hash(&Some(ch)).unwrap());
}
#[test]
fn test_profiles_different_akamai() {
let ff = StealthProfile::firefox_default();
let ch = StealthProfile::chrome_default();
assert_ne!(
akamai_fingerprint(&Some(ff)).unwrap(),
akamai_fingerprint(&Some(ch)).unwrap()
);
}
#[test]
fn test_h2_alpn_offer_with_h2() {
let profile = StealthProfile::firefox_default();
let offer = h2_alpn_offer(&profile.http2);
assert!(offer.contains("h2"));
}
#[test]
fn test_h2_alpn_offer_without_h2() {
let empty_fp = Http2Fingerprint {
header_table_size: 65536,
enable_push: false,
max_concurrent_streams: 100,
initial_window_size: 65535,
max_frame_size: 16384,
max_header_list_size: 65536,
window_update_size: 65535,
pseudo_header_order: vec![],
priority_frame_mode: PriorityFrameMode::None,
priority_frames: vec![],
};
let offer = h2_alpn_offer(&empty_fp);
assert_eq!(offer, "http/1.1");
}
#[test]
fn test_alpn_wire_firefox() {
let profile = StealthProfile::firefox_default();
let wire = alpn_wire_format(&profile.tls);
assert!(!wire.is_empty());
}
#[test]
fn test_h2_settings_wire_firefox() {
let profile = StealthProfile::firefox_default();
let wire = h2_settings_wire_format(&profile.http2);
assert_eq!(wire.len(), 36);
}
#[test]
fn test_h2_settings_wire_chrome() {
let profile = StealthProfile::chrome_default();
let wire = h2_settings_wire_format(&profile.http2);
assert_eq!(wire.len(), 36);
}
#[test]
fn test_create_stealth_request_with_headers() {
let config = create_stealth_request(
&None,
Method::POST,
"https://api.example.com",
&[("content-type".into(), "application/json".into())],
Some(b"{}"),
);
assert_eq!(config.headers.len(), 1);
assert_eq!(config.headers[0].0, "content-type");
assert_eq!(config.body.as_deref(), Some(b"{}" as &[u8]));
}
#[test]
fn test_create_stealth_request_firefox_adds_ua() {
let profile = StealthProfile::firefox_default();
let config =
create_stealth_request(&Some(profile), Method::GET, "https://x.com", &[], None);
let has_ua = config.headers.iter().any(|(k, _)| k == "user-agent");
assert!(has_ua, "Firefox profile must add user-agent header");
assert!(config.user_agent.is_some());
}
#[test]
fn test_create_stealth_request_chrome_adds_ua() {
let profile = StealthProfile::chrome_default();
let config =
create_stealth_request(&Some(profile), Method::GET, "https://x.com", &[], None);
let has_ua = config.headers.iter().any(|(k, _)| k == "user-agent");
assert!(has_ua, "Chrome profile must add user-agent header");
}
#[test]
fn test_ordered_headers_empty() {
let ordered = ordered_headers(&None, &[]);
assert!(ordered.is_empty());
}
#[test]
fn test_ordered_headers_no_profile_preserves_order() {
let headers = vec![
("z-header".to_string(), "last".to_string()),
("a-header".to_string(), "first".to_string()),
];
let ordered = ordered_headers(&None, &headers);
assert_eq!(ordered.len(), 2);
assert_eq!(ordered[0].0, "z-header"); assert_eq!(ordered[1].0, "a-header");
}
#[test]
fn test_stealth_sync_result_construction() {
let result = StealthSyncResult {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![("content-type".into(), "text/html".into())],
body: Bytes::from_static(b"<html>"),
};
assert_eq!(result.status_code, 200);
assert_eq!(result.status_text, "OK");
assert_eq!(result.headers.len(), 1);
assert_eq!(&result.body[..], b"<html>");
}
#[test]
fn test_stealth_sync_result_empty() {
let result = StealthSyncResult {
status_code: 204,
status_text: CompactString::new("No Content"),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert!(result.headers.is_empty());
assert!(result.body.is_empty());
}
#[test]
fn test_h2_alpn_offer_firefox() {
let profile = StealthProfile::firefox_default();
let offer = h2_alpn_offer(&profile.http2);
assert!(offer.contains("h2"), "Firefox should offer h2");
assert!(
offer.contains("http/1.1"),
"Firefox should fallback to http/1.1"
);
}
#[test]
fn test_h2_alpn_offer_chrome() {
let profile = StealthProfile::chrome_default();
let offer = h2_alpn_offer(&profile.http2);
assert!(offer.contains("h2"), "Chrome should offer h2");
}
#[test]
fn test_ja3_hash_firefox_chrome_differ() {
let ff_hash = ja3_hash(&Some(StealthProfile::firefox_default())).unwrap();
let ch_hash = ja3_hash(&Some(StealthProfile::chrome_default())).unwrap();
assert_ne!(ff_hash, ch_hash, "Firefox and Chrome JA3 must differ");
}
#[test]
fn test_ssl_config_tls12_list_matches_single_source() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile.clone()));
let expected = bao_stealth::boringssl_cipher_list_string(&profile.tls.cipher_suites);
let got = unsafe { std::ffi::CStr::from_ptr(config.tls12_cipher_list) }
.to_str()
.expect("utf8");
assert_eq!(got, expected);
assert!(!got.split(':').any(|n| n.starts_with("DHE-")));
assert!(!got.contains("TLS_AES"));
}
#[test]
fn test_ssl_config_curves_list_excludes_ffdhe() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let curves = unsafe { std::ffi::CStr::from_ptr(config.tls_curves_list) }
.to_str()
.expect("utf8");
assert_eq!(curves, "X25519:P-256:P-384:P-521");
assert!(!curves.contains("ffdhe"));
}
#[test]
fn test_alpn_wire_format_structure() {
let profile = StealthProfile::firefox_default();
let wire = alpn_wire_format(&profile.tls);
assert_eq!(wire.len(), 12);
}
#[test]
fn test_alpn_wire_chrome_structure() {
let profile = StealthProfile::chrome_default();
let wire = alpn_wire_format(&profile.tls);
assert_eq!(wire.len(), 12); }
#[test]
fn test_h2_settings_wire_has_6_entries() {
let profile = StealthProfile::firefox_default();
let wire = h2_settings_wire_format(&profile.http2);
assert_eq!(wire.len() % 6, 0, "wire length must be multiple of 6");
}
#[test]
fn test_ssl_config_no_profile_is_default() {
let config = stealth_profile_to_ssl_config(&None);
assert!(config.tls12_cipher_list.is_null());
assert!(config.tls13_cipher_suites.is_null());
assert!(config.tls_curves_list.is_null());
assert!(config.tls_sigalgs_list.is_null());
}
#[test]
fn test_ssl_config_firefox_has_fingerprint_fields() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert!(
!config.tls12_cipher_list.is_null(),
"tls12_cipher_list should be set"
);
assert!(
!config.tls13_cipher_suites.is_null(),
"tls13_cipher_suites should be set"
);
assert!(
!config.tls_curves_list.is_null(),
"tls_curves_list should be set"
);
assert!(
!config.tls_sigalgs_list.is_null(),
"tls_sigalgs_list should be set"
);
}
#[test]
fn test_ssl_config_chrome_has_fingerprint_fields() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert!(!config.tls12_cipher_list.is_null());
assert!(!config.tls13_cipher_suites.is_null());
assert!(!config.tls_curves_list.is_null());
assert!(!config.tls_sigalgs_list.is_null());
}
#[test]
fn test_ssl_config_firefox_tls12_cipher_content() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let s = unsafe { std::ffi::CStr::from_ptr(config.tls12_cipher_list) }
.to_str()
.unwrap();
assert!(
s.contains("ECDHE"),
"TLS 1.2 ciphers should contain ECDHE: {}",
s
);
}
#[test]
fn test_ssl_config_firefox_tls13_cipher_content() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let s = unsafe { std::ffi::CStr::from_ptr(config.tls13_cipher_suites) }
.to_str()
.unwrap();
assert!(
s.contains("TLS_AES_128_GCM_SHA256"),
"TLS 1.3 should contain AES-128: {}",
s
);
}
#[test]
fn test_ssl_config_firefox_curves_content() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let s = unsafe { std::ffi::CStr::from_ptr(config.tls_curves_list) }
.to_str()
.unwrap();
assert!(s.contains("X25519"), "Curves should contain X25519: {}", s);
}
#[test]
fn test_ssl_config_firefox_sigalgs_content() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let s = unsafe { std::ffi::CStr::from_ptr(config.tls_sigalgs_list) }
.to_str()
.unwrap();
assert!(
s.contains("ecdsa_secp256r1_sha256"),
"Sigalgs should contain ECDSA P-256: {}",
s
);
}
#[test]
fn test_ssl_config_firefox_chrome_tls12_converge_curves_differ() {
let ff = StealthProfile::firefox_default();
let ch = StealthProfile::chrome_default();
let ff_config = stealth_profile_to_ssl_config(&Some(ff));
let ch_config = stealth_profile_to_ssl_config(&Some(ch));
let ff_s = unsafe { std::ffi::CStr::from_ptr(ff_config.tls12_cipher_list) }
.to_str()
.unwrap();
let ch_s = unsafe { std::ffi::CStr::from_ptr(ch_config.tls12_cipher_list) }
.to_str()
.unwrap();
assert_eq!(
ff_s, ch_s,
"Firefox/Chrome TLS 1.2 lists must converge after DHE filtering"
);
assert_eq!(
ff_s,
TlsFingerprint::firefox().tls12_cipher_list_string(),
"exposed list must equal Firefox DHE-filtered set"
);
assert_eq!(
ch_s,
TlsFingerprint::chrome().tls12_cipher_list_string(),
"exposed list must equal Chrome DHE-filtered set"
);
let ff_c = unsafe { std::ffi::CStr::from_ptr(ff_config.tls_curves_list) }
.to_str()
.unwrap();
let ch_c = unsafe { std::ffi::CStr::from_ptr(ch_config.tls_curves_list) }
.to_str()
.unwrap();
assert_ne!(ff_c, ch_c, "Firefox/Chrome curves must differ (P-521)");
}
#[test]
fn test_ssl_config_drop_does_not_leak() {
let profile = StealthProfile::firefox_default();
let _config = stealth_profile_to_ssl_config(&Some(profile));
}
#[test]
fn test_ssl_config_no_profile_h2_fields_default() {
let config = stealth_profile_to_ssl_config(&None);
assert!(
config.h2_settings_payload.is_none(),
"no profile → None h2_settings_payload"
);
assert_eq!(
config.h2_initial_window_size, 0,
"no profile → h2_initial_window_size=0"
);
assert!(
config.h2_pseudo_header_order.is_none(),
"no profile → None h2_pseudo_header_order"
);
assert!(
config.h2_priority_frames.is_none(),
"no profile → None h2_priority_frames"
);
}
#[test]
fn test_ssl_config_firefox_h2_pseudo_header_order() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let order = config
.h2_pseudo_header_order
.as_deref()
.expect("Firefox profile must set h2_pseudo_header_order");
let names: Vec<&str> = order.iter().map(|s| &**s).collect();
assert_eq!(
names,
vec![":method", ":path", ":authority", ":scheme"],
"Firefox pseudo-header wire order"
);
}
#[test]
fn test_ssl_config_chrome_h2_pseudo_header_order() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let order = config
.h2_pseudo_header_order
.as_deref()
.expect("Chrome profile must set h2_pseudo_header_order");
let names: Vec<&str> = order.iter().map(|s| &**s).collect();
assert_eq!(
names,
vec![":method", ":authority", ":scheme", ":path"],
"Chrome pseudo-header wire order"
);
}
#[test]
fn test_ssl_config_firefox_h2_priority_frames() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let frames = config
.h2_priority_frames
.as_deref()
.expect("Firefox profile must set h2_priority_frames");
assert_eq!(frames.len(), 4, "Firefox reserves 4 priority-tree streams");
let expected = [
(3u32, 0u32, false, 40u8),
(5, 0, false, 109),
(7, 0, false, 138),
(11, 0, false, 255),
];
for (frame, want) in frames.iter().zip(expected.iter()) {
assert_eq!(
(frame.stream_id, frame.stream_dependency, frame.exclusive, frame.weight),
*want
);
}
}
#[test]
fn test_ssl_config_chrome_h2_priority_frames_empty() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let frames = config
.h2_priority_frames
.as_deref()
.expect("Chrome profile must set h2_priority_frames (possibly empty)");
assert!(frames.is_empty(), "Chrome sends no PRIORITY frames");
}
#[test]
fn test_ssl_config_firefox_h2_settings_payload_set() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config
.h2_settings_payload
.as_deref()
.expect("Firefox profile must set h2_settings_payload");
assert_eq!(
payload.len(),
36,
"Firefox H2 SETTINGS payload = 6 settings × 6 bytes = 36"
);
}
#[test]
fn test_ssl_config_chrome_h2_settings_payload_set() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config
.h2_settings_payload
.as_deref()
.expect("Chrome profile must set h2_settings_payload");
assert_eq!(
payload.len(),
36,
"Chrome H2 SETTINGS payload = 6 settings × 6 bytes = 36"
);
}
#[test]
fn test_ssl_config_firefox_h2_initial_window_size() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert_eq!(
config.h2_initial_window_size, 131072,
"Firefox initial_window_size=131072"
);
}
#[test]
fn test_ssl_config_chrome_h2_initial_window_size() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert_eq!(
config.h2_initial_window_size, 6291456,
"Chrome initial_window_size=6291456"
);
}
#[test]
fn test_ssl_config_h2_settings_firefox_chrome_differ() {
let ff = StealthProfile::firefox_default();
let ch = StealthProfile::chrome_default();
let ff_config = stealth_profile_to_ssl_config(&Some(ff));
let ch_config = stealth_profile_to_ssl_config(&Some(ch));
let ff_payload = ff_config.h2_settings_payload.as_deref().unwrap();
let ch_payload = ch_config.h2_settings_payload.as_deref().unwrap();
assert_ne!(
ff_payload, ch_payload,
"Firefox and Chrome H2 SETTINGS binary must differ"
);
}
#[test]
fn test_h2_settings_wire_format_firefox_first_setting() {
let profile = StealthProfile::firefox_default();
let wire = h2_settings_wire_format(&profile.http2);
assert_eq!(wire[0..2], [0x00, 0x01], "first setting ID = 0x0001");
let value = u32::from_be_bytes([wire[2], wire[3], wire[4], wire[5]]);
assert_eq!(value, 65536, "Firefox HEADER_TABLE_SIZE = 65536");
}
#[test]
fn test_h2_settings_wire_format_chrome_window_size() {
let profile = StealthProfile::chrome_default();
let wire = h2_settings_wire_format(&profile.http2);
let mut found_iws = false;
for i in (0..wire.len()).step_by(6) {
let id = u16::from_be_bytes([wire[i], wire[i + 1]]);
if id == 0x04 {
let value =
u32::from_be_bytes([wire[i + 2], wire[i + 3], wire[i + 4], wire[i + 5]]);
assert_eq!(value, 6291456, "Chrome INITIAL_WINDOW_SIZE = 6291456");
found_iws = true;
break;
}
}
assert!(found_iws, "INITIAL_WINDOW_SIZE setting must be present");
}
#[test]
fn test_h2_settings_wire_format_firefox_enable_push_zero() {
let profile = StealthProfile::firefox_default();
let wire = h2_settings_wire_format(&profile.http2);
for i in (0..wire.len()).step_by(6) {
let id = u16::from_be_bytes([wire[i], wire[i + 1]]);
if id == 0x02 {
let value =
u32::from_be_bytes([wire[i + 2], wire[i + 3], wire[i + 4], wire[i + 5]]);
assert_eq!(value, 0, "ENABLE_PUSH must be 0");
return;
}
}
panic!("ENABLE_PUSH setting not found");
}
#[test]
fn test_h2_settings_wire_format_chrome_max_concurrent() {
let profile = StealthProfile::chrome_default();
let wire = h2_settings_wire_format(&profile.http2);
for i in (0..wire.len()).step_by(6) {
let id = u16::from_be_bytes([wire[i], wire[i + 1]]);
if id == 0x03 {
let value =
u32::from_be_bytes([wire[i + 2], wire[i + 3], wire[i + 4], wire[i + 5]]);
assert_eq!(value, 1000, "Chrome MAX_CONCURRENT_STREAMS = 1000");
return;
}
}
panic!("MAX_CONCURRENT_STREAMS setting not found");
}
#[test]
fn test_ssl_config_h2_binary_roundtrip() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile.clone()));
let payload = config.h2_settings_payload.as_deref().unwrap();
let original = h2_settings_wire_format(&profile.http2);
assert_eq!(
payload,
&original[..],
"binary roundtrip must match original wire format"
);
}
#[test]
fn test_h2_payload_preserves_nul_bytes() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config.h2_settings_payload.as_deref().unwrap();
assert!(
payload.contains(&0u8),
"binary payload must contain NUL bytes (ENABLE_PUSH value = 0)"
);
assert_eq!(
payload.len(),
36,
"payload must not be truncated at NUL bytes"
);
}
#[test]
fn test_h2_payload_chrome_preserves_nul_bytes() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config.h2_settings_payload.as_deref().unwrap();
assert!(
payload.contains(&0u8),
"Chrome payload must contain NUL bytes"
);
assert_eq!(payload.len(), 36, "Chrome payload must be 36 bytes");
}
#[test]
fn test_h2_firefox_wire_all_settings_big_endian() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config.h2_settings_payload.as_deref().unwrap();
let decoded: Vec<(u16, u32)> = (0..payload.len())
.step_by(6)
.map(|i| {
let id = u16::from_be_bytes([payload[i], payload[i + 1]]);
let value = u32::from_be_bytes([
payload[i + 2],
payload[i + 3],
payload[i + 4],
payload[i + 5],
]);
(id, value)
})
.collect();
assert_eq!(decoded.len(), 6, "Firefox must have exactly 6 settings");
let ht = decoded.iter().find(|(id, _)| *id == 0x01);
assert_eq!(
ht.map(|(_, v)| *v),
Some(65536),
"Firefox HEADER_TABLE_SIZE = 65536"
);
let ep = decoded.iter().find(|(id, _)| *id == 0x02);
assert_eq!(ep.map(|(_, v)| *v), Some(0), "Firefox ENABLE_PUSH = 0");
let mcs = decoded.iter().find(|(id, _)| *id == 0x03);
assert_eq!(
mcs.map(|(_, v)| *v),
Some(100),
"Firefox MAX_CONCURRENT_STREAMS = 100"
);
let iws = decoded.iter().find(|(id, _)| *id == 0x04);
assert_eq!(
iws.map(|(_, v)| *v),
Some(131072),
"Firefox INITIAL_WINDOW_SIZE = 131072"
);
let mfs = decoded.iter().find(|(id, _)| *id == 0x05);
assert_eq!(
mfs.map(|(_, v)| *v),
Some(16384),
"Firefox MAX_FRAME_SIZE = 16384"
);
let mhl = decoded.iter().find(|(id, _)| *id == 0x06);
assert_eq!(
mhl.map(|(_, v)| *v),
Some(262144),
"Firefox MAX_HEADER_LIST_SIZE = 262144"
);
}
#[test]
fn test_h2_chrome_wire_all_settings_big_endian() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config.h2_settings_payload.as_deref().unwrap();
let decoded: Vec<(u16, u32)> = (0..payload.len())
.step_by(6)
.map(|i| {
let id = u16::from_be_bytes([payload[i], payload[i + 1]]);
let value = u32::from_be_bytes([
payload[i + 2],
payload[i + 3],
payload[i + 4],
payload[i + 5],
]);
(id, value)
})
.collect();
assert_eq!(decoded.len(), 6, "Chrome must have exactly 6 settings");
let mcs = decoded.iter().find(|(id, _)| *id == 0x03);
assert_eq!(
mcs.map(|(_, v)| *v),
Some(1000),
"Chrome MAX_CONCURRENT_STREAMS = 1000"
);
let iws = decoded.iter().find(|(id, _)| *id == 0x04);
assert_eq!(
iws.map(|(_, v)| *v),
Some(6291456),
"Chrome INITIAL_WINDOW_SIZE = 6291456"
);
}
#[test]
fn test_h2_window_size_firefox_pipeline() {
let profile = StealthProfile::firefox_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert_eq!(
config.h2_initial_window_size, 131072,
"Firefox window size must be 131072 (128 KiB)"
);
}
#[test]
fn test_h2_window_size_chrome_pipeline() {
let profile = StealthProfile::chrome_default();
let config = stealth_profile_to_ssl_config(&Some(profile));
assert_eq!(
config.h2_initial_window_size, 6291456,
"Chrome window size must be 6291456 (6 MiB)"
);
}
#[test]
fn test_h2_window_size_default_pipeline() {
let config = stealth_profile_to_ssl_config(&None);
assert_eq!(
config.h2_initial_window_size, 0,
"no profile → window size 0 (use LOCAL_INITIAL_WINDOW_SIZE)"
);
}
#[test]
fn test_h2_firefox_chrome_payloads_differ_in_all_bytes() {
let ff = StealthProfile::firefox_default();
let ch = StealthProfile::chrome_default();
let ff_config = stealth_profile_to_ssl_config(&Some(ff));
let ch_config = stealth_profile_to_ssl_config(&Some(ch));
let ff_payload = ff_config.h2_settings_payload.as_deref().unwrap();
let ch_payload = ch_config.h2_settings_payload.as_deref().unwrap();
assert_ne!(
ff_payload, ch_payload,
"Firefox and Chrome H2 SETTINGS payloads must differ"
);
}
#[test]
fn test_h2_no_profile_has_none_payload() {
let config = stealth_profile_to_ssl_config(&None);
assert!(
config.h2_settings_payload.is_none(),
"no profile → h2_settings_payload must be None"
);
}
#[test]
fn test_h2_payload_byte_level_identity_with_wire_format() {
for profile_fn in [
StealthProfile::firefox_default,
StealthProfile::chrome_default,
] {
let profile = profile_fn();
let wire = h2_settings_wire_format(&profile.http2);
let config = stealth_profile_to_ssl_config(&Some(profile));
let payload = config.h2_settings_payload.clone().unwrap();
assert_eq!(
&payload[..],
&wire,
"SSLConfig payload must exactly match wire format bytes"
);
}
}
}