diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4fce49e..1e4aa26b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,11 +19,12 @@ jobs: - name: Install clang for Windows if: runner.os == 'Windows' run: | - Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') + iwr -useb get.scoop.sh -outfile 'install.ps1' + .\install.ps1 -RunAsAdmin scoop install llvm --global # Scoop modifies the PATH so we make the modified PATH global. - echo "::set-env name=PATH::$env:PATH" + echo $env:PATH >> $env:GITHUB_PATH - name: Fetch code uses: actions/checkout@v2 @@ -45,7 +46,8 @@ jobs: - name: Build libllhttp.a shell: bash - run: make build/libllhttp.a + run: | + make build/libllhttp.a test: name: Run tests @@ -60,11 +62,12 @@ jobs: - name: Install clang for Windows if: runner.os == 'Windows' run: | - Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') + iwr -useb get.scoop.sh -outfile 'install.ps1' + .\install.ps1 -RunAsAdmin scoop install llvm --global # Scoop modifies the PATH so we make the modified PATH global. - echo "::set-env name=PATH::$env:PATH" + echo $env:PATH >> $env:GITHUB_PATH - name: Fetch code uses: actions/checkout@v2 diff --git a/README.md b/README.md index c95e8d04..beac2805 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Port of [http_parser][0] to [llparse][1]. +**IMPORTANT: The 2.x series is discontinued and not maintained anymore. Update to the latest version of llhttp as soon as possible. The only exception is the 2.1.x series which will be supported until Node.js 14 goes End-Of-Life).** + ## Why? Let's face it, [http_parser][0] is practically unmaintainable. Even diff --git a/package-lock.json b/package-lock.json index 443f152d..cd93b97e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,9 +48,9 @@ "dev": true }, "@types/node": { - "version": "10.17.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.52.tgz", - "integrity": "sha512-bKnO8Rcj03i6JTzweabq96k29uVNcXGB0bkwjVQTFagDgxxNged18281AZ0nTMHl+aFpPPWyPrk4Z3+NtW/z5w==", + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", "dev": true }, "@types/semver": { @@ -65,9 +65,9 @@ "dev": true }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -636,12 +636,22 @@ } }, "llparse": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/llparse/-/llparse-7.1.0.tgz", - "integrity": "sha512-M20oM+rBFvvDGxPkN9FmK84zOfnW2+W8dzp7zt4PAvN8KxXVhH0EaYpdu1uUVO2vonq2aopicDPIa/kxfBDJZQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/llparse/-/llparse-7.1.1.tgz", + "integrity": "sha512-lBxN5O6sKq6KSOaRFIGczoVpO/U/37mHhjJioQbPuiXdfZmwzP1zC3txV9xx778TRNFENzeCM0Uoo+mE1rfJOA==", "requires": { "debug": "^4.2.0", "llparse-frontend": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "llparse-builder": { @@ -652,6 +662,16 @@ "@types/debug": "4.1.5 ", "binary-search": "^1.3.6", "debug": "^4.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "llparse-dot": { @@ -672,12 +692,29 @@ "llparse-builder": "^1.5.2" }, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, + "llparse-builder": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.5.2.tgz", + "integrity": "sha512-i862UNC3YUEdlfK/NUCJxlKjtWjgAI9AJXDRgjcfRHfwFt4Sf8eFPTRsc91/2R9MBZ0kyFdfhi8SVhMsZf1gNQ==", "requires": { - "ms": "^2.1.1" + "@types/debug": "4.1.5 ", + "binary-search": "^1.3.6", + "debug": "^4.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } } } @@ -1149,14 +1186,14 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "string.prototype.trimend": { @@ -1180,12 +1217,12 @@ } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "strip-json-comments": { @@ -1273,9 +1310,9 @@ } }, "typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index 01e0db7d..21b4693b 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,18 @@ "homepage": "https://github.com/nodejs/llhttp#readme", "devDependencies": { "@types/mocha": "^5.2.7", - "@types/node": "^10.17.52", + "@types/node": "^10.17.60", "llparse-dot": "^1.0.1", - "llparse-test-fixture": "^5.0.1", + "llparse-test-fixture": "^5.0.2", "mdgator": "^1.1.2", "mocha": "^7.2.0", "ts-node": "^7.0.1", "tslint": "^5.20.1", - "typescript": "^3.9.9" + "typescript": "^3.9.10" }, "dependencies": { "@types/semver": "^5.5.0", - "llparse": "^7.1.0", + "llparse": "^7.1.1", "semver": "^5.7.1" } } diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index 45c102b9..b3d8ca72 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -6,38 +6,40 @@ export type HTTPMode = 'loose' | 'strict'; export enum ERROR { OK = 0, - INTERNAL, - STRICT, - LF_EXPECTED, - UNEXPECTED_CONTENT_LENGTH, - CLOSED_CONNECTION, - INVALID_METHOD, - INVALID_URL, - INVALID_CONSTANT, - INVALID_VERSION, - INVALID_HEADER_TOKEN, - INVALID_CONTENT_LENGTH, - INVALID_CHUNK_SIZE, - INVALID_STATUS, - INVALID_EOF_STATE, - INVALID_TRANSFER_ENCODING, - - CB_MESSAGE_BEGIN, - CB_HEADERS_COMPLETE, - CB_MESSAGE_COMPLETE, - CB_CHUNK_HEADER, - CB_CHUNK_COMPLETE, - - PAUSED, - PAUSED_UPGRADE, - - USER, + INTERNAL = 1, + STRICT = 2, + CR_EXPECTED = 25, + LF_EXPECTED = 3, + UNEXPECTED_CONTENT_LENGTH = 4, + CLOSED_CONNECTION = 5, + INVALID_METHOD = 6, + INVALID_URL = 7, + INVALID_CONSTANT = 8, + INVALID_VERSION = 9, + INVALID_HEADER_TOKEN = 10, + INVALID_CONTENT_LENGTH = 11, + INVALID_CHUNK_SIZE = 12, + INVALID_STATUS = 13, + INVALID_EOF_STATE = 14, + INVALID_TRANSFER_ENCODING = 15, + + CB_MESSAGE_BEGIN = 16, + CB_HEADERS_COMPLETE = 17, + CB_MESSAGE_COMPLETE = 18, + CB_CHUNK_HEADER = 19, + CB_CHUNK_COMPLETE = 20, + + PAUSED = 21, + PAUSED_UPGRADE = 22, + // PAUSED_H2_UPGRADE = 23 in v6.x + + USER = 24, } export enum TYPE { BOTH = 0, // default - REQUEST, - RESPONSE, + REQUEST = 1, + RESPONSE = 2, } export enum FLAGS { @@ -187,8 +189,8 @@ Object.keys(METHOD_MAP).forEach((key) => { export enum FINISH { SAFE = 0, - SAFE_WITH_CB, - UNSAFE, + SAFE_WITH_CB = 1, + UNSAFE = 2, } // Internal @@ -284,15 +286,15 @@ export const MINOR = MAJOR; export enum HEADER_STATE { GENERAL = 0, - CONNECTION, - CONTENT_LENGTH, - TRANSFER_ENCODING, - UPGRADE, - - CONNECTION_KEEP_ALIVE, - CONNECTION_CLOSE, - CONNECTION_UPGRADE, - TRANSFER_ENCODING_CHUNKED, + CONNECTION = 1, + CONTENT_LENGTH = 2, + TRANSFER_ENCODING = 3, + UPGRADE = 4, + + CONNECTION_KEEP_ALIVE = 5, + CONNECTION_CLOSE = 6, + CONNECTION_UPGRADE = 7, + TRANSFER_ENCODING_CHUNKED = 8, } export const SPECIAL_HEADERS = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index cd0ed332..1e25a337 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -46,6 +46,7 @@ const NODES: ReadonlyArray = [ 'header_field_start', 'header_field', 'header_field_colon', + 'header_field_colon_discard_ws', 'header_field_general', 'header_field_general_otherwise', 'header_value_discard_ws', @@ -55,6 +56,7 @@ const NODES: ReadonlyArray = [ 'header_value', 'header_value_otherwise', 'header_value_lenient', + 'header_value_lenient_failed', 'header_value_lws', 'header_value_te_chunked', 'header_value_te_chunked_last', @@ -372,13 +374,32 @@ export class HTTP { .select(SPECIAL_HEADERS, this.store('header_state', 'header_field_colon')) .otherwise(this.resetHeaderState('header_field_general')); + const onInvalidHeaderFieldChar = + p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header field char'); + + const checkLenientFlagsOnColon = + this.testFlags(FLAGS.LENIENT, { + 1: n('header_field_colon_discard_ws'), + }, span.headerField.end().skipTo(onInvalidHeaderFieldChar)); + n('header_field_colon') - .match(' ', n('header_field_colon')) + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 + // Whitespace character is not allowed between the header field-name + // and colon. If the next token matches whitespace then throw an error. + // + // Add a check for the lenient flag. If the lenient flag is set, the + // whitespace token is allowed to support legacy code not following + // http specs. + .peek(' ', checkLenientFlagsOnColon) .peek(':', span.headerField.end().skipTo(n('header_value_discard_ws'))) // Fallback to general header, there're additional characters: // `Connection-Duration` instead of `Connection` and so on. .otherwise(this.resetHeaderState('header_field_general')); + n('header_field_colon_discard_ws') + .match(' ', n('header_field_colon_discard_ws')) + .otherwise(n('header_field_colon')); + n('header_field_general') .match(this.TOKEN, n('header_field_general')) .otherwise(n('header_field_general_otherwise')); @@ -432,11 +453,27 @@ export class HTTP { FLAGS.CHUNKED, 'header_value_te_chunked'); + // Once chunked has been selected, no other encoding is possible in requests + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1 + const forbidAfterChunkedInRequest = (otherwise: Node) => { + return this.load('type', { + [TYPE.REQUEST]: this.testFlags(FLAGS.LENIENT, { + 0: span.headerValue.end().skipTo( + p.error(ERROR.INVALID_TRANSFER_ENCODING, 'Invalid `Transfer-Encoding` header value'), + ), + }).otherwise(otherwise), + }, otherwise); + }; + n('header_value_start') .otherwise(this.load('header_state', { [HEADER_STATE.UPGRADE]: this.setFlag(FLAGS.UPGRADE, fallback), - [HEADER_STATE.TRANSFER_ENCODING]: this.setFlag( - FLAGS.TRANSFER_ENCODING, toTransferEncoding), + [HEADER_STATE.TRANSFER_ENCODING]: this.testFlags( + FLAGS.CHUNKED, + { + 1: forbidAfterChunkedInRequest(this.setFlag(FLAGS.TRANSFER_ENCODING, toTransferEncoding)), + }, + this.setFlag(FLAGS.TRANSFER_ENCODING, toTransferEncoding)), [HEADER_STATE.CONTENT_LENGTH]: n('header_value_content_length_once'), [HEADER_STATE.CONNECTION]: n('header_value_connection'), }, 'header_value')); @@ -458,7 +495,8 @@ export class HTTP { .peek([ '\r', '\n' ], this.update('header_state', HEADER_STATE.TRANSFER_ENCODING_CHUNKED, 'header_value_otherwise')) - .otherwise(n('header_value_te_chunked')); + .peek(',', forbidAfterChunkedInRequest(n('header_value_te_chunked'))) + .otherwise(n('header_value_te_token')); n('header_value_te_token') .match(',', n('header_value_te_token_ows')) @@ -543,11 +581,10 @@ export class HTTP { const checkLenient = this.testFlags(FLAGS.LENIENT, { 1: n('header_value_lenient'), - }, p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char')); + }, n('header_value_lenient_failed')); 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'))) .otherwise(checkLenient); n('header_value_lenient') @@ -555,6 +592,12 @@ 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, @@ -702,10 +745,11 @@ export class HTTP { .otherwise(p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size')); - // just ignore this. n('chunk_parameters') .match('\r', n('chunk_size_almost_done')) - .skipTo(n('chunk_parameters')); + .match(HEADER_CHARS, n('chunk_parameters')) + .otherwise(p.error(ERROR.STRICT, + 'Invalid character in chunk parameters')); if (this.mode === 'strict') { n('chunk_size_almost_done') diff --git a/test/request/connection.md b/test/request/connection.md index f6e4007e..f2b69c7a 100644 --- a/test/request/connection.md +++ b/test/request/connection.md @@ -348,6 +348,52 @@ off=78 message complete off=78 error code=22 reason="Pause on CONNECT/Upgrade" ``` +### Invalid whitespace token with `Connection` header field + + +```http +PUT /url HTTP/1.1 +Connection : upgrade +Content-Length: 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=30 error code=10 reason="Invalid header field char" +``` + +### Invalid whitespace token with `Connection` header field (lenient) + + +```http +PUT /url HTTP/1.1 +Connection : upgrade +Content-Length: 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=11 span[header_field]="Connection " +off=32 len=7 span[header_value]="upgrade" +off=41 len=14 span[header_field]="Content-Length" +off=57 len=1 span[header_value]="4" +off=60 len=7 span[header_field]="Upgrade" +off=69 len=2 span[header_value]="ws" +off=75 headers complete method=4 v=1/1 flags=134 content_length=4 +off=75 len=4 span[body]="abcd" +off=79 message complete +off=79 error code=22 reason="Pause on CONNECT/Upgrade" +``` + ## `upgrade` ### Setting a flag and pausing diff --git a/test/request/content-length.md b/test/request/content-length.md index 80c2f646..a4cad060 100644 --- a/test/request/content-length.md +++ b/test/request/content-length.md @@ -151,6 +151,54 @@ off=57 len=8 span[header_value]="identity" off=69 headers complete method=4 v=1/1 flags=320 content_length=1 ``` +## Invalid whitespace token with `Content-Length` header field + + +```http +PUT /url HTTP/1.1 +Connection: upgrade +Content-Length : 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=31 len=7 span[header_value]="upgrade" +off=40 len=14 span[header_field]="Content-Length" +off=55 error code=10 reason="Invalid header field char" +``` + +## Invalid whitespace token with `Content-Length` header field (lenient) + + +```http +PUT /url HTTP/1.1 +Connection: upgrade +Content-Length : 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=31 len=7 span[header_value]="upgrade" +off=40 len=15 span[header_field]="Content-Length " +off=57 len=1 span[header_value]="4" +off=60 len=7 span[header_field]="Upgrade" +off=69 len=2 span[header_value]="ws" +off=75 headers complete method=4 v=1/1 flags=134 content_length=4 +off=75 len=4 span[body]="abcd" +off=79 message complete +off=79 error code=22 reason="Pause on CONNECT/Upgrade" +``` + ## Funky `Content-Length` with body diff --git a/test/request/invalid.md b/test/request/invalid.md index 93aae5e5..42c7500d 100644 --- a/test/request/invalid.md +++ b/test/request/invalid.md @@ -185,3 +185,26 @@ off=16 len=4 span[header_field]="Host" off=22 len=15 span[header_value]="www.example.com" off=52 error code=10 reason="Invalid header token" ``` + +### Missing CR between headers + + + +```http +GET / HTTP/1.1 +Host: localhost +Dummy: x\nContent-Length: 23 +GET / HTTP/1.1 +Dummy: GET /admin HTTP/1.1 +Host: localhost +``` + +```log +off=0 message begin +off=4 len=1 span[url]="/" +off=16 len=4 span[header_field]="Host" +off=22 len=9 span[header_value]="localhost" +off=33 len=5 span[header_field]="Dummy" +off=40 len=1 span[header_value]="x" +off=42 error code=25 reason="Missing expected CR after header value" +``` \ No newline at end of file diff --git a/test/request/sample.md b/test/request/sample.md index 184255a7..2fd12246 100644 --- a/test/request/sample.md +++ b/test/request/sample.md @@ -298,21 +298,7 @@ off=0 message begin off=4 len=1 span[url]="/" off=15 len=5 span[header_field]="Line1" off=24 len=3 span[header_value]="abc" -off=28 len=4 span[header_value]="\tdef" -off=33 len=4 span[header_value]=" ghi" -off=38 len=5 span[header_value]="\t\tjkl" -off=44 len=6 span[header_value]=" mno " -off=51 len=6 span[header_value]="\t \tqrs" -off=58 len=5 span[header_field]="Line2" -off=67 len=6 span[header_value]="line2\t" -off=74 len=5 span[header_field]="Line3" -off=82 len=5 span[header_value]="line3" -off=88 len=5 span[header_field]="Line4" -off=98 len=0 span[header_value]="" -off=98 len=10 span[header_field]="Connection" -off=111 len=5 span[header_value]="close" -off=118 headers complete method=1 v=1/1 flags=2 content_length=0 -off=118 message complete +off=28 error code=25 reason="Missing expected CR after header value" ``` ## Request starting with CRLF diff --git a/test/request/transfer-encoding.md b/test/request/transfer-encoding.md index 9b81ef9d..1900ab33 100644 --- a/test/request/transfer-encoding.md +++ b/test/request/transfer-encoding.md @@ -253,7 +253,7 @@ off=112 len=1 span[header_value]="5" off=117 error code=4 reason="Content-Length can't be present with Transfer-Encoding" ``` -## POST with `Transfer-Encoding` (that is not chunked) and `Content-Length` (lenient) +## POST with `Transfer-Encoding` and `Content-Length` (lenient) TODO(indutny): should we allow it even in lenient mode? (Consider disabling this). @@ -301,9 +301,8 @@ off=5 len=38 span[url]="/post_identity_body_world?q=search#hey" off=54 len=6 span[header_field]="Accept" off=62 len=3 span[header_value]="*/*" off=67 len=17 span[header_field]="Transfer-Encoding" -off=86 len=16 span[header_value]="chunked, deflate" -off=106 headers complete method=3 v=1/1 flags=200 content_length=0 -off=106 error code=15 reason="Request has invalid `Transfer-Encoding`" +off=86 len=7 span[header_value]="chunked" +off=94 error code=15 reason="Invalid `Transfer-Encoding` header value" ``` ## POST with `chunked` and duplicate transfer-encoding @@ -326,16 +325,12 @@ off=62 len=3 span[header_value]="*/*" off=67 len=17 span[header_field]="Transfer-Encoding" off=86 len=7 span[header_value]="chunked" off=95 len=17 span[header_field]="Transfer-Encoding" -off=114 len=7 span[header_value]="deflate" -off=125 headers complete method=3 v=1/1 flags=200 content_length=0 -off=125 error code=15 reason="Request has invalid `Transfer-Encoding`" +off=114 len=0 span[header_value]="" +off=115 error code=15 reason="Invalid `Transfer-Encoding` header value" ``` ## POST with `chunked` before other transfer-coding (lenient) -TODO(indutny): should we allow it even in lenient mode? (Consider disabling -this). - ```http POST /post_identity_body_world?q=search#hey HTTP/1.1 @@ -356,7 +351,32 @@ off=106 headers complete method=3 v=1/1 flags=300 content_length=0 off=106 len=5 span[body]="World" ``` -## POST with `chunked` as last transfer-coding +## POST with `chunked` and duplicate transfer-encoding (lenient) + + +```http +POST /post_identity_body_world?q=search#hey HTTP/1.1 +Accept: */* +Transfer-Encoding: chunked +Transfer-Encoding: deflate + +World +``` + +```log +off=0 message begin +off=5 len=38 span[url]="/post_identity_body_world?q=search#hey" +off=54 len=6 span[header_field]="Accept" +off=62 len=3 span[header_value]="*/*" +off=67 len=17 span[header_field]="Transfer-Encoding" +off=86 len=7 span[header_value]="chunked" +off=95 len=17 span[header_field]="Transfer-Encoding" +off=114 len=7 span[header_value]="deflate" +off=125 headers complete method=3 v=1/1 flags=300 content_length=0 +off=125 len=5 span[body]="World" +``` + +## POST with `chunked` as last transfer-encoding ```http @@ -387,6 +407,66 @@ off=121 chunk complete off=121 message complete ``` +## POST with `chunked` as last transfer-encoding (multiple headers) + + +```http +POST /post_identity_body_world?q=search#hey HTTP/1.1 +Accept: */* +Transfer-Encoding: deflate +Transfer-Encoding: chunked + +5 +World +0 + + +``` + +```log +off=0 message begin +off=5 len=38 span[url]="/post_identity_body_world?q=search#hey" +off=54 len=6 span[header_field]="Accept" +off=62 len=3 span[header_value]="*/*" +off=67 len=17 span[header_field]="Transfer-Encoding" +off=86 len=7 span[header_value]="deflate" +off=95 len=17 span[header_field]="Transfer-Encoding" +off=114 len=7 span[header_value]="chunked" +off=125 headers complete method=3 v=1/1 flags=208 content_length=0 +off=128 chunk header len=5 +off=128 len=5 span[body]="World" +off=135 chunk complete +off=138 chunk header len=0 +off=140 chunk complete +off=140 message complete +``` + +## POST with `chunkedchunked` as transfer-encoding + + +```http +POST /post_identity_body_world?q=search#hey HTTP/1.1 +Accept: */* +Transfer-Encoding: chunkedchunked + +5 +World +0 + + +``` + +```log +off=0 message begin +off=5 len=38 span[url]="/post_identity_body_world?q=search#hey" +off=54 len=6 span[header_field]="Accept" +off=62 len=3 span[header_value]="*/*" +off=67 len=17 span[header_field]="Transfer-Encoding" +off=86 len=14 span[header_value]="chunkedchunked" +off=104 headers complete method=3 v=1/1 flags=200 content_length=0 +off=104 error code=15 reason="Request has invalid `Transfer-Encoding`" +``` + ## Missing last-chunk @@ -411,3 +491,25 @@ off=52 len=3 span[body]="foo" off=57 chunk complete off=57 error code=12 reason="Invalid character in chunk size" ``` + +## Validate chunk parameters + + +```http +PUT /url HTTP/1.1 +Transfer-Encoding: chunked + +3 \n \r\n\ +foo + + +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=17 span[header_field]="Transfer-Encoding" +off=38 len=7 span[header_value]="chunked" +off=49 headers complete method=4 v=1/1 flags=208 content_length=0 +off=51 error code=2 reason="Invalid character in chunk parameters" +``` diff --git a/test/response/sample.md b/test/response/sample.md index 1804fdb5..2143fb00 100644 --- a/test/response/sample.md +++ b/test/response/sample.md @@ -225,10 +225,7 @@ off=0 message begin off=13 len=2 span[status]="OK" off=16 len=12 span[header_field]="Content-Type" off=30 len=24 span[header_value]="text/html; charset=utf-8" -off=55 len=10 span[header_field]="Connection" -off=67 len=5 span[header_value]="close" -off=74 headers complete status=200 v=1/1 flags=2 content_length=0 -off=74 len=51 span[body]="these headers are from http://news.ycombinator.com/" +off=55 error code=25 reason="Missing expected CR after header value" ``` ## Underscore in header key diff --git a/test/response/transfer-encoding.md b/test/response/transfer-encoding.md index 1a52253b..9797be21 100644 --- a/test/response/transfer-encoding.md +++ b/test/response/transfer-encoding.md @@ -43,7 +43,7 @@ off=159 chunk complete off=159 message complete ``` -## `chunked` before other transfer-coding +### `chunked` before other transfer-encoding ```http @@ -64,3 +64,66 @@ off=49 len=16 span[header_value]="chunked, deflate" off=69 headers complete status=200 v=1/1 flags=200 content_length=0 off=69 len=5 span[body]="World" ``` + +### multiple transfer-encoding where chunked is not the last one + + +```http +HTTP/1.1 200 OK +Accept: */* +Transfer-Encoding: chunked +Transfer-Encoding: identity + +World +``` + +```log +off=0 message begin +off=13 len=2 span[status]="OK" +off=17 len=6 span[header_field]="Accept" +off=25 len=3 span[header_value]="*/*" +off=30 len=17 span[header_field]="Transfer-Encoding" +off=49 len=7 span[header_value]="chunked" +off=58 len=17 span[header_field]="Transfer-Encoding" +off=77 len=8 span[header_value]="identity" +off=89 headers complete status=200 v=1/1 flags=200 content_length=0 +off=89 len=5 span[body]="World" +``` + +### `chunkedchunked` transfer-encoding does not enable chunked enconding + +This check that the word `chunked` repeat more than once (with or without spaces) does not mistakenly enables chunked encoding. + + +```http +HTTP/1.1 200 OK +Accept: */* +Transfer-Encoding: chunkedchunked + +2 +OK +0 + + +``` + +```log +off=0 message begin +off=13 len=2 span[status]="OK" +off=17 len=6 span[header_field]="Accept" +off=25 len=3 span[header_value]="*/*" +off=30 len=17 span[header_field]="Transfer-Encoding" +off=49 len=14 span[header_value]="chunkedchunked" +off=67 headers complete status=200 v=1/1 flags=200 content_length=0 +off=67 len=1 span[body]="2" +off=68 len=1 span[body]=cr +off=69 len=1 span[body]=lf +off=70 len=2 span[body]="OK" +off=72 len=1 span[body]=cr +off=73 len=1 span[body]=lf +off=74 len=1 span[body]="0" +off=75 len=1 span[body]=cr +off=76 len=1 span[body]=lf +off=77 len=1 span[body]=cr +off=78 len=1 span[body]=lf +``` \ No newline at end of file