diff --git a/README.md b/README.md index 019a1d15..3b9ce5d5 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,15 @@ this flag, control characters (except for NULL, CR & LF) will be accepted in hea This does not create any known security issue, but does allow content considered 'invalid' by [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-field-values) and so should be avoided by default. +### `void llhttp_set_lenient_optional_sp_after_status(llhttp_t* parser, int enabled)` + +Enables/disables lenient handling of spaces after status code + +As per [RFC 9112](https://www.rfc-editor.org/info/rfc9112/#section-4-8): +> A server MUST send the space that separates the status-code from the reason-phrase even when the reason-phrase is absent (i.e., the status-line would end with the space). + +This flag allows status code with no trailing space. + ## Build Instructions Make sure you have [Node.js](https://nodejs.org/), npm and npx installed. Then under project directory run: diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index dd64c623..4f103b76 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -80,6 +80,7 @@ export const LENIENT_FLAGS = { OPTIONAL_CR_BEFORE_LF: 1 << 8, SPACES_AFTER_CHUNK_SIZE: 1 << 9, HEADER_VALUE_RELAXED: 1 << 10, + OPTIONAL_SP_AFTER_STATUS: 1 << 11, } as const; export const STATUSES = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index dec22287..9c2be1fc 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -39,7 +39,7 @@ const NODES = [ 'res_status_code_digit_1', 'res_status_code_digit_2', 'res_status_code_digit_3', - 'res_status_code_otherwise', + 'res_status_trailing_space', 'res_status_start', 'res_status', 'res_line_almost_done', @@ -385,7 +385,7 @@ export class HTTP { n('res_status_code_digit_3') .select(NUM_MAP, this.mulAdd('status_code', { overflow: p.error(ERROR.INVALID_STATUS, 'Invalid status code'), - success: 'res_status_code_otherwise', + success: 'res_status_trailing_space', })) .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid status code')); @@ -393,17 +393,15 @@ export class HTTP { 'on_status_complete', ERROR.CB_STATUS_COMPLETE, n('headers_start'), ); - n('res_status_code_otherwise') + n('res_status_trailing_space') .match(' ', n('res_status_start')) - .match('\r', n('res_line_almost_done')) - .match( - '\n', - checkIfAllowLFWithoutCR( - onStatusComplete, - p.error(ERROR.INVALID_STATUS, 'Invalid response status'), - ), - ) - .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid response status')); + .otherwise( + this.testLenientFlags( + LENIENT_FLAGS.OPTIONAL_SP_AFTER_STATUS, + { 1: n('res_line_almost_done') }, + p.error(ERROR.INVALID_STATUS, 'Invalid response status') + ) + ); n('res_status_start') .otherwise(span.status.start(n('res_status'))); diff --git a/src/native/api.c b/src/native/api.c index ae5e862d..9d0b67d6 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -324,6 +324,14 @@ void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled) { } } +void llhttp_set_lenient_optional_sp_after_status(llhttp_t* parser, int enabled) { + if (enabled) { + parser->lenient_flags |= LENIENT_OPTIONAL_SP_AFTER_STATUS; + } else { + parser->lenient_flags &= ~LENIENT_OPTIONAL_SP_AFTER_STATUS; + } +} + /* Callbacks */ diff --git a/src/native/api.h b/src/native/api.h index 0a58d4e0..21f5cacb 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -370,6 +370,18 @@ void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled); LLHTTP_EXPORT void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled); +/* Enables/disables lenient handling of spaces after status code + * + * As per RFC 9112: + * A server MUST send the space that separates the status-code from the + * reason-phrase even when the reason-phrase is absent + * (i.e., the status-line would end with the space). + * + * This flag allows status code with no trailing space. + */ +LLHTTP_EXPORT +void llhttp_set_lenient_optional_sp_after_status(llhttp_t* parser, int enabled); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 5f14cb2f..0ec0781e 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -201,6 +201,16 @@ void llhttp__test_init_response_lenient_header_value_relaxed(llparse_t* s) { s->lenient_flags |= LENIENT_HEADER_VALUE_RELAXED; } +void llhttp__test_init_request_lenient_optional_sp_after_status(llparse_t* s) { + llhttp__test_init_request(s); + s->lenient_flags |= LENIENT_OPTIONAL_SP_AFTER_STATUS; +} + +void llhttp__test_init_response_lenient_optional_sp_after_status(llparse_t* s) { + llhttp__test_init_response(s); + s->lenient_flags |= LENIENT_OPTIONAL_SP_AFTER_STATUS; +} + void llhttp__test_finish(llparse_t* s) { llparse__print(NULL, NULL, "finish=%d", s->finish); diff --git a/test/response/invalid.md b/test/response/invalid.md index 879cc27c..34f8f155 100644 --- a/test/response/invalid.md +++ b/test/response/invalid.md @@ -67,7 +67,6 @@ off=5 len=3 span[version]="1.0" off=8 version complete off=8 error code=9 reason="Expected space after version" ``` ---> ### Tab after HTTP version @@ -266,6 +265,24 @@ off=18 status complete off=20 headers complete status=200 v=1/1 flags=0 content_length=0 ``` +### No space after status code (no reason) + + +```http +HTTP/1.1 301 + + +``` + +```log +off=0 message begin +off=0 len=4 span[protocol]="HTTP" +off=4 protocol complete +off=5 len=3 span[version]="1.1" +off=8 version complete +off=12 error code=13 reason="Invalid response status" +``` + ### One-digit status code diff --git a/test/response/sample.md b/test/response/sample.md index dbacb63d..9f6d7d84 100644 --- a/test/response/sample.md +++ b/test/response/sample.md @@ -288,25 +288,6 @@ off=24 status complete off=26 headers complete status=404 v=1/1 flags=0 content_length=0 ``` -## No reason phrase - - -```http -HTTP/1.1 301 - - -``` - -```log -off=0 message begin -off=0 len=4 span[protocol]="HTTP" -off=4 protocol complete -off=5 len=3 span[version]="1.1" -off=8 version complete -off=14 status complete -off=16 headers complete status=301 v=1/1 flags=0 content_length=0 -``` - ## Empty reason phrase after space