From 9b676ec888a98216275aaefdfda39bd23a6b3fa4 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Mon, 11 Sep 2023 10:05:36 +0200 Subject: [PATCH 1/2] feat: Do not allow Transfer-Encoding with Content-Length. --- src/llhttp/http.ts | 77 +++++++++++++++---------------- test/request/content-length.md | 4 +- test/request/transfer-encoding.md | 41 ++++++++++++++-- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 1c850676..116cf0ab 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -521,8 +521,40 @@ export class HTTP { .select(SPECIAL_HEADERS, this.store('header_state', 'header_field_colon')) .otherwise(this.resetHeaderState('header_field_general')); + /* https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3, paragraph 3. + * + * If a message is received with both a Transfer-Encoding and a + * Content-Length header field, the Transfer-Encoding overrides the + * Content-Length. Such a message might indicate an attempt to + * perform request smuggling (Section 9.5) or response splitting + * (Section 9.4) and **ought to be handled as an error**. A sender MUST + * remove the received Content-Length field prior to forwarding such + * a message downstream. + * + * Since llhttp 9, we go for the stricter approach and treat this as an error. + */ + const checkInvalidTransferEncoding = (otherwise: Node) => { + return this.testFlags(FLAGS.CONTENT_LENGTH, { + 1: this.testLenientFlags(LENIENT_FLAGS.CHUNKED_LENGTH, { + 0: p.error(ERROR.INVALID_TRANSFER_ENCODING, "Transfer-Encoding can't be present with Content-Length"), + }).otherwise(otherwise), + }).otherwise(otherwise); + }; + + const checkInvalidContentLength = (otherwise: Node) => { + return this.testFlags(FLAGS.TRANSFER_ENCODING, { + 1: this.testLenientFlags(LENIENT_FLAGS.CHUNKED_LENGTH, { + 0: p.error(ERROR.INVALID_CONTENT_LENGTH, "Content-Length can't be present with Transfer-Encoding"), + }).otherwise(otherwise), + }).otherwise(otherwise); + }; + const onHeaderFieldComplete = this.invokePausable( - 'on_header_field_complete', ERROR.CB_HEADER_FIELD_COMPLETE, n('header_value_discard_ws'), + 'on_header_field_complete', ERROR.CB_HEADER_FIELD_COMPLETE, + this.load('header_state', { + [HEADER_STATE.TRANSFER_ENCODING]: checkInvalidTransferEncoding(n('header_value_discard_ws')), + [HEADER_STATE.CONTENT_LENGTH]: checkInvalidContentLength(n('header_value_discard_ws')), + }, 'header_value_discard_ws'), ); const checkLenientFlagsOnColon = @@ -766,10 +798,13 @@ export class HTTP { }, span.headerValue.start(n('header_value_start')))) .otherwise(this.setHeaderFlags(onHeaderValueComplete)); + // Set `upgrade` if needed + const beforeHeadersComplete = p.invoke(callback.beforeHeadersComplete); + const checkTrailing = this.testFlags(FLAGS.TRAILING, { 1: this.invokePausable('on_chunk_complete', ERROR.CB_CHUNK_COMPLETE, 'message_done'), - }); + }).otherwise(beforeHeadersComplete); n('headers_almost_done') .match('\n', checkTrailing) @@ -778,43 +813,7 @@ export class HTTP { 1: checkTrailing, }, p.error(ERROR.STRICT, 'Expected LF after headers'))); - // Set `upgrade` if needed - const beforeHeadersComplete = p.invoke(callback.beforeHeadersComplete); - - /* Present `Transfer-Encoding` header overrides `Content-Length` even if the - * actual coding is not `chunked`. As per spec: - * - * https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3 - * - * If a message is received with both a Transfer-Encoding and a - * Content-Length header field, the Transfer-Encoding overrides the - * Content-Length. Such a message might indicate an attempt to - * perform request smuggling (Section 9.5) or response splitting - * (Section 9.4) and **ought to be handled as an error**. A sender MUST - * remove the received Content-Length field prior to forwarding such - * a message downstream. - * - * (Note our emphasis on **ought to be handled as an error** - */ - - const ENCODING_CONFLICT = FLAGS.TRANSFER_ENCODING | FLAGS.CONTENT_LENGTH; - - const onEncodingConflict = - this.testLenientFlags(LENIENT_FLAGS.CHUNKED_LENGTH, { - 0: p.error(ERROR.UNEXPECTED_CONTENT_LENGTH, - 'Content-Length can\'t be present with Transfer-Encoding'), - - // For LENIENT mode fall back to past behavior: - // Ignore `Transfer-Encoding` when `Content-Length` is present. - }).otherwise(beforeHeadersComplete); - - const checkEncConflict = this.testFlags(ENCODING_CONFLICT, { - 1: onEncodingConflict, - }).otherwise(beforeHeadersComplete); - - checkTrailing.otherwise(checkEncConflict); - - /* Here we call the headers_complete callback. This is somewhat + /* Here we call the headers_complete callback. This is somewhat * different than other callbacks because if the user returns 1, we * will interpret that as saying that this message has no body. This * is needed for the annoying case of receiving a response to a HEAD diff --git a/test/request/content-length.md b/test/request/content-length.md index e05f27ae..eddf9389 100644 --- a/test/request/content-length.md +++ b/test/request/content-length.md @@ -143,9 +143,7 @@ off=35 len=1 span[header_value]="1" off=38 header_value complete off=38 len=17 span[header_field]="Transfer-Encoding" off=56 header_field complete -off=57 len=8 span[header_value]="identity" -off=67 header_value complete -off=69 error code=4 reason="Content-Length can't be present with Transfer-Encoding" +off=56 error code=15 reason="Transfer-Encoding can't be present with Content-Length" ``` ## Invalid whitespace token with `Content-Length` header field diff --git a/test/request/transfer-encoding.md b/test/request/transfer-encoding.md index 4be8129f..84c4cedb 100644 --- a/test/request/transfer-encoding.md +++ b/test/request/transfer-encoding.md @@ -494,9 +494,7 @@ off=86 len=8 span[header_value]="identity" off=96 header_value complete off=96 len=14 span[header_field]="Content-Length" off=111 header_field complete -off=112 len=1 span[header_value]="5" -off=115 header_value complete -off=117 error code=4 reason="Content-Length can't be present with Transfer-Encoding" +off=111 error code=11 reason="Content-Length can't be present with Transfer-Encoding" ``` ## POST with `Transfer-Encoding` and `Content-Length` (lenient) @@ -541,6 +539,43 @@ off=117 headers complete method=3 v=1/1 flags=220 content_length=1 off=117 len=5 span[body]="World" ``` +## POST with empty `Transfer-Encoding` and `Content-Length` (lenient) + + +```http +POST / HTTP/1.1 +Host: foo +Content-Length: 10 +Transfer-Encoding: +Transfer-Encoding: +Transfer-Encoding: + +2 +AA +0 +``` + +```log +off=0 message begin +off=0 len=4 span[method]="POST" +off=4 method complete +off=5 len=1 span[url]="/" +off=7 url complete +off=12 len=3 span[version]="1.1" +off=15 version complete +off=17 len=4 span[header_field]="Host" +off=22 header_field complete +off=23 len=3 span[header_value]="foo" +off=28 header_value complete +off=28 len=14 span[header_field]="Content-Length" +off=43 header_field complete +off=44 len=2 span[header_value]="10" +off=48 header_value complete +off=48 len=17 span[header_field]="Transfer-Encoding" +off=66 header_field complete +off=66 error code=15 reason="Transfer-Encoding can't be present with Content-Length" +``` + ## POST with `chunked` before other transfer coding names From 311e81465d4dc168a7b1fcdbb275431f46fa8c09 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Mon, 11 Sep 2023 15:52:06 +0200 Subject: [PATCH 2/2] feat: Added new lenient flags for making CR optional. --- README.md | 8 ++ src/llhttp/constants.ts | 1 + src/llhttp/http.ts | 114 ++++++++++++++++++++++------ src/native/api.c | 8 ++ test/fixtures/extra.c | 16 +++- test/fixtures/index.ts | 12 ++- test/request/connection.md | 4 +- test/request/content-length.md | 2 +- test/request/invalid.md | 121 +++++++++++++++++++++++++++++- test/request/lenient-headers.md | 8 +- test/request/lenient-version.md | 2 +- test/request/sample.md | 4 +- test/request/transfer-encoding.md | 4 +- test/response/connection.md | 4 +- test/response/invalid.md | 55 +++++++++++++- test/response/lenient-version.md | 2 +- test/response/sample.md | 14 +++- 17 files changed, 327 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index e982ed0a..451a77f2 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,14 @@ Normally `llhttp` would error when a CR is not followed by LF when terminating t request line, the status line, the headers or a chunk header. With this flag only a CR is required to terminate such sections. +### `void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)` + +Enables/disables lenient handling of line separators. + +Normally `llhttp` would error when a LF is not preceded by CR when terminating the +request line, the status line, the headers, a chunk header or a chunk data. +With this flag only a LF is required to terminate such sections. + **Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!** ### `void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled)` diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index eb563703..96aa31aa 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -73,6 +73,7 @@ export enum LENIENT_FLAGS { DATA_AFTER_CLOSE = 1 << 5, OPTIONAL_LF_AFTER_CR = 1 << 6, OPTIONAL_CRLF_AFTER_CHUNK = 1 << 7, + OPTIONAL_CR_BEFORE_LF = 1 << 8, } export enum METHODS { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 116cf0ab..be125c3d 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -224,7 +224,7 @@ export class HTTP { p.property('i8', 'http_major'); p.property('i8', 'http_minor'); p.property('i8', 'header_state'); - p.property('i8', 'lenient_flags'); + p.property('i16', 'lenient_flags'); p.property('i8', 'upgrade'); p.property('i8', 'finish'); p.property('i16', 'flags'); @@ -311,6 +311,10 @@ export class HTTP { ); }; + const checkIfAllowLFWithoutCR = (success: Node, failure: Node) => { + return this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { 1: success }, failure); + }; + // Response n('start_res') .match('HTTP/', span.version.start(n('res_http_major'))) @@ -329,7 +333,7 @@ export class HTTP { .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version'))); n('res_http_end') - .otherwise(this.span.version.end().otherwise( + .otherwise(this.span.version.end( this.invokePausable('on_version_complete', ERROR.CB_VERSION_COMPLETE, 'res_after_version'), )); @@ -359,23 +363,36 @@ export class HTTP { })) .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid status code')); - n('res_status_code_otherwise') - .match(' ', n('res_status_start')) - .peek([ '\r', '\n' ], n('res_status_start')) - .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid response status')); - const onStatusComplete = this.invokePausable( 'on_status_complete', ERROR.CB_STATUS_COMPLETE, n('headers_start'), ); - n('res_status_start') + n('res_status_code_otherwise') + .match(' ', n('res_status_start')) .match('\r', n('res_line_almost_done')) - .match('\n', onStatusComplete) + .match( + '\n', + checkIfAllowLFWithoutCR( + onStatusComplete, + p.error(ERROR.INVALID_STATUS, 'Invalid response status'), + ), + ) + .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid response status')); + + n('res_status_start') .otherwise(span.status.start(n('res_status'))); n('res_status') .peek('\r', span.status.end().skipTo(n('res_line_almost_done'))) - .peek('\n', span.status.end().skipTo(n('res_line_almost_done'))) + .peek( + '\n', + span.status.end().skipTo( + checkIfAllowLFWithoutCR( + onStatusComplete, + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after response line'), + ), + ), + ) .skipTo(n('res_status')); n('res_line_almost_done') @@ -458,18 +475,26 @@ export class HTTP { .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version'))); n('req_http_end').otherwise( - span.version.end().otherwise( + span.version.end( this.invokePausable( 'on_version_complete', ERROR.CB_VERSION_COMPLETE, this.load('method', { [METHODS.PRI]: n('req_pri_upgrade'), }, n('req_http_complete')), - )), + ), + ), ); n('req_http_complete') .match('\r', n('req_http_complete_crlf')) + .match( + '\n', + checkIfAllowLFWithoutCR( + n('req_http_complete_crlf'), + p.error(ERROR.INVALID_VERSION, 'Expected CRLF after version'), + ), + ) .otherwise(p.error(ERROR.INVALID_VERSION, 'Expected CRLF after version')); n('req_http_complete_crlf') @@ -509,8 +534,11 @@ export class HTTP { n('header_field_start') .match('\r', n('headers_almost_done')) .match('\n', - this.testLenientFlags(LENIENT_FLAGS.HEADERS, { - 1: n('headers_done'), + this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { + 1: this.testFlags(FLAGS.TRAILING, { + 1: this.invokePausable('on_chunk_complete', + ERROR.CB_CHUNK_COMPLETE, 'message_done'), + }).otherwise(n('headers_done')), }, onInvalidHeaderFieldChar), ) .otherwise(span.headerField.start(n('header_field'))); @@ -602,7 +630,7 @@ export class HTTP { n('header_value_discard_ws') .match([ ' ', '\t' ], n('header_value_discard_ws')) .match('\r', n('header_value_discard_ws_almost_done')) - .match('\n', this.testLenientFlags(LENIENT_FLAGS.HEADERS, { + .match('\n', this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { 1: n('header_value_discard_lws'), }, p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char'))) .otherwise(span.headerValue.start(n('header_value_start'))); @@ -766,12 +794,25 @@ export class HTTP { .match(HEADER_CHARS, n('header_value')) .otherwise(n('header_value_otherwise')); + const checkIfAllowLFWithoutCR = (success: Node, failure: Node) => { + return this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { 1: success }, failure); + }; + const checkLenient = this.testLenientFlags(LENIENT_FLAGS.HEADERS, { 1: n('header_value_lenient'), }, span.headerValue.end(p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char'))); n('header_value_otherwise') .peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done'))) + .peek( + '\n', + span.headerValue.end( + checkIfAllowLFWithoutCR( + n('header_value_almost_done'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after header value'), + ), + ), + ) .otherwise(checkLenient); n('header_value_lenient') @@ -779,12 +820,6 @@ export class HTTP { .peek('\n', span.headerValue.end(n('header_value_almost_done'))) .skipTo(n('header_value_lenient')); - n('header_value_lenient_failed') - .peek('\n', span.headerValue.end().skipTo( - p.error(ERROR.CR_EXPECTED, 'Missing expected CR after header value')), - ) - .otherwise(p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char')); - n('header_value_almost_done') .match('\n', n('header_value_lws')) .otherwise(p.error(ERROR.LF_EXPECTED, @@ -890,6 +925,13 @@ export class HTTP { n('chunk_size_otherwise') .match('\r', n('chunk_size_almost_done')) + .match( + '\n', + checkIfAllowLFWithoutCR( + n('chunk_size_almost_done'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk size'), + ), + ) .match(';', n('chunk_extensions')) .otherwise(p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size')); @@ -922,6 +964,14 @@ export class HTTP { .peek('\r', this.span.chunkExtensionName.end().skipTo( onChunkExtensionNameCompleted(n('chunk_size_almost_done')), )) + .peek('\n', this.span.chunkExtensionName.end( + onChunkExtensionNameCompleted( + checkIfAllowLFWithoutCR( + n('chunk_size_almost_done'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension name'), + ), + ), + )) .otherwise(this.span.chunkExtensionName.end().skipTo( p.error(ERROR.STRICT, 'Invalid character in chunk extensions name'), )); @@ -935,6 +985,14 @@ export class HTTP { .peek('\r', this.span.chunkExtensionValue.end().skipTo( onChunkExtensionValueCompleted(n('chunk_size_almost_done')), )) + .peek('\n', this.span.chunkExtensionValue.end( + onChunkExtensionValueCompleted( + checkIfAllowLFWithoutCR( + n('chunk_size_almost_done'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension value'), + ), + ), + )) .otherwise(this.span.chunkExtensionValue.end().skipTo( p.error(ERROR.STRICT, 'Invalid character in chunk extensions value'), )); @@ -951,6 +1009,13 @@ export class HTTP { n('chunk_extension_quoted_value_done') .match(';', n('chunk_extensions')) .match('\r', n('chunk_size_almost_done')) + .peek( + '\n', + checkIfAllowLFWithoutCR( + n('chunk_size_almost_done'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension value'), + ), + ) .otherwise(p.error(ERROR.STRICT, 'Invalid character in chunk extensions quote value')); @@ -978,6 +1043,13 @@ export class HTTP { n('chunk_data_almost_done') .match('\r\n', n('chunk_complete')) + .match( + '\n', + checkIfAllowLFWithoutCR( + n('chunk_complete'), + p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk data'), + ), + ) .otherwise( this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CRLF_AFTER_CHUNK, { 1: n('chunk_complete'), diff --git a/src/native/api.c b/src/native/api.c index e0d03850..fa2133b7 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -315,6 +315,14 @@ void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled) } } +void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) { + if (enabled) { + parser->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF; + } else { + parser->lenient_flags &= ~LENIENT_OPTIONAL_CR_BEFORE_LF; + } +} + /* Callbacks */ diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 8a59635f..9b071873 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -81,7 +81,8 @@ void llhttp__test_init_request_lenient_all(llparse_t* s) { s->lenient_flags |= LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE | LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE | - LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CRLF_AFTER_CHUNK; + LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF | + LENIENT_OPTIONAL_CRLF_AFTER_CHUNK; } @@ -90,7 +91,8 @@ void llhttp__test_init_response_lenient_all(llparse_t* s) { s->lenient_flags |= LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE | LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE | - LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CRLF_AFTER_CHUNK; + LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF | + LENIENT_OPTIONAL_CRLF_AFTER_CHUNK; } @@ -159,6 +161,16 @@ void llhttp__test_init_response_lenient_optional_lf_after_cr(llparse_t* s) { s->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR; } +void llhttp__test_init_request_lenient_optional_cr_before_lf(llparse_t* s) { + llhttp__test_init_request(s); + s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF; +} + +void llhttp__test_init_response_lenient_optional_cr_before_lf(llparse_t* s) { + llhttp__test_init_response(s); + s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF; +} + void llhttp__test_init_request_lenient_optional_crlf_after_chunk(llparse_t* s) { llhttp__test_init_request(s); s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK; diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index 8604ee2b..9dfa7b90 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -12,12 +12,14 @@ export { FixtureResult }; export type TestType = 'request' | 'response' | 'request-finish' | 'response-finish' | 'request-lenient-all' | 'response-lenient-all' | - 'request-lenient-headers' | 'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' | - 'request-lenient-keep-alive' | 'response-lenient-keep-alive' | 'response-lenient-headers' | + 'request-lenient-headers' | 'response-lenient-headers' | + 'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' | + 'request-lenient-keep-alive' | 'response-lenient-keep-alive' | 'request-lenient-version' | 'response-lenient-version' | 'request-lenient-data-after-close' | 'response-lenient-data-after-close' | - 'response-lenient-optional-lf-after-cr' | 'request-lenient-optional-lf-after-cr' | - 'response-lenient-optional-crlf-after-chunk' | 'request-lenient-optional-crlf-after-chunk' | + 'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' | + 'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' | + 'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' | 'none' | 'url'; export const allowedTypes: TestType[] = [ @@ -39,6 +41,8 @@ export const allowedTypes: TestType[] = [ 'response-lenient-data-after-close', 'request-lenient-optional-lf-after-cr', 'response-lenient-optional-lf-after-cr', + 'request-lenient-optional-cr-before-lf', + 'response-lenient-optional-cr-before-lf', 'request-lenient-optional-crlf-after-chunk', 'response-lenient-optional-crlf-after-chunk', ]; diff --git a/test/request/connection.md b/test/request/connection.md index a0e2d973..a03242e1 100644 --- a/test/request/connection.md +++ b/test/request/connection.md @@ -96,7 +96,7 @@ off=21 message complete off=22 error code=5 reason="Data after `Connection: close`" ``` -### Resetting flags when keep-alive is off (1.0) and parser is in lenient mode +### Resetting flags when keep-alive is off (1.0, lenient) Even though we allow restarts in loose mode, the flags should be still set to `0` upon restart. @@ -289,7 +289,7 @@ off=133 message complete off=138 error code=5 reason="Data after `Connection: close`" ``` -### CRLF between requests, explicit `close` (lenient mode) +### CRLF between requests, explicit `close` (lenient) Loose mode is more lenient, and allows further requests. diff --git a/test/request/content-length.md b/test/request/content-length.md index eddf9389..524d1838 100644 --- a/test/request/content-length.md +++ b/test/request/content-length.md @@ -455,7 +455,7 @@ off=38 header_value complete off=39 error code=2 reason="Expected LF after headers" ``` -## Missing CRLF-CRLF before body in lenient +## Missing CRLF-CRLF before body (lenient) ```http diff --git a/test/request/invalid.md b/test/request/invalid.md index 99d8468e..bcc41b48 100644 --- a/test/request/invalid.md +++ b/test/request/invalid.md @@ -130,7 +130,7 @@ off=39 header_value complete off=39 len=1 span[header_field]="x" off=41 header_field complete off=41 len=1 span[header_value]="x" -off=42 error code=10 reason="Invalid header value char" +off=42 error code=25 reason="Missing expected CR after header value" ``` ### Headers separated by dummy characters @@ -165,7 +165,7 @@ off=45 error code=2 reason="Expected LF after headers" ``` -### Headers separated by dummy characters in lenient mode +### Headers separated by dummy characters (lenient) ```http @@ -430,7 +430,7 @@ off=33 header_value complete off=33 len=5 span[header_field]="Dummy" off=39 header_field complete off=40 len=1 span[header_value]="x" -off=41 error code=10 reason="Invalid header value char" +off=41 error code=25 reason="Missing expected CR after header value" ``` ### Invalid HTTP version @@ -467,4 +467,119 @@ off=6 url complete off=11 len=3 span[version]="1.1" off=14 version complete off=17 error code=30 reason="Unexpected space after start line" +``` + + +### Only LFs present + + +```http +POST / HTTP/1.1\n\ +Transfer-Encoding: chunked\n\ +Trailer: Baz +Foo: abc\n\ +Bar: def\n\ +\n\ +1\n\ +A\n\ +1;abc\n\ +B\n\ +1;def=ghi\n\ +C\n\ +1;jkl="mno"\n\ +D\n\ +0\n\ +\n\ +Baz: ghi\n\ +\n\ +``` + +```log +off=0 message begin +off=0 len=4 span[method]="POST" +off=4 method complete +off=5 len=1 span[url]="/" +off=7 url complete +off=12 len=3 span[version]="1.1" +off=15 version complete +off=16 error code=9 reason="Expected CRLF after version" +``` + +### Only LFs present (lenient) + + +```http +POST / HTTP/1.1\n\ +Transfer-Encoding: chunked\n\ +Trailer: Baz +Foo: abc\n\ +Bar: def\n\ +\n\ +1\n\ +A\n\ +1;abc\n\ +B\n\ +1;def=ghi\n\ +C\n\ +1;jkl="mno"\n\ +D\n\ +0\n\ +\n\ +Baz: ghi\n\ +\n +``` + +```log +off=0 message begin +off=0 len=4 span[method]="POST" +off=4 method complete +off=5 len=1 span[url]="/" +off=7 url complete +off=12 len=3 span[version]="1.1" +off=15 version complete +off=16 len=17 span[header_field]="Transfer-Encoding" +off=34 header_field complete +off=35 len=7 span[header_value]="chunked" +off=43 header_value complete +off=43 len=7 span[header_field]="Trailer" +off=51 header_field complete +off=52 len=3 span[header_value]="Baz" +off=57 header_value complete +off=57 len=3 span[header_field]="Foo" +off=61 header_field complete +off=62 len=3 span[header_value]="abc" +off=66 header_value complete +off=66 len=3 span[header_field]="Bar" +off=70 header_field complete +off=71 len=3 span[header_value]="def" +off=75 header_value complete +off=78 chunk header len=1 +off=78 len=1 span[body]="A" +off=80 chunk complete +off=82 len=3 span[chunk_extension_name]="abc" +off=85 chunk_extension_name complete +off=86 chunk header len=1 +off=86 len=1 span[body]="B" +off=88 chunk complete +off=90 len=3 span[chunk_extension_name]="def" +off=94 chunk_extension_name complete +off=94 len=3 span[chunk_extension_value]="ghi" +off=97 chunk_extension_value complete +off=98 chunk header len=1 +off=98 len=1 span[body]="C" +off=100 chunk complete +off=102 len=3 span[chunk_extension_name]="jkl" +off=106 chunk_extension_name complete +off=106 len=5 span[chunk_extension_value]=""mno"" +off=111 chunk_extension_value complete +off=112 chunk header len=1 +off=112 len=1 span[body]="D" +off=114 chunk complete +off=117 chunk header len=0 +off=117 len=3 span[header_field]="Baz" +off=121 header_field complete +off=122 len=3 span[header_value]="ghi" +off=126 header_value complete +off=127 chunk complete +off=127 message complete ``` \ No newline at end of file diff --git a/test/request/lenient-headers.md b/test/request/lenient-headers.md index 114d2163..05e105f8 100644 --- a/test/request/lenient-headers.md +++ b/test/request/lenient-headers.md @@ -3,7 +3,7 @@ Lenient header value parsing Parsing with header value token checks off. -## Header value with lenient +## Header value (lenient) ```http @@ -29,7 +29,7 @@ off=33 headers complete method=1 v=1/1 flags=0 content_length=0 off=33 message complete ``` -## Second request header value with lenient +## Second request header value (lenient) ```http @@ -73,7 +73,7 @@ off=71 headers complete method=1 v=1/1 flags=0 content_length=0 off=71 message complete ``` -## Header value without lenient +## Header value ```http @@ -98,7 +98,7 @@ off=28 len=0 span[header_value]="" off=28 error code=10 reason="Invalid header value char" ``` -### Empty headers separated by CR (Lenient) +### Empty headers separated by CR (lenient) ```http diff --git a/test/request/lenient-version.md b/test/request/lenient-version.md index dbe360c6..41855569 100644 --- a/test/request/lenient-version.md +++ b/test/request/lenient-version.md @@ -1,7 +1,7 @@ Lenient HTTP version parsing ============================ -### Invalid HTTP version with lenient +### Invalid HTTP version (lenient) ```http diff --git a/test/request/sample.md b/test/request/sample.md index a5347e62..f0a5d449 100644 --- a/test/request/sample.md +++ b/test/request/sample.md @@ -404,7 +404,7 @@ off=14 version complete off=16 len=5 span[header_field]="Line1" off=22 header_field complete off=25 len=3 span[header_value]="abc" -off=28 error code=10 reason="Invalid header value char" +off=28 error code=25 reason="Missing expected CR after header value" ``` ## No LF after CR @@ -427,7 +427,7 @@ off=14 version complete off=15 error code=2 reason="Expected CRLF after version" ``` -## No LF after CR in lenient mode +## No LF after CR (lenient) diff --git a/test/request/transfer-encoding.md b/test/request/transfer-encoding.md index 84c4cedb..19b2dec1 100644 --- a/test/request/transfer-encoding.md +++ b/test/request/transfer-encoding.md @@ -966,7 +966,7 @@ off=76 headers complete method=1 v=1/1 flags=20a content_length=0 off=78 error code=2 reason="Expected LF after chunk size" ``` -### Chunk header not terminated by CRLF in lenient mode +### Chunk header not terminated by CRLF (lenient) @@ -1077,7 +1077,7 @@ off=79 len=5 span[body]="ABCDE" off=84 error code=2 reason="Expected LF after chunk data" ``` -### Chunk data not terminated by CRLF in lenient mode +### Chunk data not terminated by CRLF (lenient) diff --git a/test/response/connection.md b/test/response/connection.md index b3f4f5e2..d1d7bdd4 100644 --- a/test/response/connection.md +++ b/test/response/connection.md @@ -172,7 +172,7 @@ off=46 message complete off=47 error code=5 reason="Data after `Connection: close`" ``` -## HTTP/1.1 with keep-alive disabled, content-length, and in lenient mode +## HTTP/1.1 with keep-alive disabled, content-length (lenient) Parser should discard extra request in lenient mode. @@ -237,7 +237,7 @@ off=70 message complete off=71 error code=5 reason="Data after `Connection: close`" ``` -## HTTP/1.1 with keep-alive disabled and 204 status in lenient mode +## HTTP/1.1 with keep-alive disabled and 204 status (lenient) ```http diff --git a/test/response/invalid.md b/test/response/invalid.md index 8e113f91..5fba2555 100644 --- a/test/response/invalid.md +++ b/test/response/invalid.md @@ -198,7 +198,7 @@ off=8 version complete off=10 error code=13 reason="Invalid status code" ``` -### Only LFs present +### Only LFs present and no body ```http @@ -210,10 +210,10 @@ off=0 message begin off=5 len=3 span[version]="1.1" off=8 version complete off=13 len=2 span[status]="OK" -off=16 error code=2 reason="Expected LF after CR" +off=16 error code=25 reason="Missing expected CR after response line" ``` -### Only LFs present (lenient) +### Only LFs present and no body (lenient) ```http @@ -231,4 +231,53 @@ off=31 header_field complete off=32 len=1 span[header_value]="0" off=34 header_value complete off=35 message complete +``` + +### Only LFs present + + +```http +HTTP/1.1 200 OK\n\ +Foo: abc\n\ +Bar: def\n\ +\n\ +BODY\n\ +``` + +```log +off=0 message begin +off=5 len=3 span[version]="1.1" +off=8 version complete +off=13 len=2 span[status]="OK" +off=16 error code=25 reason="Missing expected CR after response line" +``` + +### Only LFs present (lenient) + + +```http +HTTP/1.1 200 OK\n\ +Foo: abc\n\ +Bar: def\n\ +\n\ +BODY\n\ +``` + +```log +off=0 message begin +off=5 len=3 span[version]="1.1" +off=8 version complete +off=13 len=2 span[status]="OK" +off=16 status complete +off=16 len=3 span[header_field]="Foo" +off=20 header_field complete +off=21 len=3 span[header_value]="abc" +off=25 header_value complete +off=25 len=3 span[header_field]="Bar" +off=29 header_field complete +off=30 len=3 span[header_value]="def" +off=34 header_value complete +off=35 len=4 span[body]="BODY" +off=39 len=1 span[body]=lf +off=40 len=1 span[body]="\" ``` \ No newline at end of file diff --git a/test/response/lenient-version.md b/test/response/lenient-version.md index e0d154ab..86c6edef 100644 --- a/test/response/lenient-version.md +++ b/test/response/lenient-version.md @@ -1,7 +1,7 @@ Lenient HTTP version parsing ============================ -### Invalid HTTP version with lenient +### Invalid HTTP version (lenient) ```http diff --git a/test/response/sample.md b/test/response/sample.md index 9cf3bd0c..0fd3c1da 100644 --- a/test/response/sample.md +++ b/test/response/sample.md @@ -266,6 +266,7 @@ HTTP/1.1 200 \r\n\ off=0 message begin off=5 len=3 span[version]="1.1" off=8 version complete +off=13 len=0 span[status]="" off=15 status complete off=17 headers complete status=200 v=1/1 flags=0 content_length=0 ``` @@ -286,12 +287,12 @@ off=0 message begin off=5 len=3 span[version]="1.1" off=8 version complete off=13 len=2 span[status]="OK" -off=16 error code=2 reason="Expected LF after CR" +off=16 error code=25 reason="Missing expected CR after response line" ``` -## No carriage ret in lenient mode +## No carriage ret (lenient) - + ```http HTTP/1.1 200 OK\n\ Content-Type: text/html; charset=utf-8\n\ @@ -309,7 +310,12 @@ off=16 status complete off=16 len=12 span[header_field]="Content-Type" off=29 header_field complete off=30 len=24 span[header_value]="text/html; charset=utf-8" -off=54 error code=10 reason="Invalid header value char" +off=55 header_value complete +off=55 len=10 span[header_field]="Connection" +off=66 header_field complete +off=67 len=5 span[header_value]="close" +off=73 header_value complete +off=74 len=51 span[body]="these headers are from http://news.ycombinator.com/" ``` ## Underscore in header key