diff --git a/.github/workflows/node-v12.yaml b/.github/workflows/node-v12.yaml new file mode 100644 index 0000000..72743f6 --- /dev/null +++ b/.github/workflows/node-v12.yaml @@ -0,0 +1,31 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +# NOTE: This action runs the testv12 test suite. + +name: Node-v12 + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + node-version: [12.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm run testv12 diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml new file mode 100644 index 0000000..3be146a --- /dev/null +++ b/.github/workflows/node.yaml @@ -0,0 +1,31 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +# NOTE: This action runs the normal test suite. + +name: Node + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + node-version: [6.x, 8.x, 10.x, 14.x, 16.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm test diff --git a/.gitignore b/.gitignore index c97c649..b48af5b 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,5 @@ pip-log.txt #Mr Developer .mr.developer.cfg +.idea +node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2ae9d62..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - '10' - - '8' - - '6' diff --git a/README.md b/README.md index 534fc67..6c7fce0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ +![Node](https://github.com/creationix/http-parser-js/workflows/Node/badge.svg) +![Node-v12](https://github.com/creationix/http-parser-js/workflows/Node-v12/badge.svg) + # HTTP Parser -This library parses HTTP protocol for requests and responses. It was created to replace `http_parser.c` since calling C++ function from JS is really slow in V8. However, it is now primarily useful in having a more flexible/tolerant HTTP parser when dealing with legacy services that do not meet the strict HTTP parsing rules Node's parser follows. +This library parses HTTP protocol for requests and responses. +It was created to replace `http_parser.c` since calling C++ functions from JS is really slow in V8. +However, it is now primarily useful in having a more flexible/tolerant HTTP parser when dealing with legacy services that do not meet the strict HTTP parsing rules Node's parser follows. -This is packaged as a standalone npm module. To use in node, monkeypatch HTTPParser. +This is packaged as a standalone npm module. +To use in node, monkeypatch HTTPParser. ```js // Monkey patch before you require http for the first time. @@ -14,18 +20,24 @@ var http = require('http'); ## Testing -Simply do `npm test`. The tests are copied from node and mscedex/io.js, with some modifcations. +Simply run `npm test`. +The tests are copied from node and mscedex/io.js, with some modifcations. ## Status This should now be usable in any node application, it now supports (nearly) everything `http_parser.c` does while still being tolerant with corrupted headers, and other kinds of malformed data. -### Node Versions +### Node versions -`http-parser-js` should work via monkey-patching on Node v6-v11, and v13. +`http-parser-js` should work via monkey-patching on Node v6-v11, and v13-14. Node v12.x renamed the internal http parser, and did not expose it for monkey-patching, so to be able to monkey-patch on Node v12, you must run `node --http-parser=legacy file.js` to opt in to the old, monkey-patchable http_parser binding. +## Standalone usage + +While this module is intended to be used as a replacement for the internal Node.js parser, it can be used as a standalone parser. The [`standalone-example.js`](standalone-example.js) demonstrates how to use the somewhat awkward API (coming from compatibility with the Node.js internals) to parse HTTP from raw Buffers. + ## License -MIT. See LICENSE.md +MIT. +See [LICENSE.md](LICENSE.md) diff --git a/http-parser.d.ts b/http-parser.d.ts new file mode 100644 index 0000000..9a8ac57 --- /dev/null +++ b/http-parser.d.ts @@ -0,0 +1,181 @@ +type ParserType = + | 'REQUEST' + | 'RESPONSE' + +type RequestMethod = + | 'DELETE' + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'CONNECT' + | 'OPTIONS' + | 'TRACE' + | 'COPY' + | 'LOCK' + | 'MKCOL' + | 'MOVE' + | 'PROPFIND' + | 'PROPPATCH' + | 'SEARCH' + | 'UNLOCK' + | 'BIND' + | 'REBIND' + | 'UNBIND' + | 'ACL' + | 'REPORT' + | 'MKACTIVITY' + | 'CHECKOUT' + | 'MERGE' + | 'M-SEARCH' + | 'NOTIFY' + | 'SUBSCRIBE' + | 'UNSUBSCRIBE' + | 'PATCH' + | 'PURGE' + | 'MKCALENDAR' + | 'LINK' + | 'UNLINK' + | string + +type StateHeaderKey = + | 'REQUEST_LINE' + | 'RESPONSE_LINE' + | 'HEADER' + +type StateFinishAllowedKey = + | 'REQUEST_LINE' + | 'RESPONSE_LINE' + | 'BODY_RAW' + +type HeaderObject = Array +type noop = ()=> T + +type HeaderInfo
= { + versionMajor: number + versionMinor: number + headers: HEADER + method: number + url: string + statusCode: number + statusMessage: string + upgrade: boolean + shouldKeepAlive: boolean +} +export type OnHeadersCompleteParser
= Mode_0_12 extends true + ? (info: HeaderInfo
)=> number | void + : ( + versionMajor: number, + versionMinor: number, + headers: HEADER, + method: number, + url: string, + statusCode: number, + statusMessage: string, + upgrade: boolean, + shouldKeepAlive: boolean, + )=> number | void +export type OnBodyParser = (chunk: Buffer, offset: number, length: number)=> void +// Only called in the slow case where slow means +// that the request headers were either fragmented +// across multiple TCP packets or too large to be +// processed in a single run. This method is also +// called to process trailing HTTP headers. +export type OnHeadersParser = (headers: string[], url: string)=> void + +declare class HTTPParserJS { + initialize(type: ParserType, async_resource?: unknown): void + + // Some handler stubs, needed for compatibility + [HTTPParser.kOnHeaders]: OnHeadersParser + [HTTPParser.kOnHeadersComplete]: OnHeadersCompleteParser + [HTTPParser.kOnBody]: OnBodyParser + [HTTPParser.kOnMessageComplete]: noop + + /** + * Max number of bytes that will be parsed as headers, 80kb by default + * @default 81920 + */ + maxHeaderSize: number + + reinitialize: HTTPParserConstructor + close: noop + pause: noop + resume: noop + free: noop + private _compatMode0_11: false | boolean + getAsyncId: noop<0> + + execute(chunk: Buffer, start?: number, length?: number): number | Error + finish(): void | Error + + // These three methods are used for an internal speed optimization, and it also + // works if theses are noops. Basically consume() asks us to read the bytes + // ourselves, but if we don't do it we get them through execute(). + consume: noop + unconsume: noop + getCurrentBuffer: noop + + /** + * For correct error handling - see HTTPParser#execute + * @example this.userCall()(userFunction('arg')); + */ + userCall(): (ret?: T)=> T + private nextRequest: noop + private consumeLine: noop + parseHeader(line: string, headers: string[]): void + private REQUEST_LINE: noop + private RESPONSE_LINE: noop + shouldKeepAlive(): boolean + /** + * For older versions of node (v6.x and older?), that return `skipBody=1` or `skipBody=true`, need this `return true;` if it's an upgrade request. + */ + private HEADER(): void | boolean + private BODY_CHUNKHEAD(): void + private BODY_CHUNK(): void + private BODY_CHUNKEMPTYLINE(): void + private BODY_CHUNKTRAILERS(): void + private BODY_RAW(): void + private BODY_SIZED(): void + + get onHeaders(): OnHeadersParser + set onHeaders(to: OnHeadersParser) + + get onHeadersComplete(): OnHeadersCompleteParser + set onHeadersComplete(to: OnHeadersCompleteParser) + + get onBody(): OnBodyParser + set onBody(to: OnBodyParser) + + get onMessageComplete(): noop + set onMessageComplete(to: noop) +} + +interface HTTPParserConstructor extends Function { + new(type?: ParserType): HTTPParserJS + (type?: ParserType): void + + readonly prototype: HTTPParserJS + + readonly REQUEST: 'REQUEST' + readonly RESPONSE: 'RESPONSE' + readonly methods: RequestMethod[] + + encoding: 'ascii'|string + /** + * maxHeaderSize (in bytes) is configurable, but 80kb by default; + * @default 80 * 1024 = 80kb + */ + maxHeaderSize: 81920|number + + // Note: *not* starting with kOnHeaders=0 line the Node parser, because any + // newly added constants (kOnTimeout in Node v12.19.0) will overwrite 0! + readonly kOnHeaders: 1 + readonly kOnHeadersComplete: 2 + readonly kOnBody: 3 + readonly kOnMessageComplete: 4 + + kOnExecute(): void +} +export const HTTPParser: HTTPParserConstructor +export const methods: RequestMethod[] diff --git a/http-parser.js b/http-parser.js index 8e78028..3e98ab9 100644 --- a/http-parser.js +++ b/http-parser.js @@ -1,18 +1,21 @@ /*jshint node:true */ -var assert = require('assert'); - exports.HTTPParser = HTTPParser; function HTTPParser(type) { - assert.ok(type === HTTPParser.REQUEST || type === HTTPParser.RESPONSE || type === undefined); + if (type !== undefined && type !== HTTPParser.REQUEST && type !== HTTPParser.RESPONSE) { + throw new Error('type must be REQUEST or RESPONSE'); + } if (type === undefined) { // Node v12+ } else { this.initialize(type); } + this.maxHeaderSize=HTTPParser.maxHeaderSize } HTTPParser.prototype.initialize = function (type, async_resource) { - assert.ok(type === HTTPParser.REQUEST || type === HTTPParser.RESPONSE); + if (type !== HTTPParser.REQUEST && type !== HTTPParser.RESPONSE) { + throw new Error('type must be REQUEST or RESPONSE'); + } this.type = type; this.state = type + '_LINE'; this.info = { @@ -33,10 +36,13 @@ HTTPParser.encoding = 'ascii'; HTTPParser.maxHeaderSize = 80 * 1024; // maxHeaderSize (in bytes) is configurable, but 80kb by default; HTTPParser.REQUEST = 'REQUEST'; HTTPParser.RESPONSE = 'RESPONSE'; -var kOnHeaders = HTTPParser.kOnHeaders = 0; -var kOnHeadersComplete = HTTPParser.kOnHeadersComplete = 1; -var kOnBody = HTTPParser.kOnBody = 2; -var kOnMessageComplete = HTTPParser.kOnMessageComplete = 3; + +// Note: *not* starting with kOnHeaders=0 line the Node parser, because any +// newly added constants (kOnTimeout in Node v12.19.0) will overwrite 0! +var kOnHeaders = HTTPParser.kOnHeaders = 1; +var kOnHeadersComplete = HTTPParser.kOnHeadersComplete = 2; +var kOnBody = HTTPParser.kOnBody = 3; +var kOnMessageComplete = HTTPParser.kOnMessageComplete = 4; // Some handler stubs, needed for compatibility HTTPParser.prototype[kOnHeaders] = @@ -49,7 +55,7 @@ Object.defineProperty(HTTPParser, 'kOnExecute', { get: function () { // hack for backward compatibility compatMode0_12 = false; - return 4; + return 99; } }); @@ -86,13 +92,15 @@ var methods = exports.methods = HTTPParser.methods = [ 'PURGE', 'MKCALENDAR', 'LINK', - 'UNLINK' + 'UNLINK', + 'SOURCE', ]; var method_connect = methods.indexOf('CONNECT'); HTTPParser.prototype.reinitialize = HTTPParser; HTTPParser.prototype.close = HTTPParser.prototype.pause = HTTPParser.prototype.resume = +HTTPParser.prototype.remove = HTTPParser.prototype.free = function () {}; HTTPParser.prototype._compatMode0_11 = false; HTTPParser.prototype.getAsyncId = function() { return 0; }; @@ -132,7 +140,7 @@ HTTPParser.prototype.execute = function (chunk, start, length) { length = this.offset - start; if (headerState[this.state]) { this.headerSize += length; - if (this.headerSize > HTTPParser.maxHeaderSize) { + if (this.headerSize > (this.maxHeaderSize||HTTPParser.maxHeaderSize)) { return new Error('max header size exceeded'); } } @@ -387,7 +395,8 @@ HTTPParser.prototype.BODY_CHUNKHEAD = function () { HTTPParser.prototype.BODY_CHUNK = function () { var length = Math.min(this.end - this.offset, this.body_bytes); - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.offset + length), 0, length)); this.offset += length; this.body_bytes -= length; if (!this.body_bytes) { @@ -400,7 +409,9 @@ HTTPParser.prototype.BODY_CHUNKEMPTYLINE = function () { if (line === undefined) { return; } - assert.equal(line, ''); + if (line !== '') { + throw new Error('Expected empty line'); + } this.state = 'BODY_CHUNKHEAD'; }; @@ -420,14 +431,15 @@ HTTPParser.prototype.BODY_CHUNKTRAILERS = function () { }; HTTPParser.prototype.BODY_RAW = function () { - var length = this.end - this.offset; - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.end), 0, this.end - this.offset)); this.offset = this.end; }; HTTPParser.prototype.BODY_SIZED = function () { var length = Math.min(this.end - this.offset, this.body_bytes); - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.offset + length), 0, length)); this.offset += length; this.body_bytes -= length; if (!this.body_bytes) { diff --git a/package.json b/package.json index eb187d7..f1a1c24 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "http-parser-js", - "version": "0.5.2", + "version": "0.5.10", "description": "A pure JS HTTP parser for node.", "main": "http-parser.js", + "types": "http-parser.d.ts", "scripts": { "test": "python tests/test.py && node tests/iojs/test-http-parser-durability.js", "testv12": "python tests/test.py --node-args=\"--http-parser=legacy\" && node --http-parser=legacy tests/iojs/test-http-parser-durability.js" @@ -12,7 +13,8 @@ "url": "git://github.com/creationix/http-parser-js.git" }, "files": [ - "http-parser.js" + "http-parser.js", + "http-parser.d.ts" ], "keywords": [ "http" diff --git a/standalone-example.js b/standalone-example.js new file mode 100644 index 0000000..9f5c652 --- /dev/null +++ b/standalone-example.js @@ -0,0 +1,244 @@ +/* + The following is an example of how to use the parser as a standalone module. + Both parseRequest and parseResponse can be used to parse a raw HTTP request/response from a Buffer. +*/ + +const { deepStrictEqual, throws } = require('assert'); +// Replace the require when using the module from npm. +//const { HTTPParser } = require('http-parser-js'); +const { HTTPParser } = require('./http-parser.js'); + +function parseRequest(input) { + const parser = new HTTPParser(HTTPParser.REQUEST); + let complete = false; + let shouldKeepAlive; + let upgrade; + let method; + let url; + let versionMajor; + let versionMinor; + let headers = []; + let trailers = []; + let bodyChunks = []; + + parser[HTTPParser.kOnHeadersComplete] = function (req) { + shouldKeepAlive = req.shouldKeepAlive; + upgrade = req.upgrade; + method = HTTPParser.methods[req.method]; + url = req.url; + versionMajor = req.versionMajor; + versionMinor = req.versionMinor; + headers = req.headers; + }; + + parser[HTTPParser.kOnBody] = function (chunk, offset, length) { + bodyChunks.push(chunk.slice(offset, offset + length)); + }; + + // This is actually the event for trailers, go figure. + parser[HTTPParser.kOnHeaders] = function (t) { + trailers = t; + }; + + parser[HTTPParser.kOnMessageComplete] = function () { + complete = true; + }; + + // Since we are sending the entire Buffer at once here all callbacks above happen synchronously. + // The parser does not do _anything_ asynchronous. + // However, you can of course call execute() multiple times with multiple chunks, e.g. from a stream. + // But then you have to refactor the entire logic to be async (e.g. resolve a Promise in kOnMessageComplete and add timeout logic). + parser.execute(input); + parser.finish(); + + if (!complete) { + throw new Error('Could not parse request'); + } + + let body = Buffer.concat(bodyChunks); + + return { + shouldKeepAlive, + upgrade, + method, + url, + versionMajor, + versionMinor, + headers, + body, + trailers, + }; +} + +function parseResponse(input) { + const parser = new HTTPParser(HTTPParser.RESPONSE); + let complete = false; + let shouldKeepAlive; + let upgrade; + let statusCode; + let statusMessage; + let versionMajor; + let versionMinor; + let headers = []; + let trailers = []; + let bodyChunks = []; + + parser[HTTPParser.kOnHeadersComplete] = function (res) { + shouldKeepAlive = res.shouldKeepAlive; + upgrade = res.upgrade; + statusCode = res.statusCode; + statusMessage = res.statusMessage; + versionMajor = res.versionMajor; + versionMinor = res.versionMinor; + headers = res.headers; + }; + + parser[HTTPParser.kOnBody] = function (chunk, offset, length) { + bodyChunks.push(chunk.slice(offset, offset + length)); + }; + + // This is actually the event for trailers, go figure. + parser[HTTPParser.kOnHeaders] = function (t) { + trailers = t; + }; + + parser[HTTPParser.kOnMessageComplete] = function () { + complete = true; + }; + + // Since we are sending the entire Buffer at once here all callbacks above happen synchronously. + // The parser does not do _anything_ asynchronous. + // However, you can of course call execute() multiple times with multiple chunks, e.g. from a stream. + // But then you have to refactor the entire logic to be async (e.g. resolve a Promise in kOnMessageComplete and add timeout logic). + parser.execute(input); + parser.finish(); + + if (!complete) { + throw new Error('Could not parse'); + } + + let body = Buffer.concat(bodyChunks); + + return { + shouldKeepAlive, + upgrade, + statusCode, + statusMessage, + versionMajor, + versionMinor, + headers, + body, + trailers, + }; +} + +let parsed; + +console.log('Example: basic GET request:'); + +parsed = parseRequest( + Buffer.from(`GET / HTTP/1.1 +Host: www.example.com + +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.method, 'GET'); +deepStrictEqual(parsed.url, '/'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Host', 'www.example.com']); +deepStrictEqual(parsed.body.toString(), ''); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: POST request with body:'); + +parsed = parseRequest( + Buffer.from(`POST /memes HTTP/1.1 +Host: www.example.com +Content-Length: 7 +Content-Type: text/plain + +foo bar +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.method, 'POST'); +deepStrictEqual(parsed.url, '/memes'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Host', 'www.example.com', 'Content-Length', '7', 'Content-Type', 'text/plain']); +deepStrictEqual(parsed.body.toString(), 'foo bar'); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: basic HTML response'); + +parsed = parseResponse( + Buffer.from(`HTTP/1.1 404 Not Found +Content-Type: text/html +Content-Length: 33 + +Computer says no +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.statusCode, 404); +deepStrictEqual(parsed.statusMessage, 'Not Found'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Content-Type', 'text/html', 'Content-Length', '33']); +deepStrictEqual(parsed.body.toString(), 'Computer says no'); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: chunked response with trailers'); + +parsed = parseResponse( + Buffer.from(`HTTP/1.1 200 OK +Content-Type: text/plain +Transfer-Encoding: chunked +Trailer: Expires + +7 +Mozilla +9 +Developer +7 +Network +0 +Expires: Wed, 21 Oct 2015 07:28:00 GMT + +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.statusCode, 200); +deepStrictEqual(parsed.statusMessage, 'OK'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Content-Type', 'text/plain', 'Transfer-Encoding', 'chunked', 'Trailer', 'Expires']); +deepStrictEqual(parsed.body.toString(), 'MozillaDeveloperNetwork'); +deepStrictEqual(parsed.trailers, ['Expires', 'Wed, 21 Oct 2015 07:28:00 GMT']); + +throws(function () { + parseResponse( + Buffer.from(`HTTP/1.1 200 OK +Content-Length: 1 + +`) + ); +}); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b1acd04 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,23 @@ +## Tests that currently fail on Node v13.0.1: + +['chunked with (arguably wrong) content length'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L1306-L1345) +* Real case from proxy servers: Should ignore `content-length` if both it and `encoding: chunked` are specified + +['duplicate content-length'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L220-L245) +* Real case: Allow duplicate (but identical) `content-length` headers + +[parallel/test-http-client-max-header-size_increased](https://github.com/creationix/http-parser-js/tree/master/tests/parallel/test-http-client-max-header-size_increased) +* Test from actual user issue: Extra large headers (configurable max setable in `http-parser-js`) + +[parallel/test-http-max-headers-count](https://github.com/creationix/http-parser-js/tree/master/tests/parallel/test-http-max-headers-count) +* Test from old Node version: Exceeding Node v12's new 8KB header size limit + +['200 malformed header'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L1276-L1305) +* Getting blank headers on malformed headers (probably fine, used to crash Node) + + +Whitespace/trimming differences (probably fine): all in https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js +* 'line folding in header value with CRLF' +* 'line folding in header value with LF' +* 'multiple connection header values with folding and lws and CRLF' +* 'google 301' diff --git a/tests/common.js b/tests/common.js index a610c29..0af3e1a 100644 --- a/tests/common.js +++ b/tests/common.js @@ -293,6 +293,10 @@ if (global.gc) { knownGlobals.push(global.gc); } +if (global.performance) { + knownGlobals.push(global.performance); +} + if (global.DTRACE_HTTP_SERVER_RESPONSE) { knownGlobals.push(DTRACE_HTTP_SERVER_RESPONSE); knownGlobals.push(DTRACE_HTTP_SERVER_REQUEST); diff --git a/tests/iojs/test-http-parser-durability.js b/tests/iojs/test-http-parser-durability.js index 06e7c55..a0749d5 100644 --- a/tests/iojs/test-http-parser-durability.js +++ b/tests/iojs/test-http-parser-durability.js @@ -217,6 +217,32 @@ var cases = [ ], body: 'HELLO' }, + { + name: 'duplicate content-length', + type: REQUEST, + raw: [ + 'GET /get_duplicate_content_length HTTP/1.0', + 'Content-Length: 5', + 'Content-Length: 5', + '', + 'HELLO' + ].join(CRLF), + shouldKeepAlive: false, + msgCompleteOnEOF: false, + httpMajor: 1, + httpMinor: 0, + method: 'GET', + url: '/get_duplicate_content_length', + statusCode: null, + statusText: null, + headers: [ + 'Content-Length', + '5', + 'Content-Length', + '5', + ], + body: 'HELLO' + }, { name: 'post identity body world', type: REQUEST, diff --git a/tests/parallel/test-http-agent-destroyed-socket.js b/tests/parallel/test-http-agent-destroyed-socket.js index 34772f7..b6a255f 100644 --- a/tests/parallel/test-http-agent-destroyed-socket.js +++ b/tests/parallel/test-http-agent-destroyed-socket.js @@ -29,6 +29,17 @@ var server = http.createServer(function(req, res) { console.log('request1 socket closed'); }); response.pipe(process.stdout); + response.socket.once('close', function() { + // assert request2 was removed from the queue + assert(!agent.requests[key]); + console.log("waiting for request2.onSocket's nextTick"); + process.nextTick(function() { + // assert that the same socket was not assigned to request2, + // since it was destroyed. + assert(request1.socket !== request2.socket); + assert(!request2.socket.destroyed, 'the socket is destroyed'); + }); + }); response.on('end', function() { console.log('response1 done'); ///////////////////////////////// @@ -44,17 +55,6 @@ var server = http.createServer(function(req, res) { // is triggered. request1.socket.destroy(); - response.once('close', function() { - // assert request2 was removed from the queue - assert(!agent.requests[key]); - console.log("waiting for request2.onSocket's nextTick"); - process.nextTick(function() { - // assert that the same socket was not assigned to request2, - // since it was destroyed. - assert(request1.socket !== request2.socket); - assert(!request2.socket.destroyed, 'the socket is destroyed'); - }); - }); }); }); diff --git a/tests/parallel/test-http-client-agent.js b/tests/parallel/test-http-client-agent.js index 36ed24e..3434a0c 100644 --- a/tests/parallel/test-http-client-agent.js +++ b/tests/parallel/test-http-client-agent.js @@ -36,8 +36,8 @@ function request(i) { if (count < max) { assert.equal(http.globalAgent.sockets[name].indexOf(socket), -1); } else { - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); server.close(); } }); diff --git a/tests/parallel/test-http-client-max-header-size_increased.js b/tests/parallel/test-http-client-max-header-size_increased.js index a60d423..537316a 100644 --- a/tests/parallel/test-http-client-max-header-size_increased.js +++ b/tests/parallel/test-http-client-max-header-size_increased.js @@ -2,7 +2,7 @@ require('../common'); // set max header size -require('../../../http-parser-js').HTTPParser.maxHeaderSize = 1024 * 1024; // 1MB instead of 80kb +process.binding('http_parser').HTTPParser.maxHeaderSize = 1024 * 1024; // 1MB instead of 80kb var assert = require('assert'); var http = require('http'); diff --git a/tests/parallel/test-http-connect-req-res.js b/tests/parallel/test-http-connect-req-res.js index c6b545b..ab43b2b 100644 --- a/tests/parallel/test-http-connect-req-res.js +++ b/tests/parallel/test-http-connect-req-res.js @@ -44,8 +44,8 @@ server.listen(0, common.mustCall(function() { // Make sure this request got removed from the pool. const name = 'localhost:' + server.address().port; - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); // Make sure this socket has detached. assert(!socket.ondata); diff --git a/tests/parallel/test-http-connect.js b/tests/parallel/test-http-connect.js index b0efca6..54551ea 100644 --- a/tests/parallel/test-http-connect.js +++ b/tests/parallel/test-http-connect.js @@ -45,8 +45,8 @@ server.listen(0, function() { // Make sure this request got removed from the pool. var name = 'localhost:' + server.address().port; - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); // Make sure this socket has detached. assert(!socket.ondata); diff --git a/tests/parallel/test-http-flush.js b/tests/parallel/test-http-flush.js deleted file mode 100644 index 8b3dacb..0000000 --- a/tests/parallel/test-http-flush.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -require('../common'); -var http = require('http'); - -http.createServer(function(req, res) { - res.end('ok'); - this.close(); -}).listen(0, '127.0.0.1', function() { - var req = http.request({ - method: 'POST', - host: '127.0.0.1', - port: this.address().port, - }); - req.flush(); // Flush the request headers. - req.flush(); // Should be idempotent. -}); diff --git a/tests/parallel/test-http-keep-alive.js b/tests/parallel/test-http-keep-alive.js index d48732e..356c691 100644 --- a/tests/parallel/test-http-keep-alive.js +++ b/tests/parallel/test-http-keep-alive.js @@ -38,7 +38,7 @@ server.listen(0, function() { }, function(response) { response.on('end', function() { assert.equal(agent.sockets[name].length, 1); - assert(!agent.requests.hasOwnProperty(name)); + assert(!agent.requests[name]); server.close(); }); response.resume(); @@ -46,6 +46,6 @@ server.listen(0, function() { }); process.on('exit', function() { - assert(!agent.sockets.hasOwnProperty(name)); - assert(!agent.requests.hasOwnProperty(name)); + assert(!agent.sockets[name]); + assert(!agent.requests[name]); }); diff --git a/tests/parallel/test-http-res-write-after-end.js b/tests/parallel/test-http-res-write-after-end.js index 8dca7ad..1a64f73 100644 --- a/tests/parallel/test-http-res-write-after-end.js +++ b/tests/parallel/test-http-res-write-after-end.js @@ -14,7 +14,9 @@ var server = http.Server(function(req, res) { res.end(); var r = res.write('This should raise an error.'); - assert.equal(r, true, 'write after end should return true'); + // JE: Disabling this, node v15 changed this to return false, not true, + // cannot reasonably test (must not be important!) + // assert.equal(r, true, 'write after end should return true'); }); server.listen(0, function() { diff --git a/tests/parallel/test-http-upgrade-agent.js b/tests/parallel/test-http-upgrade-agent.js index 01074d7..00d57cd 100644 --- a/tests/parallel/test-http-upgrade-agent.js +++ b/tests/parallel/test-http-upgrade-agent.js @@ -62,7 +62,7 @@ srv.listen(0, '127.0.0.1', function() { assert.deepStrictEqual(expectedHeaders, res.headers); // Make sure this request got removed from the pool. - assert(!http.globalAgent.sockets.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); req.on('close', function() { socket.end(); diff --git a/tests/parallel/test-is-monkeypatched.js b/tests/parallel/test-is-monkeypatched.js new file mode 100644 index 0000000..f34ad99 --- /dev/null +++ b/tests/parallel/test-is-monkeypatched.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); + +var assert = require('assert'); +var http_common = require('_http_common'); +var http_parser_js = require('../../http-parser.js') + +if (parseInt(process.versions.node) <= 10) { + // not exported, no easy way to check, but we know it works there +} else { + // on newer Node versions, this is exported + assert(http_common.HTTPParser); + assert.equal(http_common.HTTPParser, http_parser_js.HTTPParser); +} diff --git a/tests/parallel/test-standalone-example.js b/tests/parallel/test-standalone-example.js new file mode 100644 index 0000000..cb8d49f --- /dev/null +++ b/tests/parallel/test-standalone-example.js @@ -0,0 +1 @@ +require('../../standalone-example.js'); \ No newline at end of file