diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index 8d6654df..0982c82d 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -48,6 +48,7 @@ export enum FLAGS { CONTENT_LENGTH = 1 << 5, SKIPBODY = 1 << 6, TRAILING = 1 << 7, + LENIENT = 1 << 8, } export enum METHODS { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 83779c42..784afd47 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -54,6 +54,7 @@ const NODES: ReadonlyArray = [ 'header_value_start', 'header_value', 'header_value_otherwise', + 'header_value_lenient', 'header_value_lws', 'header_value_te_chunked', 'header_value_content_length_once', @@ -177,7 +178,7 @@ export class HTTP { p.property('i8', 'http_major'); p.property('i8', 'http_minor'); p.property('i8', 'header_state'); - p.property('i8', 'flags'); + p.property('i16', 'flags'); p.property('i8', 'upgrade'); p.property('i16', 'status_code'); p.property('i8', 'finish'); @@ -491,12 +492,19 @@ export class HTTP { .match(HEADER_CHARS, n('header_value')) .otherwise(n('header_value_otherwise')); + const checkLenient = this.testFlags(FLAGS.LENIENT, { + 1: n('header_value_lenient'), + }, 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(n('header_value_almost_done'))) - // TODO(indutny): do we need `lenient` option? (it is always off now) - .otherwise(p.error(ERROR.INVALID_HEADER_TOKEN, - 'Invalid header value char')); + .otherwise(checkLenient); + + n('header_value_lenient') + .peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done'))) + .peek('\n', span.headerValue.end(n('header_value_almost_done'))) + .skipTo(n('header_value_lenient')); n('header_value_almost_done') .match('\n', n('header_value_lws')) diff --git a/src/native/api.c b/src/native/api.c index 45227b35..9a4a8718 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -127,6 +127,15 @@ const char* llhttp_method_name(llhttp_method_t method) { } +void llhttp_set_lenient(llhttp_t* parser, int enabled) { + if (enabled) { + parser->flags |= F_LENIENT; + } else if (parser->flags & F_LENIENT) { + parser->flags ^= F_LENIENT; + } +} + + /* Callbacks */ diff --git a/src/native/api.h b/src/native/api.h index 51ec21e6..3635fa69 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -141,6 +141,18 @@ const char* llhttp_errno_name(llhttp_errno_t err); /* Returns textual name of HTTP method */ const char* llhttp_method_name(llhttp_method_t method); + +/* Enables/disables lenient header value parsing (disabled by default). + * + * Lenient parsing disables header value token checks, extending llhttp's + * protocol support to highly non-compliant clients/server. No + * `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when + * lenient parsing is "on". + * + * **(USE AT YOUR OWN RISK)** + */ +void llhttp_set_lenient(llhttp_t* parser, int enabled); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 72869078..98a086e9 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -57,6 +57,12 @@ void llhttp__test_init_response(llparse_t* s) { } +void llhttp__test_init_request_lenient(llparse_t* s) { + llhttp__test_init_request(s); + s->flags |= F_LENIENT; +} + + 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 5393a073..fefef606 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -8,8 +8,8 @@ import * as path from 'path'; import * as llhttp from '../../src/llhttp'; -export type TestType = 'request' | 'response' | 'request-finish' | - 'response-finish' | 'none' | 'url'; +export type TestType = 'request' | 'response' | 'request-lenient' | + 'request-finish' | 'response-finish' | 'none' | 'url'; export { FixtureResult }; @@ -58,8 +58,9 @@ export function build(llparse: LLParse, node: any, outFile: string, } const extra = options.extra === undefined ? [] : options.extra.slice(); - if (ty === 'request' || ty === 'response') { - extra.push(`-DLLPARSE__TEST_INIT=llhttp__test_init_${ty}`); + if (ty === 'request' || ty === 'response' || ty === 'request-lenient') { + extra.push( + `-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`); } else if (ty === 'request-finish' || ty === 'response-finish') { if (ty === 'request-finish') { extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_request'); diff --git a/test/md-test.ts b/test/md-test.ts index 1eec09fa..91c4028d 100644 --- a/test/md-test.ts +++ b/test/md-test.ts @@ -78,6 +78,7 @@ const http: IFixtureMap = { 'none': buildMode('loose', 'none'), 'request': buildMode('loose', 'request'), 'request-finish': buildMode('loose', 'request-finish'), + 'request-lenient': buildMode('loose', 'request-lenient'), 'response': buildMode('loose', 'response'), 'response-finish': buildMode('loose', 'response-finish'), 'url': buildMode('loose', 'url'), @@ -86,6 +87,7 @@ const http: IFixtureMap = { 'none': buildMode('strict', 'none'), 'request': buildMode('strict', 'request'), 'request-finish': buildMode('strict', 'request-finish'), + 'request-lenient': buildMode('strict', 'request-lenient'), 'response': buildMode('strict', 'response'), 'response-finish': buildMode('strict', 'response-finish'), 'url': buildMode('strict', 'url'), @@ -143,6 +145,8 @@ function run(name: string): void { types.push('response'); } else if (meta.type === 'request-only') { types = [ 'request' ]; + } else if (meta.type === 'request-lenient') { + types = [ 'request-lenient' ]; } else if (meta.type === 'response-only') { types = [ 'response' ]; } else if (meta.type === 'request-finish') { @@ -231,6 +235,7 @@ function run(name: string): void { } run('request/sample'); +run('request/lenient'); run('request/method'); run('request/uri'); run('request/connection'); diff --git a/test/request/lenient.md b/test/request/lenient.md new file mode 100644 index 00000000..450dd401 --- /dev/null +++ b/test/request/lenient.md @@ -0,0 +1,41 @@ +Lenient header value parsing +============================ + +Parsing with header value token checks off. + +## Header value with lenient + + +```http +GET /url HTTP/1.1 +Header1: \f + + +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=7 span[header_field]="Header1" +off=28 len=1 span[header_value]="\f" +off=33 headers complete method=1 v=1/1 flags=100 content_length=0 +off=33 message complete +``` + +## Header value without lenient + + +```http +GET /url HTTP/1.1 +Header1: \f + + + +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=7 span[header_field]="Header1" +off=28 error code=10 reason="Invalid header value char" +```