diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index 1971a831..3da61705 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -56,6 +56,7 @@ export enum FLAGS { export enum LENIENT_FLAGS { HEADERS = 1 << 0, CHUNKED_LENGTH = 1 << 1, + KEEP_ALIVE = 1 << 2, } export enum METHODS { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 0a0c3659..4511096e 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -787,22 +787,27 @@ export class HTTP { .otherwise(this.invokePausable('on_message_complete', ERROR.CB_MESSAGE_COMPLETE, upgradeAfterDone)); + const lenientClose = this.testLenientFlags(LENIENT_FLAGS.KEEP_ALIVE, { + 1: n('restart'), + }, n('closed')); + // Check if we'd like to keep-alive + n('cleanup') + .otherwise(p.invoke(callback.afterMessageComplete, { + 1: n('restart'), + }, this.update('finish', FINISH.SAFE, lenientClose))); + if (this.mode === 'strict') { - n('cleanup') - .otherwise(p.invoke(callback.afterMessageComplete, { - 1: n('restart'), - }, this.update('finish', FINISH.SAFE, n('closed')))); + // Error on extra data after `Connection: close` + n('closed') + .match([ '\r', '\n' ], n('closed')) + .skipTo(p.error(ERROR.CLOSED_CONNECTION, + 'Data after `Connection: close`')); } else { - n('cleanup') - .otherwise(p.invoke(callback.afterMessageComplete, n('restart'))); + // Discard all data after `Connection: close` + n('closed').skipTo(n('closed')); } - n('closed') - .match([ '\r', '\n' ], n('closed')) - .skipTo(p.error(ERROR.CLOSED_CONNECTION, - 'Data after `Connection: close`')); - n('restart') .otherwise(this.update('finish', FINISH.SAFE, n('start'))); } diff --git a/src/native/api.c b/src/native/api.c index 0f9b5677..1d68639f 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -150,6 +150,7 @@ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) { } } + void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) { if (enabled) { parser->lenient_flags |= LENIENT_CHUNKED_LENGTH; @@ -159,6 +160,14 @@ void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) { } +void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled) { + if (enabled) { + parser->lenient_flags |= LENIENT_KEEP_ALIVE; + } else { + parser->lenient_flags &= ~LENIENT_KEEP_ALIVE; + } +} + /* Callbacks */ diff --git a/src/native/api.h b/src/native/api.h index f62c57fe..fb19618d 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -181,6 +181,20 @@ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled); */ void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled); + +/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0 + * requests responses. + * + * Normally `llhttp` would error on (in strict mode) or discard (in loose mode) + * the HTTP request/response after the request/response with `Connection: close` + * and `Content-Length`. This is important to prevent cache poisoning attacks, + * but might interact badly with outdated and insecure clients. With this flag + * the extra request/response will be parsed normally. + * + * **(USE AT YOUR OWN RISK)** + */ +void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 2ff6f796..7686b65c 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -70,12 +70,25 @@ void llhttp__test_init_request_lenient_headers(llparse_t* s) { s->lenient_flags |= LENIENT_HEADERS; } + void llhttp__test_init_request_lenient_chunked_length(llparse_t* s) { llhttp__test_init_request(s); s->lenient_flags |= LENIENT_CHUNKED_LENGTH; } +void llhttp__test_init_request_lenient_keep_alive(llparse_t* s) { + llhttp__test_init_request(s); + s->lenient_flags |= LENIENT_KEEP_ALIVE; +} + + +void llhttp__test_init_response_lenient_keep_alive(llparse_t* s) { + llhttp__test_init_response(s); + s->lenient_flags |= LENIENT_KEEP_ALIVE; +} + + void llhttp__test_finish(llparse_t* s) { llparse__print(NULL, NULL, "finish=%d", s->finish); } diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index 858236cf..ef8e7949 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -9,7 +9,8 @@ import * as path from 'path'; import * as llhttp from '../../src/llhttp'; export type TestType = 'request' | 'response' | 'request-lenient-headers' | - 'request-lenient-chunked-length' | 'request-finish' | 'response-finish' | + 'request-lenient-chunked-length' | 'request-lenient-keep-alive' | + 'response-lenient-keep-alive' | 'request-finish' | 'response-finish' | 'none' | 'url'; export { FixtureResult }; @@ -61,7 +62,9 @@ export async function build( const extra = options.extra === undefined ? [] : options.extra.slice(); if (ty === 'request' || ty === 'response' || ty === 'request-lenient-headers' || - ty === 'request-lenient-chunked-length') { + ty === 'request-lenient-chunked-length' || + ty === 'request-lenient-keep-alive' || + ty === 'response-lenient-keep-alive') { extra.push( `-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`); } else if (ty === 'request-finish' || ty === 'response-finish') { diff --git a/test/md-test.ts b/test/md-test.ts index 5aa740dc..6af8f86b 100644 --- a/test/md-test.ts +++ b/test/md-test.ts @@ -82,8 +82,12 @@ const http: IFixtureMap = { 'request-lenient-chunked-length': buildMode('loose', 'request-lenient-chunked-length'), 'request-lenient-headers': buildMode('loose', 'request-lenient-headers'), + 'request-lenient-keep-alive': buildMode( + 'loose', 'request-lenient-keep-alive'), 'response': buildMode('loose', 'response'), 'response-finish': buildMode('loose', 'response-finish'), + 'response-lenient-keep-alive': buildMode( + 'loose', 'response-lenient-keep-alive'), 'url': buildMode('loose', 'url'), }, strict: { @@ -93,8 +97,12 @@ const http: IFixtureMap = { 'request-lenient-chunked-length': buildMode('strict', 'request-lenient-chunked-length'), 'request-lenient-headers': buildMode('strict', 'request-lenient-headers'), + 'request-lenient-keep-alive': buildMode( + 'strict', 'request-lenient-keep-alive'), 'response': buildMode('strict', 'response'), 'response-finish': buildMode('strict', 'response-finish'), + 'response-lenient-keep-alive': buildMode( + 'strict', 'response-lenient-keep-alive'), 'url': buildMode('strict', 'url'), }, }; @@ -155,6 +163,10 @@ function run(name: string): void { types = [ 'request-lenient-headers' ]; } else if (meta.type === 'request-lenient-chunked-length') { types = [ 'request-lenient-chunked-length' ]; + } else if (meta.type === 'request-lenient-keep-alive') { + types = [ 'request-lenient-keep-alive' ]; + } else if (meta.type === 'response-lenient-keep-alive') { + types = [ 'response-lenient-keep-alive' ]; } else if (meta.type === 'response-only') { types = [ 'response' ]; } else if (meta.type === 'request-finish') { diff --git a/test/request/connection.md b/test/request/connection.md index c6dc2641..ef0393a3 100644 --- a/test/request/connection.md +++ b/test/request/connection.md @@ -79,12 +79,12 @@ 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 loose mode +### Resetting flags when keep-alive is off (1.0) and parser is in lenient mode Even though we allow restarts in loose mode, the flags should be still set to `0` upon restart. - + ```http PUT /url HTTP/1.0 Content-Length: 0 @@ -242,11 +242,11 @@ off=133 message complete off=138 error code=5 reason="Data after `Connection: close`" ``` -### CRLF between requests, explicit `close` (loose mode) +### CRLF between requests, explicit `close` (lenient mode) Loose mode is more lenient, and allows further requests. - + ```http POST / HTTP/1.1 Host: www.example.com diff --git a/test/response/connection.md b/test/response/connection.md index f9c0e743..1fa1f470 100644 --- a/test/response/connection.md +++ b/test/response/connection.md @@ -154,10 +154,103 @@ off=46 message complete off=47 error code=5 reason="Data after `Connection: close`" ``` -## HTTP/1.1 with keep-alive disabled and 204 status in loose mode +## HTTP/1.1 with keep-alive disabled, content-length, and in loose mode + +Parser should discard extra request in loose mode. ```http +HTTP/1.1 200 No content +Content-Length: 5 +Connection: close + +2ad731e3-4dcd-4f70-b871-0ad284b29ffc +``` + +```log +off=0 message begin +off=13 len=10 span[status]="No content" +off=25 status complete +off=25 len=14 span[header_field]="Content-Length" +off=40 header_field complete +off=41 len=1 span[header_value]="5" +off=44 header_value complete +off=44 len=10 span[header_field]="Connection" +off=55 header_field complete +off=56 len=5 span[header_value]="close" +off=63 header_value complete +off=65 headers complete status=200 v=1/1 flags=22 content_length=5 +off=65 len=5 span[body]="2ad73" +off=70 message complete +``` + +## HTTP/1.1 with keep-alive disabled, content-length, and in strict mode + +Parser should discard extra request in strict mode. + + +```http +HTTP/1.1 200 No content +Content-Length: 5 +Connection: close + +2ad731e3-4dcd-4f70-b871-0ad284b29ffc +``` + +```log +off=0 message begin +off=13 len=10 span[status]="No content" +off=25 status complete +off=25 len=14 span[header_field]="Content-Length" +off=40 header_field complete +off=41 len=1 span[header_value]="5" +off=44 header_value complete +off=44 len=10 span[header_field]="Connection" +off=55 header_field complete +off=56 len=5 span[header_value]="close" +off=63 header_value complete +off=65 headers complete status=200 v=1/1 flags=22 content_length=5 +off=65 len=5 span[body]="2ad73" +off=70 message complete +off=71 error code=5 reason="Data after `Connection: close`" +``` + +## HTTP/1.1 with keep-alive disabled, content-length, and in lenient mode + +Parser should process extra request in lenient mode. + + +```http +HTTP/1.1 200 No content +Content-Length: 5 +Connection: close + +2ad73HTTP/1.1 200 OK +``` + +```log +off=0 message begin +off=13 len=10 span[status]="No content" +off=25 status complete +off=25 len=14 span[header_field]="Content-Length" +off=40 header_field complete +off=41 len=1 span[header_value]="5" +off=44 header_value complete +off=44 len=10 span[header_field]="Connection" +off=55 header_field complete +off=56 len=5 span[header_value]="close" +off=63 header_value complete +off=65 headers complete status=200 v=1/1 flags=22 content_length=5 +off=65 len=5 span[body]="2ad73" +off=70 message complete +off=70 message begin +off=83 len=2 span[status]="OK" +``` + +## HTTP/1.1 with keep-alive disabled and 204 status in lenient mode + + +```http HTTP/1.1 204 No content Connection: close