use crate::stealth::{DeviceClass, StealthProfile};
use bytes::Bytes;
use http2::client::{Builder, Connection, SendRequest};
use http2::frame::{PseudoId, PseudoOrder, SettingId, SettingsOrder, StreamDependency, StreamId};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::net::error::NetError;
const HEADER_TABLE_SIZE: u32 = 65_536; const ENABLE_PUSH: bool = false; const INITIAL_STREAM_WINDOW_SIZE: u32 = 6_291_456; const MAX_HEADER_LIST_SIZE: u32 = 262_144; const INITIAL_CONNECTION_WINDOW_SIZE: u32 = 15_728_640;
const SAFARI_IOS_INITIAL_STREAM_WINDOW_SIZE: u32 = 2_097_152; const SAFARI_IOS_MAX_CONCURRENT_STREAMS: u32 = 100;
const SAFARI_IOS_INITIAL_CONNECTION_WINDOW_SIZE: u32 = 10_485_760;
const FIREFOX_INITIAL_STREAM_WINDOW_SIZE: u32 = 131_072; const FIREFOX_MAX_FRAME_SIZE: u32 = 16_384;
const FIREFOX_INITIAL_CONNECTION_WINDOW_SIZE: u32 = 12_582_912;
pub async fn handshake<T>(
io: T,
profile: &StealthProfile,
) -> Result<(SendRequest<Bytes>, Connection<T, Bytes>), NetError>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let is_safari_ios = profile.device_class == DeviceClass::MobileIOS;
let is_firefox = profile.browser_name == "Firefox";
let pseudo_order = if is_safari_ios {
PseudoOrder::builder()
.push(PseudoId::Method)
.push(PseudoId::Scheme)
.push(PseudoId::Authority)
.push(PseudoId::Path)
.build()
} else if is_firefox {
PseudoOrder::builder()
.push(PseudoId::Method)
.push(PseudoId::Path)
.push(PseudoId::Authority)
.push(PseudoId::Scheme)
.build()
} else {
PseudoOrder::builder()
.push(PseudoId::Method)
.push(PseudoId::Authority)
.push(PseudoId::Scheme)
.push(PseudoId::Path)
.build()
};
let settings_order = if is_safari_ios {
SettingsOrder::builder()
.push(SettingId::EnablePush)
.push(SettingId::MaxConcurrentStreams)
.push(SettingId::InitialWindowSize)
.push(SettingId::NoRfc7540Priorities)
.build()
} else if is_firefox {
SettingsOrder::builder()
.push(SettingId::HeaderTableSize)
.push(SettingId::InitialWindowSize)
.push(SettingId::MaxFrameSize)
.build()
} else {
SettingsOrder::builder()
.push(SettingId::HeaderTableSize)
.push(SettingId::EnablePush)
.push(SettingId::MaxConcurrentStreams)
.push(SettingId::InitialWindowSize)
.push(SettingId::MaxFrameSize)
.push(SettingId::MaxHeaderListSize)
.push(SettingId::EnableConnectProtocol)
.push(SettingId::NoRfc7540Priorities)
.build()
};
let mut builder = Builder::new();
if is_safari_ios {
builder
.enable_push(ENABLE_PUSH)
.max_concurrent_streams(SAFARI_IOS_MAX_CONCURRENT_STREAMS)
.initial_window_size(SAFARI_IOS_INITIAL_STREAM_WINDOW_SIZE)
.max_header_list_size(MAX_HEADER_LIST_SIZE)
.initial_connection_window_size(SAFARI_IOS_INITIAL_CONNECTION_WINDOW_SIZE)
.headers_pseudo_order(pseudo_order)
.settings_order(settings_order);
} else if is_firefox {
builder
.header_table_size(HEADER_TABLE_SIZE)
.initial_window_size(FIREFOX_INITIAL_STREAM_WINDOW_SIZE)
.max_frame_size(FIREFOX_MAX_FRAME_SIZE)
.initial_connection_window_size(FIREFOX_INITIAL_CONNECTION_WINDOW_SIZE)
.headers_pseudo_order(pseudo_order)
.settings_order(settings_order);
} else {
builder
.header_table_size(HEADER_TABLE_SIZE)
.enable_push(ENABLE_PUSH)
.initial_window_size(INITIAL_STREAM_WINDOW_SIZE)
.max_header_list_size(MAX_HEADER_LIST_SIZE)
.initial_connection_window_size(INITIAL_CONNECTION_WINDOW_SIZE)
.headers_pseudo_order(pseudo_order)
.settings_order(settings_order)
.headers_stream_dependency(StreamDependency::new(
StreamId::zero(),
255,
true,
));
}
builder
.handshake(io)
.await
.map_err(|e| NetError::Http(format!("HTTP/2 handshake failed: {e}")))
}
pub async fn send_get(
sender: &mut SendRequest<Bytes>,
uri: &str,
_host: &str,
headers: &[(String, String)],
) -> Result<(http::response::Parts, Vec<u8>), NetError> {
let mut ready_sender = sender
.clone()
.ready()
.await
.map_err(|e| NetError::Http(format!("HTTP/2 not ready: {e}")))?;
let mut request = http::Request::builder().method(http::Method::GET).uri(uri);
for (name, value) in headers {
request = request.header(name.as_str(), value.as_str());
}
let request = request
.body(())
.map_err(|e| NetError::Http(format!("failed to build request: {e}")))?;
let (response, _) = ready_sender
.send_request(request, true) .map_err(|e| NetError::Http(format!("failed to send request: {e}")))?;
let response = response
.await
.map_err(|e| NetError::Http(format!("HTTP/2 response error: {e}")))?;
let (parts, mut body) = response.into_parts();
let mut data = Vec::new();
while let Some(chunk) = body.data().await {
let chunk = chunk.map_err(|e| NetError::Http(format!("body read error: {e}")))?;
let _ = body.flow_control().release_capacity(chunk.len());
data.extend_from_slice(&chunk);
}
Ok((parts, data))
}
pub async fn send_post(
sender: &mut SendRequest<Bytes>,
uri: &str,
_host: &str,
headers: &[(String, String)],
body: &[u8],
) -> Result<(http::response::Parts, Vec<u8>), NetError> {
let mut ready_sender = sender
.clone()
.ready()
.await
.map_err(|e| NetError::Http(format!("HTTP/2 not ready: {e}")))?;
let mut request = http::Request::builder()
.method(http::Method::POST)
.uri(uri)
.header("content-length", body.len().to_string());
for (name, value) in headers {
request = request.header(name.as_str(), value.as_str());
}
let request = request
.body(())
.map_err(|e| NetError::Http(format!("failed to build request: {e}")))?;
let (response, mut send_stream) = ready_sender
.send_request(request, false) .map_err(|e| NetError::Http(format!("failed to send request: {e}")))?;
send_stream
.send_data(Bytes::copy_from_slice(body), true)
.map_err(|e| NetError::Http(format!("failed to send body: {e}")))?;
let response = response
.await
.map_err(|e| NetError::Http(format!("HTTP/2 response error: {e}")))?;
let (parts, mut resp_body) = response.into_parts();
let mut data = Vec::new();
while let Some(chunk) = resp_body.data().await {
let chunk = chunk.map_err(|e| NetError::Http(format!("body read error: {e}")))?;
let _ = resp_body.flow_control().release_capacity(chunk.len());
data.extend_from_slice(&chunk);
}
Ok((parts, data))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore] async fn h2_get_httpbin() {
let profile = crate::stealth::presets::chrome_148_macos();
let connector = crate::net::tls::chrome_connector(&profile).unwrap();
let tcp = crate::net::tcp::connect("httpbin.org", 443, std::time::Duration::from_secs(10))
.await
.unwrap();
let tls = crate::net::tls::connect_tls(&connector, &profile, "httpbin.org", tcp)
.await
.unwrap();
assert_eq!(
crate::net::tls::negotiated_alpn(&tls),
Some(b"h2".as_slice())
);
let (mut sender, conn) = handshake(tls, &profile).await.unwrap();
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("H2 connection error: {e}");
}
});
let (parts, body) = send_get(&mut sender, "https://httpbin.org/get", "httpbin.org", &[])
.await
.unwrap();
assert_eq!(parts.status, 200);
assert!(!body.is_empty());
let text = String::from_utf8_lossy(&body);
assert!(text.contains("httpbin.org"), "Response: {text}");
}
}