From 9bd4b0b2398274eac93803288bf047f649085bbf Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 00:52:27 +0100 Subject: [PATCH 1/8] use parser.execute --- lib/client-request.js | 13 +++++++------ lib/core/client.js | 31 ++++++++++++++++++------------- test/socket-handle-error.js | 2 +- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/client-request.js b/lib/client-request.js index 7c65b76cb37..5c438bd0fad 100644 --- a/lib/client-request.js +++ b/lib/client-request.js @@ -112,13 +112,14 @@ class RequestHandler extends AsyncResource { onComplete (trailers) { const { res } = this - removeSignal(this) - - if (trailers) { - util.parseHeaders(trailers, this.trailers) - } + util.queueMicrotask(() => { + removeSignal(this) - res.push(null) + if (trailers) { + util.parseHeaders(trailers, this.trailers) + } + res.push(null) + }) } onError (err) { diff --git a/lib/core/client.js b/lib/core/client.js index 91f97a1c817..6c3883391e8 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -423,6 +423,8 @@ class Parser extends HTTPParser { socketPause(this.socket) } + + socket.on('data', onSocketData) } [HTTPParser.kOnHeaders] (rawHeaders) { @@ -439,7 +441,7 @@ class Parser extends HTTPParser { } } - [HTTPParser.kOnExecute] (ret) { + [HTTPParser.kOnExecute] (ret, currentBuffer) { if (this.paused) { this.queue.push([this[HTTPParser.kOnExecute], ret]) return @@ -480,11 +482,12 @@ class Parser extends HTTPParser { // Reset socket state to non flowing: socket._readableState.flowing = null - socket.unshift(this.getCurrentBuffer().slice(ret)) + socket.unshift(currentBuffer.slice(ret)) try { if (!socket.destroyed && !request.aborted) { detachSocket(socket) + client[kSocket] = null client[kQueue][client[kRunningIdx]++] = null @@ -552,7 +555,6 @@ class Parser extends HTTPParser { this.request = request if (request.upgrade) { - this.unconsume() this.upgrade = true return 2 } @@ -724,7 +726,6 @@ class Parser extends HTTPParser { destroy () { clearTimeout(this.timeout) this.timeout = null - this.unconsume() setImmediate((self) => self.close(), this) } } @@ -770,6 +771,18 @@ function onSocketError (err) { } } +function onSocketData (data) { + const { [kParser]: parser } = this + + let ret + try { + ret = parser.execute(data) + } catch (err) { + ret = err + } + parser[HTTPParser.kOnExecute](ret, data) +} + function onSocketEnd () { util.destroy(this, new SocketError('other side closed')) } @@ -788,6 +801,7 @@ function detachSocket (socket) { .removeListener('session', onSocketSession) .removeListener('error', onSocketError) .removeListener('end', onSocketEnd) + .removeListener('data', onSocketData) .removeListener('close', onSocketClose) } @@ -863,15 +877,6 @@ function connect (client) { const parser = new Parser(client, socket) - /* istanbul ignore next */ - if (nodeMajorVersion >= 12) { - assert(socket._handle) - parser.consume(socket._handle) - } else { - assert(socket._handle && socket._handle._externalStream) - parser.consume(socket._handle._externalStream) - } - socket[kIdleTimeout] = null socket[kIdleTimeoutValue] = null socket[kWriting] = false diff --git a/test/socket-handle-error.js b/test/socket-handle-error.js index 951a03ea97f..a2558ba275e 100644 --- a/test/socket-handle-error.js +++ b/test/socket-handle-error.js @@ -59,7 +59,7 @@ test('resume error', (t) => { client[kSocket]._handle.readStart = () => -100 data.body.on('error', (err) => { - t.strictEqual(err.code, -100) + t.strictEqual(err.code, 'EPROTO') }) setTimeout(() => { From 08ebbedb178d8651ab4bc0c91ceb45fd6f540e5c Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 01:22:36 +0100 Subject: [PATCH 2/8] fixup --- lib/core/client.js | 24 ++----------- test/socket-handle-error.js | 72 ------------------------------------- 2 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 test/socket-handle-error.js diff --git a/lib/core/client.js b/lib/core/client.js index 6c3883391e8..17f92c87001 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -411,7 +411,7 @@ class Parser extends HTTPParser { } this.resuming = false - socketResume(this.socket) + this.socket.resume() } this._pause = () => { @@ -421,7 +421,7 @@ class Parser extends HTTPParser { this.paused = true - socketPause(this.socket) + this.socket.pause() } socket.on('data', onSocketData) @@ -892,26 +892,6 @@ function connect (client) { .on('close', onSocketClose) } -function socketPause (socket) { - if (socket._handle && socket._handle.reading) { - socket._handle.reading = false - const err = socket._handle.readStop() - if (err) { - socket.destroy(util.errnoException(err, 'read')) - } - } -} - -function socketResume (socket) { - if (socket._handle && !socket._handle.reading) { - socket._handle.reading = true - const err = socket._handle.readStart() - if (err) { - socket.destroy(util.errnoException(err, 'read')) - } - } -} - function emitDrain (client) { client[kNeedDrain] = 0 client.emit('drain') diff --git a/test/socket-handle-error.js b/test/socket-handle-error.js deleted file mode 100644 index a2558ba275e..00000000000 --- a/test/socket-handle-error.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { Client } = require('..') -const { createServer } = require('http') -const { kSocket } = require('../lib/core/symbols') - -test('stop error', (t) => { - t.plan(2) - - const server = createServer((req, res) => { - while (res.write(Buffer.alloc(4096))) { - } - }) - t.tearDown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.tearDown(client.destroy.bind(client)) - - makeRequest() - - client.once('connect', () => { - client[kSocket]._handle.readStop = () => -100 - }) - - function makeRequest () { - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) - data.body.on('error', (err) => { - t.strictEqual(err.code, -100) - }) - }) - return client.size <= client.pipelining - } - }) -}) - -test('resume error', (t) => { - t.plan(2) - - const server = createServer((req, res) => { - while (res.write(Buffer.alloc(4096))) { - } - }) - t.tearDown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.tearDown(client.destroy.bind(client)) - - makeRequest() - - function makeRequest () { - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) - data.body.pause() - - client[kSocket]._handle.readStart = () => -100 - - data.body.on('error', (err) => { - t.strictEqual(err.code, 'EPROTO') - }) - - setTimeout(() => { - data.body.resume() - }, 100) - }) - return client.size <= client.pipelining - } - }) -}) From 723afc577e6d16aa1dc661cbcd7b92aa1520b195 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 01:41:00 +0100 Subject: [PATCH 3/8] fixup --- lib/client-request.js | 13 ++++++------- lib/core/client.js | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/client-request.js b/lib/client-request.js index 5c438bd0fad..7c65b76cb37 100644 --- a/lib/client-request.js +++ b/lib/client-request.js @@ -112,14 +112,13 @@ class RequestHandler extends AsyncResource { onComplete (trailers) { const { res } = this - util.queueMicrotask(() => { - removeSignal(this) + removeSignal(this) - if (trailers) { - util.parseHeaders(trailers, this.trailers) - } - res.push(null) - }) + if (trailers) { + util.parseHeaders(trailers, this.trailers) + } + + res.push(null) } onError (err) { diff --git a/lib/core/client.js b/lib/core/client.js index 17f92c87001..96f238caebc 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -487,7 +487,6 @@ class Parser extends HTTPParser { try { if (!socket.destroyed && !request.aborted) { detachSocket(socket) - client[kSocket] = null client[kQueue][client[kRunningIdx]++] = null @@ -780,6 +779,7 @@ function onSocketData (data) { } catch (err) { ret = err } + parser[HTTPParser.kOnExecute](ret, data) } From aa7531ca5b827d687053844906d660f2675bdd28 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 11:24:20 +0100 Subject: [PATCH 4/8] fixup --- lib/core/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/client.js b/lib/core/client.js index 96f238caebc..b808d588f46 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -443,7 +443,7 @@ class Parser extends HTTPParser { [HTTPParser.kOnExecute] (ret, currentBuffer) { if (this.paused) { - this.queue.push([this[HTTPParser.kOnExecute], ret]) + this.queue.push([this[HTTPParser.kOnExecute], ret, currentBuffer]) return } @@ -487,6 +487,7 @@ class Parser extends HTTPParser { try { if (!socket.destroyed && !request.aborted) { detachSocket(socket) + client[kSocket] = null client[kQueue][client[kRunningIdx]++] = null @@ -779,7 +780,6 @@ function onSocketData (data) { } catch (err) { ret = err } - parser[HTTPParser.kOnExecute](ret, data) } From 5de2a3cefc49c42982cd9935296d050345f28eb7 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 12:04:11 +0100 Subject: [PATCH 5/8] fixup --- lib/core/client.js | 120 ++++------------------------------- lib/node/http-parser.js | 135 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 110 deletions(-) diff --git a/lib/core/client.js b/lib/core/client.js index b808d588f46..0ba2385fa6f 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -54,11 +54,6 @@ const { kBodyTimeout } = require('./symbols') -const nodeVersions = process.version.split('.') -const nodeMajorVersion = parseInt(nodeVersions[0].slice(1)) -const nodeMinorVersion = parseInt(nodeVersions[1]) -const insecureHTTPParser = process.execArgv.includes('--insecure-http-parser') - function getServerName (client, host) { return ( util.getServerName(host) || @@ -347,34 +342,9 @@ class Client extends EventEmitter { class Parser extends HTTPParser { constructor (client, socket) { - /* istanbul ignore next */ - if (nodeMajorVersion === 12 && nodeMinorVersion < 19) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - 0 - ) - } else if (nodeMajorVersion === 12 && nodeMinorVersion >= 19) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - client[kMaxHeadersSize], - 0 - ) - } else if (nodeMajorVersion > 12) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - client[kMaxHeadersSize], - insecureHTTPParser, - 0 - ) - } else { - super(HTTPParser.RESPONSE, false) - } + super({ + maxHeadersSize: client[kMaxHeadersSize] + }) this.client = client this.socket = socket @@ -386,54 +356,20 @@ class Parser extends HTTPParser { this.request = null this.paused = false - // Parser can't be paused from within a callback. - // Use a buffer in JS land in order to stop further progress while paused. - this.resuming = false - this.queue = [] - this._resume = () => { - if (!this.paused || this.resuming) { - return - } - - this.paused = false - - this.resuming = true - while (this.queue.length) { - const [fn, ...args] = this.queue.shift() - - Reflect.apply(fn, this, args) - - if (this.paused) { - this.resuming = false - return - } - } - this.resuming = false - + this.resume() this.socket.resume() } this._pause = () => { - if (this.paused) { - return - } - - this.paused = true - + this.pause() this.socket.pause() } socket.on('data', onSocketData) } - [HTTPParser.kOnHeaders] (rawHeaders) { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([this[HTTPParser.kOnHeaders], rawHeaders]) - return - } - + onHeaders (rawHeaders) { if (this.headers) { Array.prototype.push.apply(this.headers, rawHeaders) } else { @@ -441,12 +377,7 @@ class Parser extends HTTPParser { } } - [HTTPParser.kOnExecute] (ret, currentBuffer) { - if (this.paused) { - this.queue.push([this[HTTPParser.kOnExecute], ret, currentBuffer]) - return - } - + onExecute (ret, currentBuffer) { const { upgrade, socket } = this if (!Number.isFinite(ret)) { @@ -504,15 +435,7 @@ class Parser extends HTTPParser { } } - [HTTPParser.kOnHeadersComplete] (versionMajor, versionMinor, rawHeaders, method, - url, statusCode, statusMessage, upgrade, shouldKeepAlive) { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([this[HTTPParser.kOnHeadersComplete], versionMajor, versionMinor, rawHeaders, method, - url, statusCode, statusMessage, upgrade, shouldKeepAlive]) - return - } - + onHeadersComplete (rawHeaders, statusCode, statusMessage, upgrade, shouldKeepAlive) { const { client, socket } = this const request = client[kQueue][client[kRunningIdx]] @@ -611,12 +534,7 @@ class Parser extends HTTPParser { return request.method === 'HEAD' || statusCode < 200 ? 1 : 0 } - [HTTPParser.kOnBody] (chunk, offset, length) { - if (this.paused) { - this.queue.push([this[HTTPParser.kOnBody], chunk, offset, length]) - return - } - + onBody (chunk, offset, length) { const { socket, statusCode, request, timeout } = this if (socket.destroyed) { @@ -638,13 +556,7 @@ class Parser extends HTTPParser { } } - [HTTPParser.kOnMessageComplete] () { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([this[HTTPParser.kOnMessageComplete]]) - return - } - + onMessageComplete () { const { client, socket, statusCode, headers, upgrade, request, trailers } = this if (socket.destroyed) { @@ -726,7 +638,7 @@ class Parser extends HTTPParser { destroy () { clearTimeout(this.timeout) this.timeout = null - setImmediate((self) => self.close(), this) + super.destroy() } } @@ -772,15 +684,7 @@ function onSocketError (err) { } function onSocketData (data) { - const { [kParser]: parser } = this - - let ret - try { - ret = parser.execute(data) - } catch (err) { - ret = err - } - parser[HTTPParser.kOnExecute](ret, data) + this[kParser].execute(data) } function onSocketEnd () { diff --git a/lib/node/http-parser.js b/lib/node/http-parser.js index 0039a55559c..7345014d9ad 100644 --- a/lib/node/http-parser.js +++ b/lib/node/http-parser.js @@ -3,9 +3,140 @@ // TODO: This is not really allowed by Node but it works for now. const common = require('_http_common') +let NodeHTTPParser if (common.HTTPParser) { - module.exports = common.HTTPParser + NodeHTTPParser = common.HTTPParser } else { // Node 10 - module.exports = process.binding('http_parser').HTTPParser // eslint-disable-line + NodeHTTPParser = process.binding('http_parser').HTTPParser // eslint-disable-line +} + +const nodeVersions = process.version.split('.') +const nodeMajorVersion = parseInt(nodeVersions[0].slice(1)) +const nodeMinorVersion = parseInt(nodeVersions[1]) +const insecureHTTPParser = process.execArgv.includes('--insecure-http-parser') + +module.exports = class HTTPParser { + constructor ({ maxHeadersSize }) { + /* istanbul ignore next */ + if (nodeMajorVersion === 12 && nodeMinorVersion < 19) { + this.parser = new NodeHTTPParser() + this.parser.initialize( + NodeHTTPParser.RESPONSE, + {}, + 0 + ) + } else if (nodeMajorVersion === 12 && nodeMinorVersion >= 19) { + this.parser = new NodeHTTPParser() + this.parser.initialize( + NodeHTTPParser.RESPONSE, + {}, + maxHeadersSize, + 0 + ) + } else if (nodeMajorVersion > 12) { + this.parser = new NodeHTTPParser() + this.parser.initialize( + NodeHTTPParser.RESPONSE, + {}, + maxHeadersSize, + insecureHTTPParser, + 0 + ) + } else { + this.parser = new NodeHTTPParser(NodeHTTPParser.RESPONSE, false) + } + + this.parser[NodeHTTPParser.kOnHeaders] = function (rawHeaders) { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([NodeHTTPParser.kOnHeaders, rawHeaders]) + return + } + this.onHeaders(rawHeaders) + } + + this.parser[NodeHTTPParser.kOnExecute] = (ret, currentBuffer) => { + if (this.paused) { + this.queue.push([NodeHTTPParser.kOnExecute, ret, currentBuffer]) + return + } + this.onExecute(ret, currentBuffer) + } + + this.parser[NodeHTTPParser.kOnHeadersComplete] = (versionMajor, versionMinor, rawHeaders, method, + url, statusCode, statusMessage, upgrade, shouldKeepAlive) => { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([NodeHTTPParser.kOnHeadersComplete, versionMajor, versionMinor, rawHeaders, method, + url, statusCode, statusMessage, upgrade, shouldKeepAlive]) + return // TODO (fix): What do we return here? + } + return this.onHeadersComplete(rawHeaders, statusCode, statusMessage, upgrade, shouldKeepAlive) + } + + this.parser[NodeHTTPParser.kOnBody] = (chunk, offset, length) => { + if (this.paused) { + this.queue.push([NodeHTTPParser.kOnBody, chunk, offset, length]) + return + } + this.onBody(chunk, offset, length) + } + + this.parser[NodeHTTPParser.kOnMessageComplete] = () => { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([NodeHTTPParser.kOnMessageComplete]) + return + } + this.onMessageComplete() + } + + // Parser can't be paused from within a callback. + // Use a buffer in JS land in order to stop further progress while paused. + this.resuming = false + this.queue = [] + this.paused = false + } + + resume () { + if (!this.paused || this.resuming) { + return + } + + this.paused = false + + this.resuming = true + while (this.queue.length) { + const [key, ...args] = this.queue.shift() + + this[key](...args) + + if (this.paused) { + this.resuming = false + return + } + } + this.resuming = false + } + + pause () { + this.paused = true + } + + execute (data) { + let ret + try { + ret = this.parser.execute(data) + } catch (err) { + ret = err + } + + this.parser[NodeHTTPParser.kOnExecute](ret, data) + } + + destroy () { + setImmediate((parser) => parser.close(), this.parser) + this.parser = null + } } From 361b560d85625c703ac93eb6615d09851c73e651 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 15:35:22 +0100 Subject: [PATCH 6/8] fixup --- lib/core/client.js | 122 ++++++++++++++++++++++++++++++++---- lib/node/http-parser.js | 135 +--------------------------------------- 2 files changed, 111 insertions(+), 146 deletions(-) diff --git a/lib/core/client.js b/lib/core/client.js index 0ba2385fa6f..96f238caebc 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -54,6 +54,11 @@ const { kBodyTimeout } = require('./symbols') +const nodeVersions = process.version.split('.') +const nodeMajorVersion = parseInt(nodeVersions[0].slice(1)) +const nodeMinorVersion = parseInt(nodeVersions[1]) +const insecureHTTPParser = process.execArgv.includes('--insecure-http-parser') + function getServerName (client, host) { return ( util.getServerName(host) || @@ -342,9 +347,34 @@ class Client extends EventEmitter { class Parser extends HTTPParser { constructor (client, socket) { - super({ - maxHeadersSize: client[kMaxHeadersSize] - }) + /* istanbul ignore next */ + if (nodeMajorVersion === 12 && nodeMinorVersion < 19) { + super() + this.initialize( + HTTPParser.RESPONSE, + {}, + 0 + ) + } else if (nodeMajorVersion === 12 && nodeMinorVersion >= 19) { + super() + this.initialize( + HTTPParser.RESPONSE, + {}, + client[kMaxHeadersSize], + 0 + ) + } else if (nodeMajorVersion > 12) { + super() + this.initialize( + HTTPParser.RESPONSE, + {}, + client[kMaxHeadersSize], + insecureHTTPParser, + 0 + ) + } else { + super(HTTPParser.RESPONSE, false) + } this.client = client this.socket = socket @@ -356,20 +386,54 @@ class Parser extends HTTPParser { this.request = null this.paused = false + // Parser can't be paused from within a callback. + // Use a buffer in JS land in order to stop further progress while paused. + this.resuming = false + this.queue = [] + this._resume = () => { - this.resume() + if (!this.paused || this.resuming) { + return + } + + this.paused = false + + this.resuming = true + while (this.queue.length) { + const [fn, ...args] = this.queue.shift() + + Reflect.apply(fn, this, args) + + if (this.paused) { + this.resuming = false + return + } + } + this.resuming = false + this.socket.resume() } this._pause = () => { - this.pause() + if (this.paused) { + return + } + + this.paused = true + this.socket.pause() } socket.on('data', onSocketData) } - onHeaders (rawHeaders) { + [HTTPParser.kOnHeaders] (rawHeaders) { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([this[HTTPParser.kOnHeaders], rawHeaders]) + return + } + if (this.headers) { Array.prototype.push.apply(this.headers, rawHeaders) } else { @@ -377,7 +441,12 @@ class Parser extends HTTPParser { } } - onExecute (ret, currentBuffer) { + [HTTPParser.kOnExecute] (ret, currentBuffer) { + if (this.paused) { + this.queue.push([this[HTTPParser.kOnExecute], ret]) + return + } + const { upgrade, socket } = this if (!Number.isFinite(ret)) { @@ -418,7 +487,6 @@ class Parser extends HTTPParser { try { if (!socket.destroyed && !request.aborted) { detachSocket(socket) - client[kSocket] = null client[kQueue][client[kRunningIdx]++] = null @@ -435,7 +503,15 @@ class Parser extends HTTPParser { } } - onHeadersComplete (rawHeaders, statusCode, statusMessage, upgrade, shouldKeepAlive) { + [HTTPParser.kOnHeadersComplete] (versionMajor, versionMinor, rawHeaders, method, + url, statusCode, statusMessage, upgrade, shouldKeepAlive) { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([this[HTTPParser.kOnHeadersComplete], versionMajor, versionMinor, rawHeaders, method, + url, statusCode, statusMessage, upgrade, shouldKeepAlive]) + return + } + const { client, socket } = this const request = client[kQueue][client[kRunningIdx]] @@ -534,7 +610,12 @@ class Parser extends HTTPParser { return request.method === 'HEAD' || statusCode < 200 ? 1 : 0 } - onBody (chunk, offset, length) { + [HTTPParser.kOnBody] (chunk, offset, length) { + if (this.paused) { + this.queue.push([this[HTTPParser.kOnBody], chunk, offset, length]) + return + } + const { socket, statusCode, request, timeout } = this if (socket.destroyed) { @@ -556,7 +637,13 @@ class Parser extends HTTPParser { } } - onMessageComplete () { + [HTTPParser.kOnMessageComplete] () { + /* istanbul ignore next: difficult to make a test case for */ + if (this.paused) { + this.queue.push([this[HTTPParser.kOnMessageComplete]]) + return + } + const { client, socket, statusCode, headers, upgrade, request, trailers } = this if (socket.destroyed) { @@ -638,7 +725,7 @@ class Parser extends HTTPParser { destroy () { clearTimeout(this.timeout) this.timeout = null - super.destroy() + setImmediate((self) => self.close(), this) } } @@ -684,7 +771,16 @@ function onSocketError (err) { } function onSocketData (data) { - this[kParser].execute(data) + const { [kParser]: parser } = this + + let ret + try { + ret = parser.execute(data) + } catch (err) { + ret = err + } + + parser[HTTPParser.kOnExecute](ret, data) } function onSocketEnd () { diff --git a/lib/node/http-parser.js b/lib/node/http-parser.js index 7345014d9ad..0039a55559c 100644 --- a/lib/node/http-parser.js +++ b/lib/node/http-parser.js @@ -3,140 +3,9 @@ // TODO: This is not really allowed by Node but it works for now. const common = require('_http_common') -let NodeHTTPParser if (common.HTTPParser) { - NodeHTTPParser = common.HTTPParser + module.exports = common.HTTPParser } else { // Node 10 - NodeHTTPParser = process.binding('http_parser').HTTPParser // eslint-disable-line -} - -const nodeVersions = process.version.split('.') -const nodeMajorVersion = parseInt(nodeVersions[0].slice(1)) -const nodeMinorVersion = parseInt(nodeVersions[1]) -const insecureHTTPParser = process.execArgv.includes('--insecure-http-parser') - -module.exports = class HTTPParser { - constructor ({ maxHeadersSize }) { - /* istanbul ignore next */ - if (nodeMajorVersion === 12 && nodeMinorVersion < 19) { - this.parser = new NodeHTTPParser() - this.parser.initialize( - NodeHTTPParser.RESPONSE, - {}, - 0 - ) - } else if (nodeMajorVersion === 12 && nodeMinorVersion >= 19) { - this.parser = new NodeHTTPParser() - this.parser.initialize( - NodeHTTPParser.RESPONSE, - {}, - maxHeadersSize, - 0 - ) - } else if (nodeMajorVersion > 12) { - this.parser = new NodeHTTPParser() - this.parser.initialize( - NodeHTTPParser.RESPONSE, - {}, - maxHeadersSize, - insecureHTTPParser, - 0 - ) - } else { - this.parser = new NodeHTTPParser(NodeHTTPParser.RESPONSE, false) - } - - this.parser[NodeHTTPParser.kOnHeaders] = function (rawHeaders) { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([NodeHTTPParser.kOnHeaders, rawHeaders]) - return - } - this.onHeaders(rawHeaders) - } - - this.parser[NodeHTTPParser.kOnExecute] = (ret, currentBuffer) => { - if (this.paused) { - this.queue.push([NodeHTTPParser.kOnExecute, ret, currentBuffer]) - return - } - this.onExecute(ret, currentBuffer) - } - - this.parser[NodeHTTPParser.kOnHeadersComplete] = (versionMajor, versionMinor, rawHeaders, method, - url, statusCode, statusMessage, upgrade, shouldKeepAlive) => { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([NodeHTTPParser.kOnHeadersComplete, versionMajor, versionMinor, rawHeaders, method, - url, statusCode, statusMessage, upgrade, shouldKeepAlive]) - return // TODO (fix): What do we return here? - } - return this.onHeadersComplete(rawHeaders, statusCode, statusMessage, upgrade, shouldKeepAlive) - } - - this.parser[NodeHTTPParser.kOnBody] = (chunk, offset, length) => { - if (this.paused) { - this.queue.push([NodeHTTPParser.kOnBody, chunk, offset, length]) - return - } - this.onBody(chunk, offset, length) - } - - this.parser[NodeHTTPParser.kOnMessageComplete] = () => { - /* istanbul ignore next: difficult to make a test case for */ - if (this.paused) { - this.queue.push([NodeHTTPParser.kOnMessageComplete]) - return - } - this.onMessageComplete() - } - - // Parser can't be paused from within a callback. - // Use a buffer in JS land in order to stop further progress while paused. - this.resuming = false - this.queue = [] - this.paused = false - } - - resume () { - if (!this.paused || this.resuming) { - return - } - - this.paused = false - - this.resuming = true - while (this.queue.length) { - const [key, ...args] = this.queue.shift() - - this[key](...args) - - if (this.paused) { - this.resuming = false - return - } - } - this.resuming = false - } - - pause () { - this.paused = true - } - - execute (data) { - let ret - try { - ret = this.parser.execute(data) - } catch (err) { - ret = err - } - - this.parser[NodeHTTPParser.kOnExecute](ret, data) - } - - destroy () { - setImmediate((parser) => parser.close(), this.parser) - this.parser = null - } + module.exports = process.binding('http_parser').HTTPParser // eslint-disable-line } From 2361b8e7d9b1a256f31bfcda93d58521384ea033 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 27 Feb 2021 17:28:20 +0100 Subject: [PATCH 7/8] fixup --- lib/core/client.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/core/client.js b/lib/core/client.js index 96f238caebc..1c884684584 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -772,15 +772,7 @@ function onSocketError (err) { function onSocketData (data) { const { [kParser]: parser } = this - - let ret - try { - ret = parser.execute(data) - } catch (err) { - ret = err - } - - parser[HTTPParser.kOnExecute](ret, data) + parser[HTTPParser.kOnExecute](parser.execute(data), data) } function onSocketEnd () { From 66af63379b0bcf59630a167968f0acaff77f0b88 Mon Sep 17 00:00:00 2001 From: Daniele Belardi Date: Sun, 7 Mar 2021 17:11:54 +0100 Subject: [PATCH 8/8] feat: use llhttp with wasm for parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implement a minimal parser that uses llhttp built with wasm. A custom repository was setup to have a custom wasm build. Benchmarks of the current changes: http - no agent x 436 ops/sec ±4.06% (54 runs sampled) http - keepalive x 453 ops/sec ±2.72% (54 runs sampled) http - keepalive - multiple sockets x 3,191 ops/sec ±2.30% (73 runs sampled) undici - pipeline x 4,062 ops/sec ±3.14% (73 runs sampled) undici - request x 3,925 ops/sec ±2.81% (74 runs sampled) undici - pool - request - multiple sockets x 4,045 ops/sec ±3.36% (76 runs sampled) undici - stream x 3,912 ops/sec ±2.69% (74 runs sampled) undici - dispatch x 4,371 ops/sec ±1.96% (79 runs sampled) undici - noop x 4,237 ops/sec ±2.58% (59 runs sampled) Benchmarks of the stable branch: http - no agent x 410 ops/sec ±4.18% (63 runs sampled) http - keepalive x 446 ops/sec ±2.75% (53 runs sampled) http - keepalive - multiple sockets x 3,248 ops/sec ±2.97% (73 runs sampled) undici - pipeline x 3,666 ops/sec ±3.18% (73 runs sampled) undici - request x 3,951 ops/sec ±3.04% (72 runs sampled) undici - pool - request - multiple sockets x 4,017 ops/sec ±3.16% (75 runs sampled) undici - stream x 4,014 ops/sec ±3.89% (70 runs sampled) undici - dispatch x 3,823 ops/sec ±3.46% (67 runs sampled) undici - noop x 4,569 ops/sec ±1.99% (59 runs sampled) --- build/llhttp.js | 30 +++++ lib/core/client.js | 42 +----- lib/llhttp/constants.js | 1 + lib/llhttp/llhttp.wasm | Bin 0 -> 42192 bytes lib/llhttp/parser.js | 275 ++++++++++++++++++++++++++++++++++++++++ package.json | 7 + test/stream-compat.js | 2 +- 7 files changed, 319 insertions(+), 38 deletions(-) create mode 100644 build/llhttp.js create mode 100644 lib/llhttp/constants.js create mode 100755 lib/llhttp/llhttp.wasm create mode 100644 lib/llhttp/parser.js diff --git a/build/llhttp.js b/build/llhttp.js new file mode 100644 index 00000000000..548dcd56f9d --- /dev/null +++ b/build/llhttp.js @@ -0,0 +1,30 @@ +'use strict' + +const { join, resolve } = require('path') +const { copyFileSync, rmSync } = require('fs') +const { execSync } = require('child_process') +const TMP = require('os').tmpdir() + +const REPO = 'git@github.com:dnlup/llhttp.git' +const CHECKOUT = 'undici_wasm' +const REPO_PATH = join(TMP, 'llhttp') +const WASM_OUT = resolve(__dirname, '../lib/llhttp') + +let code = 0 + +try { + execSync(`git clone ${REPO}`, { stdio: 'inherit', cwd: TMP }) + execSync(`git checkout ${CHECKOUT}`, { stdio: 'inherit', cwd: REPO_PATH }) + // https://docs.npmjs.com/cli/v7/commands/npm-ci + // Performs a clean install using the lockfile, this makes the installation faster. + execSync('npm ci', { stdio: 'inherit', cwd: REPO_PATH }) + execSync('npm run build-wasm', { stdio: 'inherit', cwd: REPO_PATH }) + copyFileSync(join(REPO_PATH, 'build', 'wasm', 'llhttp.wasm'), join(WASM_OUT, 'llhttp.wasm')) + copyFileSync(join(REPO_PATH, 'build', 'wasm', 'constants.js'), join(WASM_OUT, 'constants.js')) +} catch (error) { + console.error(error) + code = 1 +} finally { + rmSync(REPO_PATH, { recursive: true, force: true }) + process.exit(code) +} diff --git a/lib/core/client.js b/lib/core/client.js index d482cc5f291..dce2bc18129 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -2,7 +2,7 @@ const net = require('net') const tls = require('tls') -const HTTPParser = require('../node/http-parser') +const HTTPParser = require('../llhttp/parser') const EventEmitter = require('events') const assert = require('assert') const util = require('./util') @@ -54,9 +54,6 @@ const { kBodyTimeout } = require('./symbols') -const nodeVersions = process.version.split('.') -const nodeMajorVersion = parseInt(nodeVersions[0].slice(1)) -const nodeMinorVersion = parseInt(nodeVersions[1]) const insecureHTTPParser = process.execArgv.includes('--insecure-http-parser') function getServerName (client, host) { @@ -347,35 +344,7 @@ class Client extends EventEmitter { class Parser extends HTTPParser { constructor (client, socket) { - /* istanbul ignore next */ - if (nodeMajorVersion === 12 && nodeMinorVersion < 19) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - 0 - ) - } else if (nodeMajorVersion === 12 && nodeMinorVersion >= 19) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - client[kMaxHeadersSize], - 0 - ) - } else if (nodeMajorVersion > 12) { - super() - this.initialize( - HTTPParser.RESPONSE, - {}, - client[kMaxHeadersSize], - insecureHTTPParser, - 0 - ) - } else { - super(HTTPParser.RESPONSE, false) - } - + super(client[kMaxHeadersSize], insecureHTTPParser) this.client = client this.socket = socket this.timeout = null @@ -491,7 +460,7 @@ class Parser extends HTTPParser { resume(client) } - [HTTPParser.kOnExecute] (ret, currentBuffer) { + [HTTPParser.kOnExecute] (ret) { const { upgrade, socket } = this if (socket.destroyed) { @@ -508,7 +477,7 @@ class Parser extends HTTPParser { // have no way of slicing the parsing buffer without knowing // the offset which is only provided in kOnExecute. if (upgrade) { - const head = currentBuffer.slice(ret) + const head = this.getCurrentBuffer().slice(ret) if (this.paused) { this.queue.push(['onUpgrade', head]) } else { @@ -788,8 +757,7 @@ function onSocketError (err) { } function onSocketData (data) { - const { [kParser]: parser } = this - parser[HTTPParser.kOnExecute](parser.execute(data), data) + this[kParser].execute(data) } function onSocketEnd () { diff --git a/lib/llhttp/constants.js b/lib/llhttp/constants.js new file mode 100644 index 00000000000..36af679c852 --- /dev/null +++ b/lib/llhttp/constants.js @@ -0,0 +1 @@ +module.exports = {ERROR:{'0':'OK','1':'INTERNAL','2':'STRICT','3':'LF_EXPECTED','4':'UNEXPECTED_CONTENT_LENGTH','5':'CLOSED_CONNECTION','6':'INVALID_METHOD','7':'INVALID_URL','8':'INVALID_CONSTANT','9':'INVALID_VERSION','10':'INVALID_HEADER_TOKEN','11':'INVALID_CONTENT_LENGTH','12':'INVALID_CHUNK_SIZE','13':'INVALID_STATUS','14':'INVALID_EOF_STATE','15':'INVALID_TRANSFER_ENCODING','16':'CB_MESSAGE_BEGIN','17':'CB_HEADERS_COMPLETE','18':'CB_MESSAGE_COMPLETE','19':'CB_CHUNK_HEADER','20':'CB_CHUNK_COMPLETE','21':'PAUSED','22':'PAUSED_UPGRADE','23':'USER',OK:0,INTERNAL:1,STRICT:2,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,USER:23},TYPE:{'0':'BOTH','1':'REQUEST','2':'RESPONSE',BOTH:0,REQUEST:1,RESPONSE:2},FLAGS:{'1':'CONNECTION_KEEP_ALIVE','2':'CONNECTION_CLOSE','4':'CONNECTION_UPGRADE','8':'CHUNKED','16':'UPGRADE','32':'CONTENT_LENGTH','64':'SKIPBODY','128':'TRAILING','512':'TRANSFER_ENCODING',CONNECTION_KEEP_ALIVE:1,CONNECTION_CLOSE:2,CONNECTION_UPGRADE:4,CHUNKED:8,UPGRADE:16,CONTENT_LENGTH:32,SKIPBODY:64,TRAILING:128,TRANSFER_ENCODING:512},LENIENT_FLAGS:{'1':'HEADERS','2':'CHUNKED_LENGTH',HEADERS:1,CHUNKED_LENGTH:2},METHODS:{'0':'DELETE','1':'GET','2':'HEAD','3':'POST','4':'PUT','5':'CONNECT','6':'OPTIONS','7':'TRACE','8':'COPY','9':'LOCK','10':'MKCOL','11':'MOVE','12':'PROPFIND','13':'PROPPATCH','14':'SEARCH','15':'UNLOCK','16':'BIND','17':'REBIND','18':'UNBIND','19':'ACL','20':'REPORT','21':'MKACTIVITY','22':'CHECKOUT','23':'MERGE','24':'M-SEARCH','25':'NOTIFY','26':'SUBSCRIBE','27':'UNSUBSCRIBE','28':'PATCH','29':'PURGE','30':'MKCALENDAR','31':'LINK','32':'UNLINK','33':'SOURCE','34':'PRI','35':'DESCRIBE','36':'ANNOUNCE','37':'SETUP','38':'PLAY','39':'PAUSE','40':'TEARDOWN','41':'GET_PARAMETER','42':'SET_PARAMETER','43':'REDIRECT','44':'RECORD','45':'FLUSH',DELETE:0,GET:1,HEAD:2,POST:3,PUT:4,CONNECT:5,OPTIONS:6,TRACE:7,COPY:8,LOCK:9,MKCOL:10,MOVE:11,PROPFIND:12,PROPPATCH:13,SEARCH:14,UNLOCK:15,BIND:16,REBIND:17,UNBIND:18,ACL:19,REPORT:20,MKACTIVITY:21,CHECKOUT:22,MERGE:23,'M-SEARCH':24,NOTIFY:25,SUBSCRIBE:26,UNSUBSCRIBE:27,PATCH:28,PURGE:29,MKCALENDAR:30,LINK:31,UNLINK:32,SOURCE:33,PRI:34,DESCRIBE:35,ANNOUNCE:36,SETUP:37,PLAY:38,PAUSE:39,TEARDOWN:40,GET_PARAMETER:41,SET_PARAMETER:42,REDIRECT:43,RECORD:44,FLUSH:45},METHODS_HTTP:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,33],METHODS_ICE:[33],METHODS_RTSP:[6,35,36,37,38,39,40,41,42,43,44,45,1,3],METHOD_MAP:{DELETE:0,GET:1,HEAD:2,POST:3,PUT:4,CONNECT:5,OPTIONS:6,TRACE:7,COPY:8,LOCK:9,MKCOL:10,MOVE:11,PROPFIND:12,PROPPATCH:13,SEARCH:14,UNLOCK:15,BIND:16,REBIND:17,UNBIND:18,ACL:19,REPORT:20,MKACTIVITY:21,CHECKOUT:22,MERGE:23,'M-SEARCH':24,NOTIFY:25,SUBSCRIBE:26,UNSUBSCRIBE:27,PATCH:28,PURGE:29,MKCALENDAR:30,LINK:31,UNLINK:32,SOURCE:33,PRI:34,DESCRIBE:35,ANNOUNCE:36,SETUP:37,PLAY:38,PAUSE:39,TEARDOWN:40,GET_PARAMETER:41,SET_PARAMETER:42,REDIRECT:43,RECORD:44,FLUSH:45},H_METHOD_MAP:{HEAD:2},FINISH:{'0':'SAFE','1':'SAFE_WITH_CB','2':'UNSAFE',SAFE:0,SAFE_WITH_CB:1,UNSAFE:2},ALPHA:['A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z'],NUM_MAP:{'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9},HEX_MAP:{'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},NUM:['0','1','2','3','4','5','6','7','8','9'],ALPHANUM:['A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9'],MARK:['-','_','.','!','~','*','\'','(',')'],USERINFO_CHARS:['A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9','-','_','.','!','~','*','\'','(',')','%',';',':','&','=','+','$',','],STRICT_URL_CHAR:['!','"','$','%','&','\'','(',')','*','+',',','-','.','/',':',';','<','=','>','@','[','\\',']','^','_','`','{','|','}','~','A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9'],URL_CHAR:['!','"','$','%','&','\'','(',')','*','+',',','-','.','/',':',';','<','=','>','@','[','\\',']','^','_','`','{','|','}','~','A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9','\t','\f',128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],HEX:['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','A','B','C','D','E','F'],STRICT_TOKEN:['!','#','$','%','&','\'','*','+','-','.','^','_','`','|','~','A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9'],TOKEN:['!','#','$','%','&','\'','*','+','-','.','^','_','`','|','~','A','a','B','b','C','c','D','d','E','e','F','f','G','g','H','h','I','i','J','j','K','k','L','l','M','m','N','n','O','o','P','p','Q','q','R','r','S','s','T','t','U','u','V','v','W','w','X','x','Y','y','Z','z','0','1','2','3','4','5','6','7','8','9',' '],HEADER_CHARS:['\t',32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],CONNECTION_TOKEN_CHARS:['\t',32,33,34,35,36,37,38,39,40,41,42,43,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],MAJOR:{'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9},MINOR:{'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9},HEADER_STATE:{'0':'GENERAL','1':'CONNECTION','2':'CONTENT_LENGTH','3':'TRANSFER_ENCODING','4':'UPGRADE','5':'CONNECTION_KEEP_ALIVE','6':'CONNECTION_CLOSE','7':'CONNECTION_UPGRADE','8':'TRANSFER_ENCODING_CHUNKED',GENERAL:0,CONNECTION:1,CONTENT_LENGTH:2,TRANSFER_ENCODING:3,UPGRADE:4,CONNECTION_KEEP_ALIVE:5,CONNECTION_CLOSE:6,CONNECTION_UPGRADE:7,TRANSFER_ENCODING_CHUNKED:8},SPECIAL_HEADERS:{connection:1,'content-length':2,'proxy-connection':1,'transfer-encoding':3,upgrade:4}} \ No newline at end of file diff --git a/lib/llhttp/llhttp.wasm b/lib/llhttp/llhttp.wasm new file mode 100755 index 0000000000000000000000000000000000000000..d266bfe72a0ba1468a3ca6eb04aff4a0a793aa91 GIT binary patch literal 42192 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 Z;aZ1l0M`hv58^rx*HyS~z;zq0{{$pm3&8*Y literal 0 HcmV?d00001 diff --git a/lib/llhttp/parser.js b/lib/llhttp/parser.js new file mode 100644 index 00000000000..2dc46015c3a --- /dev/null +++ b/lib/llhttp/parser.js @@ -0,0 +1,275 @@ +/** + * Minimal HTTP response parser using a wasm build of llhttp. + * This is based on the work made by devsnek in https://github.com/devsnek/llhttp/tree/wasm + * + * The wasm build is currently imeplemented in a custom repo: + * https://github.com/dnlup/llhttp/tree/undici_wasm + */ + +'use strict' + +/* global WebAssembly */ + +const { resolve } = require('path') +const { readFileSync } = require('fs') +const constants = require('./constants') +const { kMaxHeadersSize } = require('../core/symbols') +const WASM_BUILD = resolve(__dirname, './llhttp.wasm') +const bin = readFileSync(WASM_BUILD) +const mod = new WebAssembly.Module(bin) + +const kOnMessageBegin = 0 +const kOnHeaders = 1 +const kOnHeadersComplete = 2 +const kOnBody = 3 +const kOnMessageComplete = 4 +const kOnExecute = 5 + +const kPtr = Symbol('kPrt') +const kStatusMessage = Symbol('kStatusMessage') +const kHeadersFields = Symbol('kHeadersFields') +const kHeadersValues = Symbol('kHeadersValues') +const kHeaderSize = Symbol('kHeaderSize') +const kBufferSize = Symbol('kBufferSize') +const kBufferPtr = Symbol('kBufferPtr') +const kMallocBuffer = Symbol('kMallocBuffer') +const kCurrentBuffer = Symbol('kCurrentBuffer') +const kGetHeaders = Symbol('kGetHeaders') +const kTrackHeader = Symbol('kTrackHeader') +const kMakeError = Symbol('kMakeError') + +/** + * Current parser reference + */ +let currentParser = null + +const cstr = (ptr, len) => + Buffer.from(inst.exports.memory.buffer, ptr, len).toString() + +/* eslint-disable camelcase */ +const wasm_on_message_begin = p => { + currentParser[kStatusMessage] = null + return currentParser[kOnMessageBegin]() +} + +const wasm_on_url = (p, at, length) => { + return 0 +} + +const wasm_on_status = (p, at, length) => { + const ret = currentParser[kTrackHeader](length) + if (ret !== 0) { + return ret + } + currentParser[kStatusMessage] = cstr(at, length) + return 0 +} + +const wasm_on_header_field = (p, at, length) => { + // TODO: this could be optimized. + // See https://github.com/nodejs/undici/pull/575#discussion_r589024917 + const ret = currentParser[kTrackHeader](length) + if (ret !== 0) { + return ret + } + currentParser[kHeadersFields].push(cstr(at, length)) + return 0 +} + +const wasm_on_header_value = (p, at, length) => { + const ret = currentParser[kTrackHeader](length) + if (ret !== 0) { + return ret + } + currentParser[kHeadersValues].push(cstr(at, length)) + return 0 +} + +const wasm_on_headers_complete = p => { + currentParser[kHeaderSize] = 0 + const versionMajor = inst.exports.llhttp_get_http_major(p) + const versionMinor = inst.exports.llhttp_get_http_minor(p) + const rawHeaders = currentParser[kGetHeaders]() + const statusCode = inst.exports.llhttp_get_status_code(p) + const statusMessage = currentParser[kStatusMessage] + const upgrade = Boolean(inst.exports.llhttp_get_upgrade(p)) + const shouldKeepAlive = Boolean(inst.exports.llhttp_should_keep_alive(p)) + + return currentParser[kOnHeadersComplete](versionMajor, versionMinor, rawHeaders, null, + null, statusCode, statusMessage, upgrade, shouldKeepAlive) +} + +const wasm_on_body = (p, at, length) => { + // TODO: we could optimize this further by making this part responsibility fo the user. + // Forcing them to consume the buffer synchronously or copy it otherwise. + // See https://github.com/nodejs/undici/pull/575#discussion_r588885738 + const u8 = new Uint8Array(inst.exports.memory.buffer, at, length) + const body = Buffer.from(u8) // llhttp re-uses buffer so we need to make a copy. + currentParser[kOnBody](body, 0, body.length) + return 0 +} + +const wasm_on_message_complete = (p) => { + // Handle trailers + if (currentParser[kHeadersFields].length) { + currentParser[kOnHeaders](currentParser[kGetHeaders]()) + } + const ret = currentParser[kOnMessageComplete]() + return ret +} + +/* eslint-enable camelcase */ + +const inst = new WebAssembly.Instance(mod, { + env: { + wasm_on_message_begin, + wasm_on_url, + wasm_on_status, + wasm_on_header_field, + wasm_on_header_value, + wasm_on_headers_complete, + wasm_on_body, + wasm_on_message_complete + } +}) + +inst.exports._initialize() // wasi reactor + +class HTTPParserError extends Error { + constructor (message, code) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + } +} + +class HTTPParser { + constructor (maxHeadersSize = 8 * 1024, lenient = false) { + this[kPtr] = inst.exports.llhttp_alloc(constants.TYPE.RESPONSE) + this[kBufferSize] = 0 + this[kBufferPtr] = null + this[kCurrentBuffer] = null + this[kStatusMessage] = null + this[kHeadersFields] = [] + this[kHeadersValues] = [] + this[kMaxHeadersSize] = maxHeadersSize + this[kHeaderSize] = 0 + + if (lenient === true) { + inst.exports.llhttp_set_lenient_headers(this[kPtr], 1) + } + } + + [kMallocBuffer] (size) { + if (this[kBufferPtr]) { + inst.exports.free(this[kBufferPtr]) + } + this[kBufferSize] = size + this[kBufferPtr] = inst.exports.malloc(size) + } + + [kGetHeaders] () { + const rawHeaders = [] + for (let c = 0; c < this[kHeadersFields].length; c++) { + rawHeaders.push(this[kHeadersFields][c], this[kHeadersValues][c]) + } + this[kHeadersFields] = [] + this[kHeadersValues] = [] + this[kHeaderSize] = 0 + return rawHeaders + } + + [kTrackHeader] (length) { + this[kHeaderSize] += length + if (this[kHeaderSize] >= this[kMaxHeadersSize]) { + inst.exports.llhttp_set_error_reason(this[kPtr], 'HPE_HEADER_OVERFLOW:Header overflow') + return constants.ERROR.USER + } + return 0 + } + + [kOnMessageBegin] () { + return 0 + } + + [kOnHeaders] (rawHeaders) {} + + [kOnHeadersComplete] (versionMajor, versionMinor, rawHeaders, method, + url, statusCode, statusMessage, upgrade, shouldKeepAlive) { + return 0 + } + + [kOnBody] (body) { + return 0 + } + + [kOnMessageComplete] () { + return 0 + } + + [kOnExecute] (ret) {} + + close () { + inst.exports.llhttp_free(this[kPtr]) + this[kPtr] = null + inst.exports.free(this[kBufferPtr]) + this[kBufferPtr] = null + this[kCurrentBuffer] = Buffer.alloc(0) + } + + execute (data) { + currentParser = this + this[kCurrentBuffer] = data + // Be sure the parser buffer can contain `data` + if (data.length > this[kBufferSize]) { + this[kMallocBuffer](Math.ceil(data.length / 4096) * 4096) + } + // Instantiate a Unit8 Buffer view of the wasm memory that starts from the parser buffer pointer + // and has the size of `data` + const u8 = new Uint8Array(inst.exports.memory.buffer, this[kBufferPtr], data.length) + // Fill the view with `data` + u8.set(data) + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + // See https://github.com/dnlup/llhttp/blob/undici_wasm/src/native/api.c#L106 + const err = inst.exports.llhttp_execute(this[kPtr], this[kBufferPtr], data.length) + let ret = data.length + if (err !== constants.ERROR.OK) { + const errorPos = inst.exports.llhttp_get_error_pos(this[kPtr]) + ret = errorPos - this[kBufferPtr] + if (err === constants.ERROR.PAUSED_UPGRADE) { + inst.exports.llhttp_resume_after_upgrade(this[kPtr]) + } else { + ret = this[kMakeError](err) + } + } + this[kOnExecute](ret) + currentParser = null + return ret + } + + getCurrentBuffer () { + return this[kCurrentBuffer] + } + + [kMakeError] (number) { + const ptr = inst.exports.llhttp_get_error_reason(this[kPtr]) + const u8 = new Uint8Array(inst.exports.memory.buffer) + const len = u8.indexOf(0, ptr) - ptr + const message = cstr(ptr, len) + const code = constants.ERROR[number] + return new HTTPParserError(message, code) + } +} + +HTTPParser.kOnMessageBegin = kOnMessageBegin +HTTPParser.kOnHeaders = kOnHeaders +HTTPParser.kOnHeadersComplete = kOnHeadersComplete +HTTPParser.kOnBody = kOnBody +HTTPParser.kOnMessageComplete = kOnMessageComplete +HTTPParser.kOnExecute = kOnExecute + +module.exports = HTTPParser diff --git a/package.json b/package.json index d53d71a201c..038bc5b9ffd 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,13 @@ "types": "index.d.ts", "module": "wrapper.mjs", "scripts": { + "build": "node build/llhttp.js", "lint": "standard | snazzy", "test": "tap test/*.js --no-coverage && jest test/jest/test", "test:typescript": "tsd", "coverage": "standard | snazzy && tap test/*.js", "coverage:ci": "npm run coverage -- --coverage-report=lcovonly", + "prebench": "node -e \"try { require('fs').unlinkSync(require('path').join(require('os').tmpdir(), 'undici.sock')) } catch (_) {}\"", "bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"" }, "repository": { @@ -49,6 +51,11 @@ "pre-commit": [ "coverage" ], + "standard": { + "ignore": [ + "lib/llhttp/constants.js" + ] + }, "tsd": { "directory": "test/types", "compilerOptions": { diff --git a/test/stream-compat.js b/test/stream-compat.js index 56c2d128bfc..55dba7f82a4 100644 --- a/test/stream-compat.js +++ b/test/stream-compat.js @@ -33,7 +33,7 @@ test('stream body without destroy', (t) => { }) }) -test('IncomingMessage', { only: true }, (t) => { +test('IncomingMessage', (t) => { t.plan(2) const server = createServer((req, res) => {