diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index 64e992ad..e1ba48a8 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -60,6 +60,7 @@ export enum LENIENT_FLAGS { CHUNKED_LENGTH = 1 << 1, KEEP_ALIVE = 1 << 2, TRANSFER_ENCODING = 1 << 3, + VERSION = 1 << 4, } export enum METHODS { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index f46d8946..e44c8598 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -244,8 +244,27 @@ export class HTTP { .match('HTTP/', this.update('type', TYPE.RESPONSE, 'res_http_major')) .otherwise(p.error(ERROR.INVALID_CONSTANT, 'Invalid word encountered')); - // Response + const checkVersion = (destination: string): Node => { + return this.testLenientFlags(LENIENT_FLAGS.VERSION, + { + 1: n(destination), + }, + this.load('http_major', { + 0: this.load('http_minor', { + 9: n(destination), + }, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')), + 1: this.load('http_minor', { + 0: n(destination), + 1: n(destination), + }, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')), + 2: this.load('http_minor', { + 0: n(destination), + }, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')), + }, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')), + ); + }; + // Response n('start_res') .match('HTTP/', n('res_http_major')) .otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/')); @@ -259,7 +278,7 @@ export class HTTP { .otherwise(p.error(ERROR.INVALID_VERSION, 'Expected dot')); n('res_http_minor') - .select(MINOR, this.store('http_minor', 'res_http_end')) + .select(MINOR, this.store('http_minor', checkVersion('res_http_end'))) .otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')); n('res_http_end') @@ -365,7 +384,7 @@ export class HTTP { .otherwise(p.error(ERROR.INVALID_VERSION, 'Expected dot')); n('req_http_minor') - .select(MINOR, this.store('http_minor', 'req_http_end')) + .select(MINOR, this.store('http_minor', checkVersion('req_http_end'))) .otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')); n('req_http_end').otherwise(this.load('method', { diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 93d74735..64e9ae97 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -88,11 +88,22 @@ void llhttp__test_init_request_lenient_transfer_encoding(llparse_t* s) { } +void llhttp__test_init_request_lenient_version(llparse_t* s) { + llhttp__test_init_request(s); + s->lenient_flags |= LENIENT_VERSION; +} + + 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_init_response_lenient_version(llparse_t* s) { + llhttp__test_init_response(s); + s->lenient_flags |= LENIENT_VERSION; +} + 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 a9450da8..9ac0fd05 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -11,6 +11,7 @@ import * as llhttp from '../../src/llhttp'; export type TestType = 'request' | 'response' | 'request-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-finish' | 'response-finish' | 'none' | 'url'; @@ -66,7 +67,9 @@ export async function build( ty === 'request-lenient-chunked-length' || ty === 'request-lenient-transfer-encoding' || ty === 'request-lenient-keep-alive' || - ty === 'response-lenient-keep-alive') { + ty === 'response-lenient-keep-alive' || + ty === 'request-lenient-version' || + ty === 'response-lenient-version') { 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 4947ab14..20d9b3d2 100644 --- a/test/md-test.ts +++ b/test/md-test.ts @@ -79,34 +79,30 @@ const http: IFixtureMap = { 'none': buildMode('loose', 'none'), 'request': buildMode('loose', 'request'), 'request-finish': buildMode('loose', 'request-finish'), - 'request-lenient-chunked-length': - buildMode('loose', 'request-lenient-chunked-length'), + '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'), - 'request-lenient-transfer-encoding': - buildMode('loose', 'request-lenient-transfer-encoding'), + 'request-lenient-keep-alive': buildMode( 'loose', 'request-lenient-keep-alive'), + 'request-lenient-transfer-encoding': buildMode('loose', 'request-lenient-transfer-encoding'), + 'request-lenient-version': buildMode( 'loose', 'request-lenient-version'), 'response': buildMode('loose', 'response'), 'response-finish': buildMode('loose', 'response-finish'), - 'response-lenient-keep-alive': buildMode( - 'loose', 'response-lenient-keep-alive'), + 'response-lenient-keep-alive': buildMode( 'loose', 'response-lenient-keep-alive'), + 'response-lenient-version': buildMode( 'loose', 'response-lenient-version'), 'url': buildMode('loose', 'url'), }, strict: { 'none': buildMode('strict', 'none'), 'request': buildMode('strict', 'request'), 'request-finish': buildMode('strict', 'request-finish'), - 'request-lenient-chunked-length': - buildMode('strict', 'request-lenient-chunked-length'), + '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'), - 'request-lenient-transfer-encoding': - buildMode('strict', 'request-lenient-transfer-encoding'), + 'request-lenient-keep-alive': buildMode( 'strict', 'request-lenient-keep-alive'), + 'request-lenient-transfer-encoding': buildMode('strict', 'request-lenient-transfer-encoding'), + 'request-lenient-version': buildMode( 'strict', 'request-lenient-version'), 'response': buildMode('strict', 'response'), 'response-finish': buildMode('strict', 'response-finish'), - 'response-lenient-keep-alive': buildMode( - 'strict', 'response-lenient-keep-alive'), + 'response-lenient-keep-alive': buildMode('strict', 'response-lenient-keep-alive'), + 'response-lenient-version': buildMode( 'strict', 'response-lenient-version'), 'url': buildMode('strict', 'url'), }, }; @@ -171,8 +167,12 @@ function run(name: string): void { types = [ 'request-lenient-keep-alive' ]; } else if (meta.type === 'request-lenient-transfer-encoding') { types = [ 'request-lenient-transfer-encoding' ]; + } else if (meta.type === 'request-lenient-version') { + types = [ 'request-lenient-version' ]; } else if (meta.type === 'response-lenient-keep-alive') { types = [ 'response-lenient-keep-alive' ]; + } else if (meta.type === 'response-lenient-version') { + types = [ 'response-lenient-version' ]; } else if (meta.type === 'response-only') { types = [ 'response' ]; } else if (meta.type === 'request-finish') { @@ -278,6 +278,7 @@ function run(name: string): void { run('request/sample'); run('request/lenient-headers'); +run('request/lenient-version'); run('request/method'); run('request/uri'); run('request/connection'); @@ -292,5 +293,5 @@ run('response/content-length'); run('response/transfer-encoding'); run('response/invalid'); run('response/finish'); - +run('request/lenient-version'); run('url'); diff --git a/test/request/invalid.md b/test/request/invalid.md index c2b6b4ed..2154ddf3 100644 --- a/test/request/invalid.md +++ b/test/request/invalid.md @@ -229,4 +229,18 @@ off=33 len=5 span[header_field]="Dummy" off=39 header_field complete off=40 len=1 span[header_value]="x" off=42 error code=25 reason="Missing expected CR after header value" +``` + +### Invalid HTTP version + + +```http +GET / HTTP/5.6 +``` + +```log +off=0 message begin +off=4 len=1 span[url]="/" +off=6 url complete +off=14 error code=9 reason="Invalid HTTP version" ``` \ No newline at end of file diff --git a/test/request/lenient-version.md b/test/request/lenient-version.md new file mode 100644 index 00000000..7fae4e2b --- /dev/null +++ b/test/request/lenient-version.md @@ -0,0 +1,19 @@ +Lenient HTTP version parsing +============================ + +### Invalid HTTP version with lenient + + +```http +GET / HTTP/5.6 + + +``` + +```log +off=0 message begin +off=4 len=1 span[url]="/" +off=6 url complete +off=18 headers complete method=1 v=5/6 flags=0 content_length=0 +off=18 message complete +``` \ No newline at end of file diff --git a/test/response/invalid.md b/test/response/invalid.md index 60bd3042..bbdc40ba 100644 --- a/test/response/invalid.md +++ b/test/response/invalid.md @@ -106,3 +106,17 @@ off=21 header_field complete off=22 len=1 span[header_value]="1" off=24 error code=3 reason="Missing expected LF after header value" ``` + +### Invalid HTTP version + + +```http +HTTP/5.6 200 OK + + +``` + +```log +off=0 message begin +off=8 error code=9 reason="Invalid HTTP version" +``` \ No newline at end of file diff --git a/test/response/lenient-version.md b/test/response/lenient-version.md new file mode 100644 index 00000000..94642b38 --- /dev/null +++ b/test/response/lenient-version.md @@ -0,0 +1,18 @@ +Lenient HTTP version parsing +============================ + +### Invalid HTTP version with lenient + + +```http +HTTP/5.6 200 OK + + +``` + +```log +off=0 message begin +off=13 len=2 span[status]="OK" +off=17 status complete +off=19 headers complete status=200 v=5/6 flags=0 content_length=0 +```