use std::collections::VecDeque;
use futures_lite::io::{AsyncReadExt, AsyncWriteExt};
use hpack::encoder::encode_integer_into;
use crate::browser_emulation::Http2Fingerprint;
use crate::decode::CompressionMode;
use crate::error::{Error, ErrorKind, Result};
use crate::header::HeaderMap;
use crate::request::{Method, TimeoutConfig};
use crate::url::Url;
use crate::util::build_httpx_regular_headers;
use super::transport::{BoxedStream, with_timeout_io};
pub(super) const DEFAULT_MAX_FRAME_SIZE: usize = 16 * 1024;
pub(super) const MAX_FRAME_SIZE_UPPER_BOUND: usize = (1 << 24) - 1;
pub(super) const DEFAULT_INITIAL_WINDOW_SIZE: i32 = 65_535;
pub(super) const MAX_FLOW_CONTROL_WINDOW: i32 = 0x7FFF_FFFF;
pub(super) const DEFAULT_HEADER_TABLE_SIZE: usize = 4096;
const CLIENT_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
const SETTING_HEADER_TABLE_SIZE: u16 = 0x1;
const SETTING_ENABLE_PUSH: u16 = 0x2;
const SETTING_MAX_CONCURRENT_STREAMS: u16 = 0x3;
const SETTING_INITIAL_WINDOW_SIZE: u16 = 0x4;
const SETTING_MAX_FRAME_SIZE: u16 = 0x5;
const SETTING_MAX_HEADER_LIST_SIZE: u16 = 0x6;
const HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK: u8 = 0x20;
const HPACK_LITERAL_WITH_INDEXING_MASK: u8 = 0x40;
const HPACK_INDEXED_HEADER_MASK: u8 = 0x80;
const HPACK_STATIC_TABLE: &[(&[u8], &[u8])] = &[
(b":authority", b""),
(b":method", b"GET"),
(b":method", b"POST"),
(b":path", b"/"),
(b":path", b"/index.html"),
(b":scheme", b"http"),
(b":scheme", b"https"),
(b":status", b"200"),
(b":status", b"204"),
(b":status", b"206"),
(b":status", b"304"),
(b":status", b"400"),
(b":status", b"404"),
(b":status", b"500"),
(b"accept-charset", b""),
(b"accept-encoding", b"gzip, deflate"),
(b"accept-language", b""),
(b"accept-ranges", b""),
(b"accept", b""),
(b"access-control-allow-origin", b""),
(b"age", b""),
(b"allow", b""),
(b"authorization", b""),
(b"cache-control", b""),
(b"content-disposition", b""),
(b"content-encoding", b""),
(b"content-language", b""),
(b"content-length", b""),
(b"content-location", b""),
(b"content-range", b""),
(b"content-type", b""),
(b"cookie", b""),
(b"date", b""),
(b"etag", b""),
(b"expect", b""),
(b"expires", b""),
(b"from", b""),
(b"host", b""),
(b"if-match", b""),
(b"if-modified-since", b""),
(b"if-none-match", b""),
(b"if-range", b""),
(b"if-unmodified-since", b""),
(b"last-modified", b""),
(b"link", b""),
(b"location", b""),
(b"max-forwards", b""),
(b"proxy-authenticate", b""),
(b"proxy-authorization", b""),
(b"range", b""),
(b"referer", b""),
(b"refresh", b""),
(b"retry-after", b""),
(b"server", b""),
(b"set-cookie", b""),
(b"strict-transport-security", b""),
(b"transfer-encoding", b""),
(b"user-agent", b""),
(b"vary", b""),
(b"via", b""),
(b"www-authenticate", b""),
];
struct HpackEncoder {
dynamic_table: VecDeque<(Vec<u8>, Vec<u8>)>,
dynamic_table_size: usize,
max_dynamic_table_size: usize,
pending_table_size_update: Option<usize>,
}
impl HpackEncoder {
fn new() -> Self {
Self {
dynamic_table: VecDeque::new(),
dynamic_table_size: 0,
max_dynamic_table_size: DEFAULT_HEADER_TABLE_SIZE,
pending_table_size_update: None,
}
}
fn set_max_dynamic_table_size(&mut self, size: usize) {
if self.max_dynamic_table_size == size {
return;
}
self.max_dynamic_table_size = size;
self.consolidate_dynamic_table();
self.pending_table_size_update = Some(size);
}
fn encode_request(&mut self, headers: &[(String, String)]) -> Result<Vec<u8>> {
let mut encoded = Vec::new();
if let Some(size) = self.pending_table_size_update.take() {
self.encode_table_size_update(size, &mut encoded)?;
}
for (name, value) in headers {
self.encode_header(name.as_bytes(), value.as_bytes(), &mut encoded)?;
}
Ok(encoded)
}
fn encode_header(&mut self, name: &[u8], value: &[u8], output: &mut Vec<u8>) -> Result<()> {
match self.find_header(name, value) {
Some((index, true)) => self.encode_indexed(index, output),
Some((index, false)) => self.encode_indexed_name(index, value, false, output),
None => {
self.encode_literal(name, value, true, output)?;
self.insert_dynamic_header(name, value);
Ok(())
}
}
}
fn find_header(&self, name: &[u8], value: &[u8]) -> Option<(usize, bool)> {
let mut matching_name = None;
for (index, (candidate_name, candidate_value)) in HPACK_STATIC_TABLE.iter().enumerate() {
if name == *candidate_name {
if value == *candidate_value {
return Some((index + 1, true));
}
matching_name = Some(index + 1);
}
}
for (offset, (candidate_name, candidate_value)) in self.dynamic_table.iter().enumerate() {
if name == candidate_name.as_slice() {
let index = HPACK_STATIC_TABLE.len() + offset + 1;
if value == candidate_value.as_slice() {
return Some((index, true));
}
if matching_name.is_none() {
matching_name = Some(index);
}
}
}
matching_name.map(|index| (index, false))
}
fn encode_table_size_update(&self, size: usize, output: &mut Vec<u8>) -> Result<()> {
encode_hpack_integer(
size,
5,
HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK,
output,
"failed to encode http2 dynamic table size update",
)
}
fn encode_literal(
&self,
name: &[u8],
value: &[u8],
should_index: bool,
output: &mut Vec<u8>,
) -> Result<()> {
let mask = if should_index {
HPACK_LITERAL_WITH_INDEXING_MASK
} else {
0x00
};
encode_hpack_integer(0, 4, mask, output, "failed to encode http2 header name")?;
encode_hpack_string(name, output)?;
encode_hpack_string(value, output)
}
fn encode_indexed_name(
&self,
index: usize,
value: &[u8],
should_index: bool,
output: &mut Vec<u8>,
) -> Result<()> {
let (mask, prefix) = if should_index {
(HPACK_LITERAL_WITH_INDEXING_MASK, 6)
} else {
(0x00, 4)
};
encode_hpack_integer(
index,
prefix,
mask,
output,
"failed to encode indexed http2 header name",
)?;
encode_hpack_string(value, output)
}
fn encode_indexed(&self, index: usize, output: &mut Vec<u8>) -> Result<()> {
encode_hpack_integer(
index,
7,
HPACK_INDEXED_HEADER_MASK,
output,
"failed to encode indexed http2 header",
)
}
fn insert_dynamic_header(&mut self, name: &[u8], value: &[u8]) {
self.dynamic_table
.push_front((name.to_vec(), value.to_vec()));
self.dynamic_table_size += hpack_entry_size(name, value);
self.consolidate_dynamic_table();
}
fn consolidate_dynamic_table(&mut self) {
while self.dynamic_table_size > self.max_dynamic_table_size {
let Some((name, value)) = self.dynamic_table.pop_back() else {
self.dynamic_table_size = 0;
break;
};
self.dynamic_table_size = self
.dynamic_table_size
.saturating_sub(hpack_entry_size(&name, &value));
}
}
}
pub(super) struct HeaderCodec {
encoder: HpackEncoder,
decoder: hpack::Decoder<'static>,
}
impl HeaderCodec {
pub(super) fn new() -> Self {
Self {
encoder: HpackEncoder::new(),
decoder: hpack::Decoder::new(),
}
}
pub(super) fn set_max_decoder_table_size(&mut self, size: usize) {
self.decoder.set_max_table_size(size);
}
pub(super) fn encode_request(
&mut self,
headers: &[(String, String)],
peer_header_table_size: usize,
) -> Result<Vec<u8>> {
self.encoder
.set_max_dynamic_table_size(peer_header_table_size);
self.encoder.encode_request(headers)
}
pub(super) fn decode_block(&mut self, block: &[u8]) -> Result<Vec<(String, String)>> {
let decoded = self.decoder.decode(block).map_err(|err| {
Error::new(
ErrorKind::Transport,
format!("failed to decode hpack: {err:?}"),
)
})?;
let mut headers = Vec::with_capacity(decoded.len());
for (name, value) in decoded {
let name = String::from_utf8(name).map_err(|err| {
Error::with_source(ErrorKind::Transport, "invalid hpack header name", err)
})?;
let value = String::from_utf8(value).map_err(|err| {
Error::with_source(ErrorKind::Transport, "invalid hpack header value", err)
})?;
headers.push((name, value));
}
Ok(headers)
}
}
pub(super) fn build_request_header_list(
method: Method,
url: &Url,
headers: &HeaderMap,
cookies: &[(String, String)],
compression_mode: CompressionMode,
body_len: Option<u64>,
pseudo_header_order: Option<&[String]>,
regular_header_order: Option<&[String]>,
) -> Result<Vec<(String, String)>> {
let mut list =
build_request_headers(method, url, headers, cookies, compression_mode, body_len)?;
let pseudo_order_len = pseudo_header_order.map_or(0, |order| order.len());
let regular_order_len = regular_header_order.map_or(0, |order| order.len());
list.sort_by_key(|(name, _)| {
if name.starts_with(':') {
let pos = pseudo_header_order
.and_then(|order| {
order
.iter()
.position(|item| item.eq_ignore_ascii_case(name))
})
.unwrap_or(pseudo_order_len);
(0, pos)
} else {
let pos = regular_header_order
.and_then(|order| {
order
.iter()
.position(|item| item.eq_ignore_ascii_case(name))
})
.unwrap_or(regular_order_len);
(1, pos)
}
});
Ok(list)
}
pub(super) fn header_list_size(headers: &[(String, String)]) -> usize {
headers
.iter()
.map(|(name, value)| name.len() + value.len() + 32)
.sum()
}
fn hpack_entry_size(name: &[u8], value: &[u8]) -> usize {
name.len() + value.len() + 32
}
fn encode_hpack_integer(
value: usize,
prefix: u8,
leading_bits: u8,
output: &mut Vec<u8>,
context: &'static str,
) -> Result<()> {
encode_integer_into(value, prefix, leading_bits, output)
.map_err(|err| Error::with_source(ErrorKind::Transport, context, err))
}
fn encode_hpack_string(bytes: &[u8], output: &mut Vec<u8>) -> Result<()> {
encode_integer_into(bytes.len(), 7, 0x00, output).map_err(|err| {
Error::with_source(
ErrorKind::Transport,
"failed to encode http2 header string",
err,
)
})?;
output.extend_from_slice(bytes);
Ok(())
}
pub(super) fn client_settings_payload_with_fingerprint(
fingerprint: Option<&Http2Fingerprint>,
) -> Result<Vec<u8>> {
let mut payload = Vec::new();
let mut settings = vec![("ENABLE_PUSH", SETTING_ENABLE_PUSH, 0u32)];
if let Some(fp) = fingerprint {
if let Some(size) = fp.header_table_size {
settings.push(("HEADER_TABLE_SIZE", SETTING_HEADER_TABLE_SIZE, size));
}
if let Some(size) = fp.initial_window_size {
if size > MAX_FLOW_CONTROL_WINDOW as u32 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 INITIAL_WINDOW_SIZE value",
));
}
settings.push(("INITIAL_WINDOW_SIZE", SETTING_INITIAL_WINDOW_SIZE, size));
}
if let Some(size) = fp.max_frame_size {
if !(DEFAULT_MAX_FRAME_SIZE as u32..=MAX_FRAME_SIZE_UPPER_BOUND as u32).contains(&size)
{
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 MAX_FRAME_SIZE value",
));
}
settings.push(("MAX_FRAME_SIZE", SETTING_MAX_FRAME_SIZE, size));
}
if let Some(size) = fp.initial_connection_window_size {
if !(DEFAULT_INITIAL_WINDOW_SIZE as u32..=MAX_FLOW_CONTROL_WINDOW as u32)
.contains(&size)
{
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 INITIAL_CONNECTION_WINDOW_SIZE value",
));
}
}
if !fp.settings_order.is_empty() {
settings.sort_by_key(|(name, _, _)| {
fp.settings_order
.iter()
.position(|item| item.eq_ignore_ascii_case(name))
.unwrap_or(fp.settings_order.len())
});
}
}
for (_, setting, value) in settings {
push_setting(&mut payload, setting, value);
}
Ok(payload)
}
fn push_setting(payload: &mut Vec<u8>, setting: u16, value: u32) {
payload.push((setting >> 8) as u8);
payload.push((setting & 0xFF) as u8);
payload.push((value >> 24) as u8);
payload.push((value >> 16) as u8);
payload.push((value >> 8) as u8);
payload.push((value & 0xFF) as u8);
}
pub(super) async fn write_client_preface(
stream: &mut BoxedStream,
timeout_config: TimeoutConfig,
) -> Result<()> {
with_timeout_io(
timeout_config.write,
stream.write_all(CLIENT_PREFACE),
"write timed out",
)
.await
}
pub(super) async fn write_client_settings(
stream: &mut BoxedStream,
timeout_config: TimeoutConfig,
settings_payload: Vec<u8>,
) -> Result<()> {
let settings_frame = Frame::new(FrameType::Settings, Flags::empty(), 0, settings_payload);
write_frame(stream, settings_frame, timeout_config).await
}
pub(super) async fn write_preface_and_settings(
stream: &mut BoxedStream,
timeout_config: TimeoutConfig,
settings_payload: Vec<u8>,
) -> Result<()> {
write_client_preface(stream, timeout_config).await?;
write_client_settings(stream, timeout_config, settings_payload).await
}
pub(super) async fn write_headers_frames(
stream: &mut BoxedStream,
stream_id: u32,
header_block: &[u8],
end_stream: bool,
timeout_config: TimeoutConfig,
max_frame_size: usize,
) -> Result<()> {
let frame_size = normalize_frame_size(max_frame_size);
let chunk_count = header_block.len().max(1).div_ceil(frame_size);
let mut chunks = header_block.chunks(frame_size);
let first_chunk = chunks.next().unwrap_or(&[]);
let mut first_flags = Flags::empty();
if end_stream {
first_flags |= Flags::END_STREAM;
}
if chunk_count == 1 {
first_flags |= Flags::END_HEADERS;
}
write_frame(
stream,
Frame::new(
FrameType::Headers,
first_flags,
stream_id,
first_chunk.to_vec(),
),
timeout_config,
)
.await?;
for (index, chunk) in chunks.enumerate() {
let mut flags = Flags::empty();
if index + 2 == chunk_count {
flags |= Flags::END_HEADERS;
}
write_frame(
stream,
Frame::new(FrameType::Continuation, flags, stream_id, chunk.to_vec()),
timeout_config,
)
.await?;
}
Ok(())
}
pub(super) async fn write_data_frame(
stream: &mut BoxedStream,
stream_id: u32,
payload: &[u8],
end_stream: bool,
timeout_config: TimeoutConfig,
) -> Result<()> {
let mut flags = Flags::empty();
if end_stream {
flags |= Flags::END_STREAM;
}
write_frame(
stream,
Frame::new(FrameType::Data, flags, stream_id, payload.to_vec()),
timeout_config,
)
.await
}
pub(super) async fn write_settings_ack(
stream: &mut BoxedStream,
timeout_config: TimeoutConfig,
) -> Result<()> {
let frame = Frame::new(FrameType::Settings, Flags::ACK, 0, Vec::new());
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn write_ping_ack(
stream: &mut BoxedStream,
payload: &[u8],
timeout_config: TimeoutConfig,
) -> Result<()> {
let frame = Frame::new(FrameType::Ping, Flags::ACK, 0, payload.to_vec());
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn write_ping(
stream: &mut BoxedStream,
payload: &[u8],
timeout_config: TimeoutConfig,
) -> Result<()> {
let frame = Frame::new(FrameType::Ping, Flags::empty(), 0, payload.to_vec());
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn write_rst_stream(
stream: &mut BoxedStream,
stream_id: u32,
error_code: u32,
timeout_config: TimeoutConfig,
) -> Result<()> {
if stream_id == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 RST_STREAM frame must use a non-zero stream",
));
}
let frame = Frame::new(
FrameType::RstStream,
Flags::empty(),
stream_id,
error_code.to_be_bytes().to_vec(),
);
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn write_window_update(
stream: &mut BoxedStream,
stream_id: u32,
increment: u32,
timeout_config: TimeoutConfig,
) -> Result<()> {
if increment == 0 {
return Ok(());
}
let mut payload = [0u8; 4];
let value = increment & 0x7FFF_FFFF;
payload.copy_from_slice(&value.to_be_bytes());
let frame = Frame::new(
FrameType::WindowUpdate,
Flags::empty(),
stream_id,
payload.to_vec(),
);
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn write_priority_frame(
stream: &mut BoxedStream,
stream_id: u32,
stream_dependency: u32,
weight: u16,
exclusive: bool,
timeout_config: TimeoutConfig,
) -> Result<()> {
if stream_id == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 PRIORITY frame must use a non-zero stream",
));
}
if !(1..=256).contains(&weight) {
return Err(Error::new(
ErrorKind::Transport,
"http2 PRIORITY weight must be within 1..=256",
));
}
let dependency = stream_dependency & 0x7FFF_FFFF;
let dependency = if exclusive {
dependency | 0x8000_0000
} else {
dependency
};
let mut payload = Vec::with_capacity(5);
payload.extend_from_slice(&dependency.to_be_bytes());
payload.push((weight - 1) as u8);
let frame = Frame::new(FrameType::Priority, Flags::empty(), stream_id, payload);
write_frame(stream, frame, timeout_config).await
}
pub(super) async fn read_frame(
stream: &mut BoxedStream,
timeout_config: TimeoutConfig,
max_frame_size: usize,
) -> Result<Frame> {
let mut header = [0u8; 9];
with_timeout_io(
timeout_config.read,
stream.read_exact(&mut header),
"read timed out",
)
.await?;
let len = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
if len > max_frame_size {
return Err(Error::new(
ErrorKind::Transport,
format!("http2 frame length {len} exceeds negotiated max frame size {max_frame_size}"),
));
}
let frame_type = FrameType::from(header[3]);
let flags = Flags::from(header[4]);
let stream_id = u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7FFF_FFFF;
let mut payload = vec![0u8; len];
if len > 0 {
with_timeout_io(
timeout_config.read,
stream.read_exact(&mut payload),
"read timed out",
)
.await?;
}
Ok(Frame::new(frame_type, flags, stream_id, payload))
}
pub(super) async fn read_header_block(
stream: &mut BoxedStream,
first: Frame,
timeout_config: TimeoutConfig,
max_frame_size: usize,
) -> Result<HeaderBlock> {
let stream_id = first.stream_id;
let initial = first.into_header_block_fragment()?;
let mut header_block = initial.fragment;
let mut end_headers = initial.end_headers;
while !end_headers {
let continuation = read_frame(stream, timeout_config, max_frame_size).await?;
if continuation.frame_type != FrameType::Continuation || continuation.stream_id != stream_id
{
return Err(Error::new(
ErrorKind::Transport,
"invalid continuation sequence in http2 response",
));
}
let fragment = continuation.into_continuation_fragment()?;
header_block.extend_from_slice(&fragment.fragment);
end_headers = fragment.end_headers;
}
Ok(HeaderBlock {
fragment: header_block,
end_stream: initial.end_stream,
})
}
pub(super) async fn read_push_promise(
stream: &mut BoxedStream,
first: Frame,
timeout_config: TimeoutConfig,
max_frame_size: usize,
) -> Result<PushPromiseBlock> {
let stream_id = first.stream_id;
let initial = first.into_push_promise_fragment()?;
let mut header_block = initial.fragment;
let mut end_headers = initial.end_headers;
while !end_headers {
let continuation = read_frame(stream, timeout_config, max_frame_size).await?;
if continuation.frame_type != FrameType::Continuation || continuation.stream_id != stream_id
{
return Err(Error::new(
ErrorKind::Transport,
"invalid continuation sequence in http2 push promise",
));
}
let fragment = continuation.into_continuation_fragment()?;
header_block.extend_from_slice(&fragment.fragment);
end_headers = fragment.end_headers;
}
Ok(PushPromiseBlock {
promised_stream_id: initial.promised_stream_id,
fragment: header_block,
})
}
async fn write_frame(
stream: &mut BoxedStream,
frame: Frame,
timeout_config: TimeoutConfig,
) -> Result<()> {
let len = frame.payload.len();
let mut buf = Vec::with_capacity(9 + len);
buf.push(((len >> 16) & 0xFF) as u8);
buf.push(((len >> 8) & 0xFF) as u8);
buf.push((len & 0xFF) as u8);
buf.push(frame.frame_type.into());
buf.push(frame.flags.bits());
let stream_id = frame.stream_id & 0x7FFF_FFFF;
buf.extend_from_slice(&stream_id.to_be_bytes());
buf.extend_from_slice(&frame.payload);
with_timeout_io(
timeout_config.write,
stream.write_all(&buf),
"write timed out",
)
.await?;
with_timeout_io(timeout_config.write, stream.flush(), "write timed out").await
}
fn build_request_headers(
method: Method,
url: &Url,
headers: &HeaderMap,
cookies: &[(String, String)],
compression_mode: CompressionMode,
body_len: Option<u64>,
) -> Result<Vec<(String, String)>> {
let mut header_list = Vec::new();
header_list.push((":method".to_owned(), method.as_str().to_owned()));
header_list.push((":scheme".to_owned(), url.scheme().to_owned()));
header_list.push((":authority".to_owned(), url.authority().to_owned()));
header_list.push((":path".to_owned(), url.path_and_query().to_owned()));
header_list.extend(build_httpx_regular_headers(
headers,
cookies,
compression_mode,
body_len,
));
Ok(header_list)
}
fn normalize_frame_size(size: usize) -> usize {
size.clamp(DEFAULT_MAX_FRAME_SIZE, MAX_FRAME_SIZE_UPPER_BOUND)
}
fn strip_padding(mut payload: Vec<u8>) -> Result<Vec<u8>> {
if payload.is_empty() {
return Err(Error::new(
ErrorKind::Transport,
"padded http2 frame payload is empty",
));
}
let pad_length = payload[0] as usize;
payload.drain(..1);
if pad_length > payload.len() {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 padding length",
));
}
payload.truncate(payload.len() - pad_length);
Ok(payload)
}
#[derive(Debug)]
pub(super) struct HeaderBlock {
pub(super) fragment: Vec<u8>,
pub(super) end_stream: bool,
}
pub(super) struct PushPromiseBlock {
pub(super) promised_stream_id: u32,
pub(super) fragment: Vec<u8>,
}
#[derive(Debug)]
struct HeaderBlockFragment {
fragment: Vec<u8>,
end_headers: bool,
end_stream: bool,
}
#[derive(Debug)]
struct PushPromiseFragment {
promised_stream_id: u32,
fragment: Vec<u8>,
end_headers: bool,
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Setting {
HeaderTableSize(usize),
EnablePush(bool),
MaxConcurrentStreams(u32),
InitialWindowSize(i32),
MaxFrameSize(usize),
MaxHeaderListSize(u32),
Unknown(u16, u32),
}
#[derive(Clone, Copy, Debug)]
pub(super) struct GoAwayFrame {
pub(super) last_stream_id: u32,
pub(super) error_code: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum FrameType {
Data,
Headers,
Priority,
RstStream,
Settings,
PushPromise,
Ping,
GoAway,
WindowUpdate,
Continuation,
Unknown,
}
impl From<u8> for FrameType {
fn from(value: u8) -> Self {
match value {
0x0 => Self::Data,
0x1 => Self::Headers,
0x2 => Self::Priority,
0x3 => Self::RstStream,
0x4 => Self::Settings,
0x5 => Self::PushPromise,
0x6 => Self::Ping,
0x7 => Self::GoAway,
0x8 => Self::WindowUpdate,
0x9 => Self::Continuation,
_ => Self::Unknown,
}
}
}
impl From<FrameType> for u8 {
fn from(value: FrameType) -> Self {
match value {
FrameType::Data => 0x0,
FrameType::Headers => 0x1,
FrameType::Priority => 0x2,
FrameType::RstStream => 0x3,
FrameType::Settings => 0x4,
FrameType::PushPromise => 0x5,
FrameType::Ping => 0x6,
FrameType::GoAway => 0x7,
FrameType::WindowUpdate => 0x8,
FrameType::Continuation => 0x9,
FrameType::Unknown => unreachable!("Unknown frame type must not be serialized"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct Flags(u8);
impl Flags {
pub(super) const END_STREAM: Self = Self(0x1);
pub(super) const ACK: Self = Self(0x1);
pub(super) const END_HEADERS: Self = Self(0x4);
pub(super) const PADDED: Self = Self(0x8);
pub(super) const PRIORITY: Self = Self(0x20);
pub(super) fn empty() -> Self {
Self(0)
}
pub(super) fn bits(self) -> u8 {
self.0
}
pub(super) fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
}
impl std::ops::BitOr for Flags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for Flags {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl From<u8> for Flags {
fn from(value: u8) -> Self {
Self(value)
}
}
#[derive(Debug)]
pub(super) struct Frame {
pub(super) frame_type: FrameType,
pub(super) flags: Flags,
pub(super) stream_id: u32,
pub(super) payload: Vec<u8>,
}
impl Frame {
pub(super) fn new(
frame_type: FrameType,
flags: Flags,
stream_id: u32,
payload: Vec<u8>,
) -> Self {
Self {
frame_type,
flags,
stream_id,
payload,
}
}
pub(super) fn is_end_stream(&self) -> bool {
self.flags.contains(Flags::END_STREAM)
}
pub(super) fn into_data_payload(self) -> Result<Vec<u8>> {
if self.frame_type != FrameType::Data {
return Err(Error::new(
ErrorKind::Transport,
"http2 data payload requested for non-data frame",
));
}
if self.flags.contains(Flags::PADDED) {
return strip_padding(self.payload);
}
Ok(self.payload)
}
fn into_header_block_fragment(self) -> Result<HeaderBlockFragment> {
match self.frame_type {
FrameType::Headers => {
let mut payload = self.payload;
if self.flags.contains(Flags::PADDED) {
payload = strip_padding(payload)?;
}
if self.flags.contains(Flags::PRIORITY) {
if payload.len() < 5 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 priority fields length",
));
}
payload.drain(..5);
}
Ok(HeaderBlockFragment {
fragment: payload,
end_headers: self.flags.contains(Flags::END_HEADERS),
end_stream: self.flags.contains(Flags::END_STREAM),
})
}
FrameType::Continuation => self.into_continuation_fragment(),
_ => Err(Error::new(
ErrorKind::Transport,
"http2 header block requested for non-header frame",
)),
}
}
fn into_continuation_fragment(self) -> Result<HeaderBlockFragment> {
if self.frame_type != FrameType::Continuation {
return Err(Error::new(
ErrorKind::Transport,
"http2 continuation fragment requested for non-continuation frame",
));
}
Ok(HeaderBlockFragment {
fragment: self.payload,
end_headers: self.flags.contains(Flags::END_HEADERS),
end_stream: false,
})
}
fn into_push_promise_fragment(self) -> Result<PushPromiseFragment> {
if self.frame_type != FrameType::PushPromise {
return Err(Error::new(
ErrorKind::Transport,
"http2 push promise fragment requested for non-push-promise frame",
));
}
if self.stream_id == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 PUSH_PROMISE frame must use a non-zero stream",
));
}
let mut payload = self.payload;
if self.flags.contains(Flags::PADDED) {
payload = strip_padding(payload)?;
}
if payload.len() < 4 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 PUSH_PROMISE payload length",
));
}
let promised_stream_id =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) & 0x7FFF_FFFF;
if promised_stream_id == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 PUSH_PROMISE promised stream must be non-zero",
));
}
payload.drain(..4);
Ok(PushPromiseFragment {
promised_stream_id,
fragment: payload,
end_headers: self.flags.contains(Flags::END_HEADERS),
})
}
pub(super) fn settings(&self) -> Result<Vec<Setting>> {
if self.frame_type != FrameType::Settings {
return Err(Error::new(
ErrorKind::Transport,
"http2 settings requested for non-settings frame",
));
}
if self.stream_id != 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 SETTINGS frame must use stream 0",
));
}
if self.flags.contains(Flags::ACK) {
if !self.payload.is_empty() {
return Err(Error::new(
ErrorKind::Transport,
"http2 SETTINGS ack frame must have empty payload",
));
}
return Ok(Vec::new());
}
if self.payload.len() % 6 != 0 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 SETTINGS payload length",
));
}
let mut settings = Vec::with_capacity(self.payload.len() / 6);
for chunk in self.payload.chunks_exact(6) {
let identifier = u16::from_be_bytes([chunk[0], chunk[1]]);
let value = u32::from_be_bytes([chunk[2], chunk[3], chunk[4], chunk[5]]);
let setting = match identifier {
SETTING_HEADER_TABLE_SIZE => Setting::HeaderTableSize(value as usize),
SETTING_ENABLE_PUSH => {
if value > 1 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 ENABLE_PUSH value",
));
}
Setting::EnablePush(value == 1)
}
SETTING_MAX_CONCURRENT_STREAMS => Setting::MaxConcurrentStreams(value),
SETTING_INITIAL_WINDOW_SIZE => {
if value > MAX_FLOW_CONTROL_WINDOW as u32 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 INITIAL_WINDOW_SIZE value",
));
}
Setting::InitialWindowSize(value as i32)
}
SETTING_MAX_FRAME_SIZE => {
let value = value as usize;
if !(DEFAULT_MAX_FRAME_SIZE..=MAX_FRAME_SIZE_UPPER_BOUND).contains(&value) {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 MAX_FRAME_SIZE value",
));
}
Setting::MaxFrameSize(value)
}
SETTING_MAX_HEADER_LIST_SIZE => Setting::MaxHeaderListSize(value),
_ => Setting::Unknown(identifier, value),
};
settings.push(setting);
}
Ok(settings)
}
pub(super) fn validated_ping_payload(&self) -> Result<&[u8]> {
if self.frame_type != FrameType::Ping {
return Err(Error::new(
ErrorKind::Transport,
"http2 ping payload requested for non-ping frame",
));
}
if self.stream_id != 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 PING frame must use stream 0",
));
}
if self.payload.len() != 8 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 PING payload length",
));
}
Ok(&self.payload)
}
pub(super) fn rst_stream_error_code(&self) -> Result<u32> {
if self.frame_type != FrameType::RstStream {
return Err(Error::new(
ErrorKind::Transport,
"http2 RST_STREAM requested for non-rst frame",
));
}
if self.stream_id == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 RST_STREAM frame must use a non-zero stream",
));
}
if self.payload.len() != 4 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 RST_STREAM payload length",
));
}
Ok(u32::from_be_bytes([
self.payload[0],
self.payload[1],
self.payload[2],
self.payload[3],
]))
}
pub(super) fn goaway(&self) -> Result<GoAwayFrame> {
if self.frame_type != FrameType::GoAway {
return Err(Error::new(
ErrorKind::Transport,
"http2 GOAWAY requested for non-goaway frame",
));
}
if self.stream_id != 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 GOAWAY frame must use stream 0",
));
}
if self.payload.len() < 8 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 GOAWAY payload length",
));
}
let last_stream_id = u32::from_be_bytes([
self.payload[0],
self.payload[1],
self.payload[2],
self.payload[3],
]) & 0x7FFF_FFFF;
let error_code = u32::from_be_bytes([
self.payload[4],
self.payload[5],
self.payload[6],
self.payload[7],
]);
Ok(GoAwayFrame {
last_stream_id,
error_code,
})
}
pub(super) fn window_update_increment(&self) -> Result<u32> {
if self.frame_type != FrameType::WindowUpdate {
return Err(Error::new(
ErrorKind::Transport,
"http2 WINDOW_UPDATE requested for non-window-update frame",
));
}
if self.payload.len() != 4 {
return Err(Error::new(
ErrorKind::Transport,
"invalid http2 WINDOW_UPDATE payload length",
));
}
let increment = u32::from_be_bytes([
self.payload[0],
self.payload[1],
self.payload[2],
self.payload[3],
]) & 0x7FFF_FFFF;
if increment == 0 {
return Err(Error::new(
ErrorKind::Transport,
"http2 WINDOW_UPDATE increment must be non-zero",
));
}
Ok(increment)
}
}
#[cfg(test)]
mod tests {
use super::{
DEFAULT_HEADER_TABLE_SIZE, HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK, HeaderCodec,
build_request_header_list, client_settings_payload_with_fingerprint,
};
use crate::browser_emulation::Http2Fingerprint;
use crate::decode::CompressionMode;
use crate::header::HeaderMap;
use crate::request::Method;
use crate::url::Url;
fn build_test_request_headers(custom_name: &str, custom_value: &str) -> Vec<(String, String)> {
let mut headers = HeaderMap::new();
headers.insert(custom_name, custom_value).unwrap();
let url = Url::parse("https://example.com/path").unwrap();
build_request_header_list(
Method::Get,
&url,
&headers,
&[],
CompressionMode::Auto,
None,
None,
None,
)
.unwrap()
}
#[test]
fn header_codec_reuses_dynamic_table_with_small_peer_table() {
let request_headers = build_test_request_headers("x-custom", "value");
let mut codec = HeaderCodec::new();
let mut decoder = HeaderCodec::new();
let first = codec.encode_request(&request_headers, 64).unwrap();
let second = codec.encode_request(&request_headers, 64).unwrap();
assert_eq!(first[0] & 0xE0, HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK);
assert_eq!(decoder.decode_block(&first).unwrap(), request_headers);
assert_eq!(decoder.decode_block(&second).unwrap(), request_headers);
assert_ne!(first, second);
assert!(second.len() < first.len());
}
#[test]
fn header_codec_emits_dynamic_table_size_update_when_peer_size_changes() {
let request_headers = build_test_request_headers("x-custom", "value");
let mut codec = HeaderCodec::new();
let mut decoder = HeaderCodec::new();
let first = codec
.encode_request(&request_headers, DEFAULT_HEADER_TABLE_SIZE)
.unwrap();
let second = codec.encode_request(&request_headers, 64).unwrap();
let third = codec.encode_request(&request_headers, 64).unwrap();
assert_ne!(first[0] & 0xE0, HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK);
assert_eq!(second[0] & 0xE0, HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK);
assert_ne!(third[0] & 0xE0, HPACK_DYNAMIC_TABLE_SIZE_UPDATE_MASK);
assert_eq!(decoder.decode_block(&first).unwrap(), request_headers);
assert_eq!(decoder.decode_block(&second).unwrap(), request_headers);
assert_eq!(decoder.decode_block(&third).unwrap(), request_headers);
}
#[test]
fn header_codec_does_not_reuse_entry_larger_than_peer_table() {
let warmup_headers = build_test_request_headers("x-small", "ok");
let oversized_headers = build_test_request_headers(
"x-very-long-custom-header-name",
"abcdefghijklmnopqrstuvwxyz",
);
let mut codec = HeaderCodec::new();
let mut decoder = HeaderCodec::new();
let warmup = codec.encode_request(&warmup_headers, 64).unwrap();
assert_eq!(decoder.decode_block(&warmup).unwrap(), warmup_headers);
let first = codec.encode_request(&oversized_headers, 64).unwrap();
let second = codec.encode_request(&oversized_headers, 64).unwrap();
assert_eq!(decoder.decode_block(&first).unwrap(), oversized_headers);
assert_eq!(decoder.decode_block(&second).unwrap(), oversized_headers);
assert_eq!(first, second);
}
#[test]
fn header_order_applies_fingerprint() {
let mut headers = HeaderMap::new();
headers.insert("x-a", "1").unwrap();
headers.insert("x-b", "2").unwrap();
let url = Url::parse("https://example.com/path").unwrap();
let pseudo_order = vec![":path".to_owned()];
let order = vec![":path".to_owned(), "x-b".to_owned(), "x-a".to_owned()];
let request_headers = build_request_header_list(
Method::Get,
&url,
&headers,
&[],
CompressionMode::Auto,
None,
Some(pseudo_order.as_slice()),
Some(order.as_slice()),
)
.unwrap();
let names: Vec<_> = request_headers.iter().map(|(n, _)| n.as_str()).collect();
let pos = |name: &str| names.iter().position(|n| *n == name).unwrap();
assert!(pos(":path") < pos("x-b"));
assert!(pos("x-b") < pos("x-a"));
}
#[test]
fn header_order_keeps_pseudo_headers_first() {
let mut headers = HeaderMap::new();
headers.insert("x-a", "1").unwrap();
let url = Url::parse("https://example.com/path").unwrap();
let order = vec!["x-a".to_owned()];
let request_headers = build_request_header_list(
Method::Get,
&url,
&headers,
&[],
CompressionMode::Auto,
None,
None,
Some(order.as_slice()),
)
.unwrap();
let names: Vec<_> = request_headers.iter().map(|(n, _)| n.as_str()).collect();
let first_regular = names.iter().position(|n| !n.starts_with(':')).unwrap();
assert!(names[..first_regular].iter().all(|n| n.starts_with(':')));
}
#[test]
fn client_settings_payload_includes_fingerprint_settings() {
let fp = Http2Fingerprint {
settings_order: Vec::new(),
pseudo_header_order: Vec::new(),
regular_header_order: Vec::new(),
initial_window_size: Some(65_535),
initial_connection_window_size: None,
max_frame_size: Some(16_384),
header_table_size: Some(4_096),
priorities: Vec::new(),
};
let payload = client_settings_payload_with_fingerprint(Some(&fp)).unwrap();
assert_eq!(payload.len(), 24);
assert_eq!(&payload[0..6], &[0, 2, 0, 0, 0, 0]);
assert_eq!(&payload[6..12], &[0, 1, 0, 0, 16, 0]);
assert_eq!(&payload[12..18], &[0, 4, 0, 0, 255, 255]);
assert_eq!(&payload[18..24], &[0, 5, 0, 0, 64, 0]);
}
#[test]
fn client_settings_payload_rejects_invalid_max_frame_size() {
let fp = Http2Fingerprint {
settings_order: Vec::new(),
pseudo_header_order: Vec::new(),
regular_header_order: Vec::new(),
initial_window_size: None,
initial_connection_window_size: None,
max_frame_size: Some(1),
header_table_size: None,
priorities: Vec::new(),
};
let err = client_settings_payload_with_fingerprint(Some(&fp)).unwrap_err();
assert!(err.to_string().contains("MAX_FRAME_SIZE"));
}
#[test]
fn client_settings_payload_respects_settings_order() {
let fp = Http2Fingerprint {
settings_order: vec![
"INITIAL_WINDOW_SIZE".into(),
"HEADER_TABLE_SIZE".into(),
"ENABLE_PUSH".into(),
"MAX_FRAME_SIZE".into(),
],
pseudo_header_order: Vec::new(),
regular_header_order: Vec::new(),
header_table_size: Some(4_096),
initial_window_size: Some(65_535),
initial_connection_window_size: None,
max_frame_size: Some(16_384),
priorities: Vec::new(),
};
let payload = client_settings_payload_with_fingerprint(Some(&fp)).unwrap();
assert_eq!(&payload[0..6], &[0, 4, 0, 0, 255, 255]);
assert_eq!(&payload[6..12], &[0, 1, 0, 0, 16, 0]);
assert_eq!(&payload[12..18], &[0, 2, 0, 0, 0, 0]);
assert_eq!(&payload[18..24], &[0, 5, 0, 0, 64, 0]);
}
}