diff --git a/docs/cpp.md b/docs/cpp.md index 776e11d..8db91e7 100644 --- a/docs/cpp.md +++ b/docs/cpp.md @@ -110,7 +110,11 @@ A struct representing a parser. It has the following fields: - `skip_body` (`bool`): If the parser should skip the body. - `debug` (`bool`): If debug tracing is enabled for this parser. It only affects tracing in debug-enabled builds. - `max_start_line_length` (`uintptr_t`): Maximum allowed request/status line length. By default is `8192`. -- `max_header_length` (`uintptr_t`): Maximum allowed header length. By default is `8192`. +- `max_header_length` (`uintptr_t`): Maximum allowed individual header or trailer line length. By default is `8192`. +- `max_header_bytes` (`uintptr_t`): Maximum aggregate initial-header field bytes per message. By default is `65536`; `0` means unlimited. +- `max_header_count` (`uintptr_t`): Maximum initial-header field count per message. By default is `100`; `0` means unlimited. +- `max_trailer_bytes` (`uintptr_t`): Maximum aggregate trailer field bytes per message. By default is `65536`; `0` means unlimited. +- `max_trailer_count` (`uintptr_t`): Maximum trailer field count per message. By default is `100`; `0` means unlimited. - `max_body_payload` (`uint64_t`): Maximum body payload bytes consumed in a single `milo_parse()` call. `0` means unlimited and is the default. - `context` (`void*`): The context of this parser. Use is reserved to the developer. - `state` (`uint8_t`): The current parser state. @@ -130,6 +134,8 @@ A struct representing a parser. It has the following fields: - `has_connection_upgrade` (`bool`): If the current message has a `Connection: upgrade` token. - `has_upgrade` (`bool`): If the current message has an `Upgrade` header. - `has_trailers` (`bool`): If the current message has a `Trailer` header. +- `header_bytes` / `header_count` (`uintptr_t`): Committed initial-header field bytes and count for the current message. +- `trailer_bytes` / `trailer_count` (`uintptr_t`): Committed trailer field bytes and count for the current message. - `active_callbacks` (`uint64_t`): Active callback bitmask. Set to one or more `CALLBACK_ACTIVE_*` values. - `active_events` (`uint64_t`): Active event bitmask. Set to one or more `EVENT_ACTIVE_*` values. - `callbacks` (`ParserCallbacks`): The callbacks for the current parser. @@ -151,6 +157,10 @@ All the fields **MUST** be considered readonly, with the following exceptions: - `debug` - `max_start_line_length` - `max_header_length` +- `max_header_bytes` +- `max_header_count` +- `max_trailer_bytes` +- `max_trailer_count` - `max_body_payload` - `context` - `active_callbacks` @@ -167,6 +177,12 @@ The event buffer is terminated by `EVENT_END`. Do not rely on the internal buffe If an active event would exceed the internal event buffer, parsing stops before consuming the data that would have produced the event. This is not a parser error and does not pause the parser. Call `milo_parse()` again after draining the event buffer. +## Header and Trailer Budgets + +Initial headers and trailers have independent aggregate byte and field-count limits. Defaults are 65,536 bytes and 100 fields for each section; set an individual limit to `0` to disable it. Native callers may write the public configuration fields or use `milo_set_max_header_bytes()`, `milo_set_max_header_count()`, `milo_set_max_trailer_bytes()`, and `milo_set_max_trailer_count()`. + +The byte total includes every committed field line, including its terminating CRLF, but excludes the final empty CRLF. Complete field lines are checked against the budget before field syntax and semantics are processed, so an over-budget malformed field reports the budget error. Totals commit only after validation and event delivery; fragmented input and a retry after event-buffer suspension do not count a side-effect-free field more than once. Exceeding a limit fails parsing with `ERROR_UNEXPECTED_CHARACTER`. + ## Body Payload Limit `max_body_payload` limits how many body payload bytes a single `milo_parse()` invocation can consume. The default value is `0`, which means unlimited. diff --git a/docs/js.md b/docs/js.md index d43df97..c7961c1 100644 --- a/docs/js.md +++ b/docs/js.md @@ -125,6 +125,12 @@ Read the event buffer pointer from `parser + ParserFields.EVENTS`, then drain re If an active event would exceed the internal event buffer, parsing stops before consuming the data that would have produced the event. This is not a parser error and does not pause the parser. Call `parse()` again after draining the event buffer. +### Header and Trailer Budgets + +`setMaxHeaderBytes`, `setMaxHeaderCount`, `setMaxTrailerBytes`, and `setMaxTrailerCount` configure independent aggregate initial-header and trailer limits. Defaults are 65,536 bytes and 100 fields for each section; set an individual limit to `0` to disable it. + +The byte total includes every committed field line, including its terminating CRLF, but excludes the final empty CRLF. Complete field lines are checked against the budget before field syntax and semantics are processed, so an over-budget malformed field reports the budget error. Totals commit only after validation and event delivery; fragmented input and a retry after event-buffer suspension do not count a side-effect-free field more than once. Exceeding a limit fails parsing with `ERROR_UNEXPECTED_CHARACTER`. + ### Body Payload Limit `setMaxBodyPayload(parser, value)` limits how many body payload bytes a single `parse()` invocation can consume. The default value is `0`, which means unlimited. @@ -385,10 +391,18 @@ Default is `8192`. #### `getMaxHeaderLength(parser)` -Returns the parser maximum allowed header length. +Returns the parser maximum allowed individual header or trailer line length. Default is `8192`. +#### `getMaxHeaderBytes(parser)` / `getMaxHeaderCount(parser)` + +Return the aggregate initial-header byte or field-count limit. Defaults are `65536` and `100`. + +#### `getMaxTrailerBytes(parser)` / `getMaxTrailerCount(parser)` + +Return the aggregate trailer byte or field-count limit. Defaults are `65536` and `100`. + #### `getMaxBodyPayload(parser)` Returns the maximum body payload bytes consumed by a single `parse()` invocation. @@ -499,10 +513,18 @@ Defaults to `8192` in a new parser. #### `setMaxHeaderLength(parser, value)` -Sets the parser maximum allowed header length. +Sets the parser maximum allowed individual header or trailer line length. Defaults to `8192` in a new parser. +#### `setMaxHeaderBytes(parser, value)` / `setMaxHeaderCount(parser, value)` + +Set the aggregate initial-header byte or field-count limit. Use `0` for unlimited. + +#### `setMaxTrailerBytes(parser, value)` / `setMaxTrailerCount(parser, value)` + +Set the aggregate trailer byte or field-count limit. Use `0` for unlimited. + #### `setMaxBodyPayload(parser, value)` Sets the maximum body payload bytes consumed by a single `parse()` invocation. Use `0` for unlimited. diff --git a/docs/rust.md b/docs/rust.md index 1ad19ae..4dd51d2 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -111,7 +111,11 @@ A struct representing a parser. It has the following fields: - `skip_body` (`bool`): If the parser should skip the body. - `debug` (`bool`): If debug tracing is enabled for this parser. It only affects tracing in debug-enabled builds. - `max_start_line_length` (`usize`): Maximum allowed request/status line length. By default is `8192`. -- `max_header_length` (`usize`): Maximum allowed header length. By default is `8192`. +- `max_header_length` (`usize`): Maximum allowed individual header or trailer line length. By default is `8192`. +- `max_header_bytes` (`usize`): Maximum aggregate initial-header field bytes per message. By default is `65536`; `0` means unlimited. +- `max_header_count` (`usize`): Maximum initial-header field count per message. By default is `100`; `0` means unlimited. +- `max_trailer_bytes` (`usize`): Maximum aggregate trailer field bytes per message. By default is `65536`; `0` means unlimited. +- `max_trailer_count` (`usize`): Maximum trailer field count per message. By default is `100`; `0` means unlimited. - `max_body_payload` (`u64`): Maximum body payload bytes consumed in a single `parse()` call. `0` means unlimited and is the default. - `context` (`*mut c_void`): The context of this parser. Use is reserved to the developer. - `state` (`u8`): The current parser state. @@ -131,6 +135,8 @@ A struct representing a parser. It has the following fields: - `has_connection_upgrade` (`bool`): If the current message has a `Connection: upgrade` token. - `has_upgrade` (`bool`): If the current message has an `Upgrade` header. - `has_trailers` (`bool`): If the current message has a `Trailer` header. +- `header_bytes` / `header_count` (`usize`): Committed initial-header field bytes and count for the current message. +- `trailer_bytes` / `trailer_count` (`usize`): Committed trailer field bytes and count for the current message. - `active_callbacks` (`u64`): Active callback bitmask. Set to one or more `CALLBACK_ACTIVE_*` flags. - `active_events` (`u64`): Active event bitmask. Set to one or more `EVENT_ACTIVE_*` flags. - `callbacks` (`ParserCallbacks`): The callbacks for the current parser. @@ -152,6 +158,10 @@ All the fields **MUST** be considered readonly, with the following exceptions: - `debug` - `max_start_line_length` - `max_header_length` +- `max_header_bytes` +- `max_header_count` +- `max_trailer_bytes` +- `max_trailer_count` - `max_body_payload` - `context` - `active_callbacks` @@ -168,6 +178,12 @@ The event buffer is terminated by `EVENT_END`. Do not rely on the internal buffe If an active event would exceed the internal event buffer, parsing stops before consuming the data that would have produced the event. This is not a parser error and does not pause the parser. Call `parse()` again after draining the event buffer. +## Header and Trailer Budgets + +Initial headers and trailers have independent aggregate byte and field-count limits. Defaults are 65,536 bytes and 100 fields for each section; set an individual limit to `0` to disable it. + +The byte total includes every committed field line, including its terminating CRLF, but excludes the final empty CRLF. Complete field lines are checked against the budget before field syntax and semantics are processed, so an over-budget malformed field reports the budget error. Totals commit only after validation and event delivery; fragmented input and a retry after event-buffer suspension do not count a side-effect-free field more than once. Exceeding a limit fails parsing with `ERROR_UNEXPECTED_CHARACTER`. + ## Body Payload Limit `max_body_payload` limits how many body payload bytes a single `parse()` invocation can consume. The default value is `0`, which means unlimited. diff --git a/macros/src/parser_fields.rs b/macros/src/parser_fields.rs index c547583..b18e468 100644 --- a/macros/src/parser_fields.rs +++ b/macros/src/parser_fields.rs @@ -12,6 +12,10 @@ type WasmUsize = u32; struct ParserStub { max_start_line_length: WasmUsize, max_header_length: WasmUsize, + max_header_bytes: WasmUsize, + max_header_count: WasmUsize, + max_trailer_bytes: WasmUsize, + max_trailer_count: WasmUsize, max_body_payload: u64, autodetect: bool, is_request: bool, @@ -39,6 +43,10 @@ struct ParserStub { has_connection_upgrade: bool, has_upgrade: bool, has_trailers: bool, + header_bytes: WasmUsize, + header_count: WasmUsize, + trailer_bytes: WasmUsize, + trailer_count: WasmUsize, active_callbacks: u64, active_events: u64, ptr: WasmPointer, @@ -52,6 +60,10 @@ struct ParserStub { pub const FIELDS: &[(&str, usize)] = &[ ("MAX_START_LINE_LENGTH", offset_of!(ParserStub, max_start_line_length)), ("MAX_HEADER_LENGTH", offset_of!(ParserStub, max_header_length)), + ("MAX_HEADER_BYTES", offset_of!(ParserStub, max_header_bytes)), + ("MAX_HEADER_COUNT", offset_of!(ParserStub, max_header_count)), + ("MAX_TRAILER_BYTES", offset_of!(ParserStub, max_trailer_bytes)), + ("MAX_TRAILER_COUNT", offset_of!(ParserStub, max_trailer_count)), ("MAX_BODY_PAYLOAD", offset_of!(ParserStub, max_body_payload)), ("AUTODETECT", offset_of!(ParserStub, autodetect)), ("IS_REQUEST", offset_of!(ParserStub, is_request)), @@ -85,6 +97,10 @@ pub const FIELDS: &[(&str, usize)] = &[ ("HAS_CONNECTION_UPGRADE", offset_of!(ParserStub, has_connection_upgrade)), ("HAS_UPGRADE", offset_of!(ParserStub, has_upgrade)), ("HAS_TRAILERS", offset_of!(ParserStub, has_trailers)), + ("HEADER_BYTES", offset_of!(ParserStub, header_bytes)), + ("HEADER_COUNT", offset_of!(ParserStub, header_count)), + ("TRAILER_BYTES", offset_of!(ParserStub, trailer_bytes)), + ("TRAILER_COUNT", offset_of!(ParserStub, trailer_count)), ("ACTIVE_CALLBACKS", offset_of!(ParserStub, active_callbacks)), ("ACTIVE_EVENTS", offset_of!(ParserStub, active_events)), ("PTR", offset_of!(ParserStub, ptr)), diff --git a/parser/src/lib.rs b/parser/src/lib.rs index f5f0b50..f5d9b32 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -22,6 +22,10 @@ pub struct Parser { pub context: *mut c_void, pub max_start_line_length: usize, pub max_header_length: usize, + pub max_header_bytes: usize, + pub max_header_count: usize, + pub max_trailer_bytes: usize, + pub max_trailer_count: usize, pub max_body_payload: u64, pub autodetect: bool, pub is_request: bool, @@ -53,6 +57,10 @@ pub struct Parser { pub has_connection_upgrade: bool, pub has_upgrade: bool, pub has_trailers: bool, + pub header_bytes: usize, + pub header_count: usize, + pub trailer_bytes: usize, + pub trailer_count: usize, // Callback handling pub active_callbacks: u64, @@ -101,6 +109,10 @@ impl Parser { context: ptr::null_mut(), max_start_line_length: 8192, max_header_length: 8192, + max_header_bytes: 65536, + max_header_count: 100, + max_trailer_bytes: 65536, + max_trailer_count: 100, max_body_payload: 0, autodetect: true, is_request: false, @@ -130,6 +142,10 @@ impl Parser { has_connection_upgrade: false, has_upgrade: false, has_trailers: false, + header_bytes: 0, + header_count: 0, + trailer_bytes: 0, + trailer_count: 0, // Callbacks handling active_callbacks: 0, active_events: 0, @@ -158,6 +174,13 @@ impl Parser { /// * suspend_after_headers /// * manage_unconsumed /// * continue_without_data + /// * max_start_line_length + /// * max_header_length + /// * max_header_bytes + /// * max_header_count + /// * max_trailer_bytes + /// * max_trailer_count + /// * max_body_payload /// * debug /// * context pub fn reset(&mut self, keep_parsed: bool) { @@ -209,6 +232,10 @@ impl Parser { self.has_connection_upgrade = false; self.has_upgrade = false; self.has_trailers = false; + self.header_bytes = 0; + self.header_count = 0; + self.trailer_bytes = 0; + self.trailer_count = 0; self.content_length = 0; self.chunk_size = 0; self.remaining_content_length = 0; diff --git a/parser/src/native.rs b/parser/src/native.rs index 071802e..5c4903e 100644 --- a/parser/src/native.rs +++ b/parser/src/native.rs @@ -89,6 +89,38 @@ pub extern "C" fn milo_set_active_events(parser: *mut Parser, value: u64) { } } +/// Sets the maximum aggregate initial-header bytes per message. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_max_header_bytes(parser: *mut Parser, value: usize) { + unsafe { + (*parser).max_header_bytes = value; + } +} + +/// Sets the maximum initial-header field count per message. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_max_header_count(parser: *mut Parser, value: usize) { + unsafe { + (*parser).max_header_count = value; + } +} + +/// Sets the maximum aggregate trailer bytes per message. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_max_trailer_bytes(parser: *mut Parser, value: usize) { + unsafe { + (*parser).max_trailer_bytes = value; + } +} + +/// Sets the maximum trailer field count per message. +#[unsafe(no_mangle)] +pub extern "C" fn milo_set_max_trailer_count(parser: *mut Parser, value: usize) { + unsafe { + (*parser).max_trailer_count = value; + } +} + /// Sets the maximum body payload consumed by a single parse invocation. #[unsafe(no_mangle)] pub extern "C" fn milo_set_max_body_payload(parser: *mut Parser, value: u64) { @@ -114,6 +146,12 @@ pub extern "C" fn milo_set_suspend_after_headers(parser: *mut Parser, value: boo /// * autodetect /// * is_request /// * suspend_after_headers +/// * max_start_line_length +/// * max_header_length +/// * max_header_bytes +/// * max_header_count +/// * max_trailer_bytes +/// * max_trailer_count /// * max_body_payload /// * manage_unconsumed /// * continue_without_data diff --git a/parser/src/parse.rs b/parser/src/parse.rs index 80105a0..501fcc7 100644 --- a/parser/src/parse.rs +++ b/parser/src/parse.rs @@ -485,6 +485,19 @@ impl Parser { } } + let next_header_count = match self.header_count.checked_add(1) { + Some(count) if self.max_header_count == 0 || count <= self.max_header_count => count, + _ => { + fail!(UNEXPECTED_CHARACTER, "Too many header fields"); + } + }; + let next_header_bytes = match self.header_bytes.checked_add(cr + 2) { + Some(bytes) if self.max_header_bytes == 0 || bytes <= self.max_header_bytes => bytes, + _ => { + fail!(UNEXPECTED_CHARACTER, "Header section too large"); + } + }; + // RFC 9112 section.4 // RFC 9110 section 5.5 and 5.6 let header_name_start = 0; @@ -796,6 +809,8 @@ impl Parser { ); } + self.header_count = next_header_count; + self.header_bytes = next_header_bytes; advance!(cr + 2); } HeaderLineScanResult::Invalid(invalid) => { @@ -1324,6 +1339,19 @@ impl Parser { next!(); } + let next_trailer_count = match self.trailer_count.checked_add(1) { + Some(count) if self.max_trailer_count == 0 || count <= self.max_trailer_count => count, + _ => { + fail!(UNEXPECTED_CHARACTER, "Too many trailer fields"); + } + }; + let next_trailer_bytes = match self.trailer_bytes.checked_add(cr + 2) { + Some(bytes) if self.max_trailer_bytes == 0 || bytes <= self.max_trailer_bytes => bytes, + _ => { + fail!(UNEXPECTED_CHARACTER, "Trailer section too large"); + } + }; + let trailer_name_start = 0; let trailer_name_end = match find_char(data, trailer_name_start, cr, b':') { Some(index) if index > trailer_name_start => index, @@ -1358,6 +1386,9 @@ impl Parser { trailer_value_end - trailer_value_start ); } + + self.trailer_count = next_trailer_count; + self.trailer_bytes = next_trailer_bytes; advance!(cr + 2); } HeaderLineScanResult::Invalid(invalid) => { diff --git a/parser/src/wasm.rs b/parser/src/wasm.rs index 73d380c..68aa195 100644 --- a/parser/src/wasm.rs +++ b/parser/src/wasm.rs @@ -161,6 +161,26 @@ pub fn get_max_header_length(parser: *const c_void) -> usize { unsafe { (*(parser as *const Parser)).max_header_length } } +// Get the parser max_header_bytes property. +#[unsafe(no_mangle)] +pub fn get_max_header_bytes(parser: *const c_void) -> usize { unsafe { (*(parser as *const Parser)).max_header_bytes } } + +// Get the parser max_header_count property. +#[unsafe(no_mangle)] +pub fn get_max_header_count(parser: *const c_void) -> usize { unsafe { (*(parser as *const Parser)).max_header_count } } + +// Get the parser max_trailer_bytes property. +#[unsafe(no_mangle)] +pub fn get_max_trailer_bytes(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).max_trailer_bytes } +} + +// Get the parser max_trailer_count property. +#[unsafe(no_mangle)] +pub fn get_max_trailer_count(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).max_trailer_count } +} + // Get the parser max_body_payload property. #[unsafe(no_mangle)] pub fn get_max_body_payload(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).max_body_payload } } @@ -313,6 +333,34 @@ pub fn set_max_header_length(parser: *mut c_void, value: usize) { } } +#[unsafe(no_mangle)] +pub fn set_max_header_bytes(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_header_bytes = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_header_count(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_header_count = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_trailer_bytes(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_trailer_bytes = value; + } +} + +#[unsafe(no_mangle)] +pub fn set_max_trailer_count(parser: *mut c_void, value: usize) { + unsafe { + (*(parser as *mut Parser)).max_trailer_count = value; + } +} + #[unsafe(no_mangle)] pub fn set_max_body_payload(parser: *mut c_void, value: u64) { unsafe { diff --git a/parser/tests/basic.rs b/parser/tests/basic.rs index 278d194..596659e 100644 --- a/parser/tests/basic.rs +++ b/parser/tests/basic.rs @@ -5,7 +5,7 @@ use std::ffi::c_uchar; use milo_parser::{ CALLBACK_ACTIVE_ON_HEADERS, ERROR_NONE, ERROR_UNEXPECTED_CHARACTER, ERROR_UNEXPECTED_STATE, - EVENT_ACTIVE_ON_HEADER_NAME, EVENT_ACTIVE_ON_HEADER_VALUE, STATE_BODY_DECISION, STATE_ERROR, STATE_FINISH, + EVENT_ACTIVE_ON_HEADER_NAME, EVENT_ACTIVE_ON_HEADER_VALUE, Parser, STATE_BODY_DECISION, STATE_ERROR, STATE_FINISH, STATE_HEADER, STATE_START, }; @@ -240,6 +240,115 @@ fn basic_connection_close() { assert_eq!(parser.state, STATE_FINISH); } +#[test] +fn basic_header_and_trailer_budget_defaults() { + let parser = Parser::new(); + + assert_eq!(parser.max_header_bytes, 65536); + assert_eq!(parser.max_header_count, 100); + assert_eq!(parser.max_trailer_bytes, 65536); + assert_eq!(parser.max_trailer_count, 100); +} + +#[test] +fn basic_initial_header_budgets() { + let message = "GET / HTTP/1.1\r\nA: b\r\nB: c\r\n\r\n"; + + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.max_header_bytes = 12; + parser.max_header_count = 2; + assert_eq!(parser.parse(message.as_ptr(), message.len()), message.len()); + assert_eq!(parser.header_bytes, 12); + assert_eq!(parser.header_count, 2); + assert_ne!(parser.state, STATE_ERROR); + + for (max_bytes, max_count, description) in [(11, 2, "Header section too large"), (12, 1, "Too many header fields")] { + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.max_header_bytes = max_bytes; + parser.max_header_count = max_count; + parser.parse(message.as_ptr(), message.len()); + assert_eq!(parser.state, STATE_ERROR); + assert_eq!(parser.header_bytes, 6); + assert_eq!(parser.header_count, 1); + assert_eq!(parser.error_description_str(), description); + } + + let unlimited = format!("GET / HTTP/1.1\r\n{}\r\n", "X: y\r\n".repeat(101)); + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.max_header_bytes = 0; + parser.max_header_count = 0; + assert_eq!(parser.parse(unlimited.as_ptr(), unlimited.len()), unlimited.len()); + assert_eq!(parser.header_count, 101); + assert_ne!(parser.state, STATE_ERROR); +} + +#[test] +fn basic_trailer_budgets() { + let message = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nA: b\r\nB: c\r\n\r\n"; + + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.max_trailer_bytes = 12; + parser.max_trailer_count = 2; + assert_eq!(parser.parse(message.as_ptr(), message.len()), message.len()); + assert_eq!(parser.trailer_bytes, 12); + assert_eq!(parser.trailer_count, 2); + assert_ne!(parser.state, STATE_ERROR); + + for (max_bytes, max_count, description) in [(11, 2, "Trailer section too large"), (12, 1, "Too many trailer fields")] + { + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.max_trailer_bytes = max_bytes; + parser.max_trailer_count = max_count; + parser.parse(message.as_ptr(), message.len()); + assert_eq!(parser.state, STATE_ERROR); + assert_eq!(parser.trailer_bytes, 6); + assert_eq!(parser.trailer_count, 1); + assert_eq!(parser.error_description_str(), description); + } +} + +#[test] +fn basic_header_budget_survives_managed_fragments_and_event_suspension() { + const FIELD_COUNT: usize = 4000; + let fields = "X: y\r\n".repeat(FIELD_COUNT); + let message = format!("GET / HTTP/1.1\r\n{fields}\r\n"); + let expected_bytes = fields.len(); + + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.manage_unconsumed = true; + parser.max_header_bytes = expected_bytes; + parser.max_header_count = FIELD_COUNT; + parser.active_events = EVENT_ACTIVE_ON_HEADER_NAME | EVENT_ACTIVE_ON_HEADER_VALUE; + + let split = 20; + assert_eq!(parser.parse(message.as_ptr(), split), 16); + assert_eq!(parser.header_count, 0); + + let first = parser.parse(unsafe { message.as_ptr().add(split) }, message.len() - split); + assert!(first < message.len() - 16); + assert!(parser.header_count > 0); + assert!(parser.header_count < FIELD_COUNT); + + let consumed = parser.parse([].as_ptr(), 0); + assert!(consumed > 0); + assert_eq!(parser.state, STATE_START); + assert_eq!(parser.header_bytes, expected_bytes); + assert_eq!(parser.header_count, FIELD_COUNT); + assert_eq!(parser.error_code, ERROR_NONE); +} + #[test] fn basic_max_body_payload_content_length() { let mut parser = create_parser(); @@ -401,6 +510,8 @@ fn basic_event_buffer_full_stops_parsing() { let mut parser = milo_parser::Parser::new(); parser.autodetect = false; parser.is_request = false; + parser.max_header_bytes = 0; + parser.max_header_count = 0; parser.active_events = EVENT_ACTIVE_ON_HEADER_NAME | EVENT_ACTIVE_ON_HEADER_VALUE; let mut message = String::from("HTTP/1.1 200 OK\r\n"); diff --git a/parser/tests/issue.rs b/parser/tests/issue.rs index 7df3624..830c915 100644 --- a/parser/tests/issue.rs +++ b/parser/tests/issue.rs @@ -27,6 +27,10 @@ fn issue_26__preserve_events_before_error() { }; let mut parser = Parser::new(); parser.autodetect = false; + // This regression intentionally creates thousands of fields to fill the event + // buffer. + parser.max_header_bytes = 0; + parser.max_header_count = 0; parser.active_events = EVENT_ACTIVE_ON_HEADERS | EVENT_ACTIVE_ON_MESSAGE_COMPLETE | EVENT_ACTIVE_ON_HEADER_VALUE; if errors { diff --git a/parser/wasm/test/basic.test.js b/parser/wasm/test/basic.test.js index 716ccd2..fc28e21 100644 --- a/parser/wasm/test/basic.test.js +++ b/parser/wasm/test/basic.test.js @@ -150,6 +150,132 @@ it('basic_connection_close', t => { assert.equal(milo.getState(parser), milo.STATE_FINISH) }) +it('basic_header_and_trailer_budget_defaults', t => { + const { milo, parser } = createParser(t, setup) + + assert.equal(milo.getMaxHeaderBytes(parser), 65536) + assert.equal(milo.getMaxHeaderCount(parser), 100) + assert.equal(milo.getMaxTrailerBytes(parser), 65536) + assert.equal(milo.getMaxTrailerCount(parser), 100) +}) + +it('basic_initial_header_budgets', t => { + const message = 'GET / HTTP/1.1\r\nA: b\r\nB: c\r\n\r\n' + const current = (milo, parser, field) => new DataView(milo.memory.buffer).getUint32(parser + field, true) + + { + const { milo, parser, parse } = createParser(t, setup) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setMaxHeaderBytes(parser, 12) + milo.setMaxHeaderCount(parser, 2) + assert.equal(parse(message), message.length) + assert.equal(current(milo, parser, milo.PARSER_FIELD_HEADER_BYTES), 12) + assert.equal(current(milo, parser, milo.PARSER_FIELD_HEADER_COUNT), 2) + assert.notEqual(milo.getState(parser), milo.STATE_ERROR) + } + + for (const [maxBytes, maxCount, description] of [ + [11, 2, 'Header section too large'], + [12, 1, 'Too many header fields'] + ]) { + const { milo, parser, parse } = createParser(t, setup) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setMaxHeaderBytes(parser, maxBytes) + milo.setMaxHeaderCount(parser, maxCount) + parse(message) + assert.equal(milo.getState(parser), milo.STATE_ERROR) + assert.equal(current(milo, parser, milo.PARSER_FIELD_HEADER_BYTES), 6) + assert.equal(current(milo, parser, milo.PARSER_FIELD_HEADER_COUNT), 1) + assert.equal(milo.getErrorDescription(parser), description) + } + + { + const unlimited = `GET / HTTP/1.1\r\n${'X: y\r\n'.repeat(101)}\r\n` + const { milo, parser, parse } = createParser(t, setup) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setMaxHeaderBytes(parser, 0) + milo.setMaxHeaderCount(parser, 0) + assert.equal(parse(unlimited), unlimited.length) + assert.equal(current(milo, parser, milo.PARSER_FIELD_HEADER_COUNT), 101) + assert.notEqual(milo.getState(parser), milo.STATE_ERROR) + } +}) + +it('basic_trailer_budgets', t => { + const message = 'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nA: b\r\nB: c\r\n\r\n' + const current = (milo, parser, field) => new DataView(milo.memory.buffer).getUint32(parser + field, true) + + { + const { milo, parser, parse } = createParser(t, setup) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setMaxTrailerBytes(parser, 12) + milo.setMaxTrailerCount(parser, 2) + assert.equal(parse(message), message.length) + assert.equal(current(milo, parser, milo.PARSER_FIELD_TRAILER_BYTES), 12) + assert.equal(current(milo, parser, milo.PARSER_FIELD_TRAILER_COUNT), 2) + assert.notEqual(milo.getState(parser), milo.STATE_ERROR) + } + + for (const [maxBytes, maxCount, description] of [ + [11, 2, 'Trailer section too large'], + [12, 1, 'Too many trailer fields'] + ]) { + const { milo, parser, parse } = createParser(t, setup) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setMaxTrailerBytes(parser, maxBytes) + milo.setMaxTrailerCount(parser, maxCount) + parse(message) + assert.equal(milo.getState(parser), milo.STATE_ERROR) + assert.equal(current(milo, parser, milo.PARSER_FIELD_TRAILER_BYTES), 6) + assert.equal(current(milo, parser, milo.PARSER_FIELD_TRAILER_COUNT), 1) + assert.equal(milo.getErrorDescription(parser), description) + } +}) + +it('basic_header_budget_survives_managed_fragments_and_event_suspension', t => { + const fieldCount = 4000 + const fields = 'X: y\r\n'.repeat(fieldCount) + const message = `GET / HTTP/1.1\r\n${fields}\r\n` + const milo = setup() + const parser = milo.create() + t.after(() => milo.destroy(parser)) + + function parse (input) { + const bytes = Buffer.from(input) + const ptr = milo.alloc(bytes.length) + new Uint8Array(milo.memory.buffer, ptr, bytes.length).set(bytes) + return milo.parse(parser, ptr, bytes.length) + } + + const current = field => new DataView(milo.memory.buffer).getUint32(parser + field, true) + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setShouldManageUnconsumed(parser, true) + milo.setMaxHeaderBytes(parser, fields.length) + milo.setMaxHeaderCount(parser, fieldCount) + milo.setActiveEvents(parser, milo.EVENT_ACTIVE_ON_HEADER_NAME | milo.EVENT_ACTIVE_ON_HEADER_VALUE) + + const split = 20 + assert.equal(parse(message.slice(0, split)), 16) + assert.equal(current(milo.PARSER_FIELD_HEADER_COUNT), 0) + + const first = parse(message.slice(split)) + assert.ok(first < message.length - 16) + assert.ok(current(milo.PARSER_FIELD_HEADER_COUNT) > 0) + assert.ok(current(milo.PARSER_FIELD_HEADER_COUNT) < fieldCount) + + assert.ok(parse('') > 0) + assert.equal(milo.getState(parser), milo.STATE_START) + assert.equal(current(milo.PARSER_FIELD_HEADER_BYTES), fields.length) + assert.equal(current(milo.PARSER_FIELD_HEADER_COUNT), fieldCount) + assert.equal(milo.getErrorCode(parser), milo.ERROR_NONE) +}) + it('basic_max_body_payload_content_length', t => { const { milo, parser, parse } = createParser(t, setup) milo.setShouldAutodetect(parser, false) @@ -266,6 +392,8 @@ it('basic_event_buffer_full_stops_parsing', t => { const { milo, parser, parse } = createParser(t, setup) milo.setShouldAutodetect(parser, false) milo.setIsRequest(parser, false) + milo.setMaxHeaderBytes(parser, 0) + milo.setMaxHeaderCount(parser, 0) milo.setActiveCallbacks(parser, 0n) milo.setActiveEvents(parser, milo.EVENT_ACTIVE_ON_HEADER_NAME | milo.EVENT_ACTIVE_ON_HEADER_VALUE) let message = 'HTTP/1.1 200 OK\r\n' @@ -310,6 +438,8 @@ it('basic_event_buffer_full_resumes_completion', () => { ) const ptr = milo.alloc(message.length) try { + milo.setMaxHeaderBytes(parser, 0) + milo.setMaxHeaderCount(parser, 0) milo.setActiveCallbacks( parser, milo.CALLBACK_ACTIVE_ON_HEADER_VALUE | diff --git a/parser/wasm/test/issues.test.js b/parser/wasm/test/issues.test.js index 3ee83c1..b835301 100644 --- a/parser/wasm/test/issues.test.js +++ b/parser/wasm/test/issues.test.js @@ -89,6 +89,9 @@ it('issue-26 - preserve_events_before_error', () => { try { milo.setShouldAutodetect(parser, false) milo.setIsRequest(parser, false) + // This regression intentionally creates thousands of fields to fill the event buffer. + milo.setMaxHeaderBytes(parser, 0) + milo.setMaxHeaderCount(parser, 0) const mask = milo.EVENT_ACTIVE_ON_HEADERS | milo.EVENT_ACTIVE_ON_MESSAGE_COMPLETE | diff --git a/scripts/src/bin/postbuild-wasm.rs b/scripts/src/bin/postbuild-wasm.rs index 33a2482..191112b 100644 --- a/scripts/src/bin/postbuild-wasm.rs +++ b/scripts/src/bin/postbuild-wasm.rs @@ -34,6 +34,10 @@ const GETTERS: &[(&str, GetterType, &str)] = &[ ), ("getMaxStartLineLength", GetterType::Number, "get_max_start_line_length"), ("getMaxHeaderLength", GetterType::Number, "get_max_header_length"), + ("getMaxHeaderBytes", GetterType::Number, "get_max_header_bytes"), + ("getMaxHeaderCount", GetterType::Number, "get_max_header_count"), + ("getMaxTrailerBytes", GetterType::Number, "get_max_trailer_bytes"), + ("getMaxTrailerCount", GetterType::Number, "get_max_trailer_count"), ("getMaxBodyPayload", GetterType::BigInt, "get_max_body_payload"), ( "shouldContinueWithoutData", @@ -81,6 +85,10 @@ const SETTERS: &[(&str, &str)] = &[ ("setShouldSuspendAfterHeaders", "set_should_suspend_after_headers"), ("setMaxStartLineLength", "set_max_start_line_length"), ("setMaxHeaderLength", "set_max_header_length"), + ("setMaxHeaderBytes", "set_max_header_bytes"), + ("setMaxHeaderCount", "set_max_header_count"), + ("setMaxTrailerBytes", "set_max_trailer_bytes"), + ("setMaxTrailerCount", "set_max_trailer_count"), ("setMaxBodyPayload", "set_max_body_payload"), ("setShouldSkipBody", "set_should_skip_body"), ("setActiveCallbacks", "set_active_callbacks"),