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 864b3f3967e..88cd46673b5 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') @@ -56,9 +56,6 @@ const { kStrictContentLength } = 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) { @@ -357,35 +354,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 @@ -427,7 +396,7 @@ class Parser extends HTTPParser { } this.resuming = false - socketResume(this.socket) + this.socket.resume() } this._pause = () => { @@ -437,8 +406,10 @@ class Parser extends HTTPParser { this.paused = true - socketPause(this.socket) + this.socket.pause() } + + socket.on('data', onSocketData) } [HTTPParser.kOnHeaders] (rawHeaders) { @@ -542,7 +513,6 @@ class Parser extends HTTPParser { this.request = request if (request.upgrade) { - this.unconsume() this.upgrade = true return } @@ -747,7 +717,6 @@ class Parser extends HTTPParser { destroy () { clearTimeout(this.timeout) this.timeout = null - this.unconsume() setImmediate((self) => self.close(), this) } } @@ -793,6 +762,10 @@ function onSocketError (err) { } } +function onSocketData (data) { + this[kParser].execute(data) +} + function onSocketEnd () { util.destroy(this, new SocketError('other side closed')) } @@ -811,6 +784,7 @@ function detachSocket (socket) { .removeListener('session', onSocketSession) .removeListener('error', onSocketError) .removeListener('end', onSocketEnd) + .removeListener('data', onSocketData) .removeListener('close', onSocketClose) } @@ -885,15 +859,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 @@ -909,26 +874,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/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 00000000000..d266bfe72a0 Binary files /dev/null and b/lib/llhttp/llhttp.wasm differ 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 79e5c3f5668..8292c9fcf53 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ "main": "index.js", "types": "index.d.ts", "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", + "prebench": "node -e \"try { require('fs').unlinkSync(require('path').join(require('os').tmpdir(), 'undici.sock')) } catch (_) {}\"", "coverage:ci": "npm run coverage -- --coverage-report=lcovonly", "bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"", "serve-website": "docsify serve .", @@ -51,10 +53,15 @@ "tap": "^14.10.8", "tsd": "^0.13.1" }, + "standard": { + "ignore": [ + "lib/llhttp/constants.js" + ] + }, "tsd": { "directory": "test/types", "compilerOptions": { "esModuleInterop": true } } -} +} \ No newline at end of file diff --git a/test/socket-handle-error.js b/test/socket-handle-error.js deleted file mode 100644 index 951a03ea97f..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, -100) - }) - - setTimeout(() => { - data.body.resume() - }, 100) - }) - return client.size <= client.pipelining - } - }) -}) 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) => {