diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 1e7fc8adeefa..648b148798b2 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -63,16 +63,15 @@ function IncomingMessage(socket) { return deprecateInstantiation(IncomingMessage, 'DEP0195', socket); } - let streamOptions; + Readable.call(this); if (socket) { - streamOptions = { - highWaterMark: socket.readableHighWaterMark, - }; + const hwm = socket.readableHighWaterMark; + if (this._readableState.highWaterMark !== hwm) { + this._readableState.highWaterMark = hwm; + } } - Readable.call(this, streamOptions); - this._readableState.readingMore = true; this.socket = socket; @@ -530,6 +529,43 @@ IncomingMessage.prototype._dump = function _dump() { } }; +// Case-insensitive ASCII compare against an already-lowercased name. +// Avoids allocating a lowercased copy of every header name. +function asciiEqualIgnoreCase(a, lower) { + const len = lower.length; + if (a.length !== len) + return false; + if (a === lower) + return true; + for (let i = 0; i < len; i++) { + let c = a.charCodeAt(i); + if (c >= 65 && c <= 90) + c += 32; + if (c !== lower.charCodeAt(i)) + return false; + } + return true; +} + +function getRawHeader(msg, lowerName, joinDuplicates) { + const rawHeaders = msg.rawHeaders; + const count = msg[kHeadersCount]; + let result; + for (let i = 0; i < count; i += 2) { + if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName)) + continue; + const value = rawHeaders[i + 1]; + if (result === undefined) { + result = value; + if (!joinDuplicates) + return result; + } else { + result += ', ' + value; + } + } + return result; +} + function onError(self, error, cb) { // This is to keep backward compatible behavior. // An error is emitted only if there are listeners attached to the event. @@ -544,6 +580,8 @@ module.exports = { IncomingMessage, kDetachAbortSignal, kHeadersCount, + asciiEqualIgnoreCase, + getRawHeader, readStart, readStop, }; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index a9d982796749..0b728f6ae4d4 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -659,10 +659,38 @@ function storeHeader(self, state, key, value, validate, lenient) { matchHeader(self, state, key, value); } +function lowerOutgoingHeaderName(field) { + switch (field) { + case 'Connection': + case 'connection': + return 'connection'; + case 'Content-Length': + case 'content-length': + return 'content-length'; + case 'Transfer-Encoding': + case 'transfer-encoding': + return 'transfer-encoding'; + case 'Date': + case 'date': + return 'date'; + case 'Expect': + case 'expect': + return 'expect'; + case 'Trailer': + case 'trailer': + return 'trailer'; + case 'Keep-Alive': + case 'keep-alive': + return 'keep-alive'; + default: + return field.toLowerCase(); + } +} + function matchHeader(self, state, field, value) { if (field.length < 4 || field.length > 17) return; - field = field.toLowerCase(); + field = lowerOutgoingHeaderName(field); switch (field) { case 'connection': state.connection = true; diff --git a/lib/_http_server.js b/lib/_http_server.js index 64189a4a6640..799f5b7c5ac8 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -74,6 +74,8 @@ const { IncomingMessage, kDetachAbortSignal, kHeadersCount, + asciiEqualIgnoreCase, + getRawHeader, } = require('_http_incoming'); const { ConnResetException, @@ -225,7 +227,8 @@ function ServerResponse(req, options) { this._expect_continue = false; if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { - this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); + this.useChunkedEncodingByDefault = + chunkExpression.test(getRawHeader(req, 'te', true)); this.shouldKeepAlive = false; } @@ -1289,10 +1292,8 @@ function hasRequestHeader(req, length, lowerName) { const count = req[kHeadersCount]; for (let i = 0; i < count; i += 2) { const key = rawHeaders[i]; - if (key.length === length && - (key === lowerName || key.toLowerCase() === lowerName)) { + if (key.length === length && asciiEqualIgnoreCase(key, lowerName)) return true; - } } return false; } @@ -1301,12 +1302,14 @@ function hasBodyHeaders(req) { const rawHeaders = req.rawHeaders; const count = req[kHeadersCount]; for (let i = 0; i < count; i += 2) { - const length = rawHeaders[i].length; - if (length === 14 || length === 17) { - const key = rawHeaders[i].toLowerCase(); - if (key === 'content-length' || key === 'transfer-encoding') { + const key = rawHeaders[i]; + const length = key.length; + if (length === 14) { + if (asciiEqualIgnoreCase(key, 'content-length')) + return true; + } else if (length === 17) { + if (asciiEqualIgnoreCase(key, 'transfer-encoding')) return true; - } } } return false; @@ -1441,22 +1444,25 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { server.emit('dropRequest', req, socket); res.writeHead(503); res.end(); - } else if (hasRequestHeader(req, 6, 'expect')) { - handled = true; - - if (continueExpression.test(req.headers.expect)) { - res._expect_continue = true; - if (server.listenerCount('checkContinue') > 0) { - server.emit('checkContinue', req, res); + } else { + const expect = getRawHeader(req, 'expect', true); + if (expect !== undefined) { + handled = true; + + if (continueExpression.test(expect)) { + res._expect_continue = true; + if (server.listenerCount('checkContinue') > 0) { + server.emit('checkContinue', req, res); + } else { + res.writeContinue(); + server.emit('request', req, res); + } + } else if (server.listenerCount('checkExpectation') > 0) { + server.emit('checkExpectation', req, res); } else { - res.writeContinue(); - server.emit('request', req, res); + res.writeHead(417); + res.end(); } - } else if (server.listenerCount('checkExpectation') > 0) { - server.emit('checkExpectation', req, res); - } else { - res.writeHead(417); - res.end(); } } } diff --git a/test/parallel/test-http-server-raw-header-lookup.js b/test/parallel/test-http-server-raw-header-lookup.js new file mode 100644 index 000000000000..620321c0ff30 --- /dev/null +++ b/test/parallel/test-http-server-raw-header-lookup.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const net = require('net'); + +const server = http.createServer({ + optimizeEmptyRequests: true, +}, common.mustCall((req, res) => { + // Host / Expect lookups must not force materialization of req.headers. + // Accessing headers afterwards still works, including unusual casing. + assert.strictEqual(req.headers.host, 'example.test'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}, 3)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + function request(headers) { + return new Promise((resolve, reject) => { + const socket = net.connect(port, '127.0.0.1', () => { + socket.write(headers); + }); + let data = ''; + socket.setEncoding('utf8'); + socket.on('data', (chunk) => { data += chunk; }); + socket.on('end', () => resolve(data)); + socket.on('error', reject); + }); + } + + (async () => { + // Mixed-case Host must satisfy requireHostHeader. + const mixedHost = await request( + 'GET / HTTP/1.1\r\nHOST: example.test\r\nConnection: close\r\n\r\n', + ); + assert.match(mixedHost, /^HTTP\/1\.1 200 /); + + // Unusual Host casing. + const oddHost = await request( + 'GET / HTTP/1.1\r\nhOsT: example.test\r\nConnection: close\r\n\r\n', + ); + assert.match(oddHost, /^HTTP\/1\.1 200 /); + + // Mixed-case Expect: 100-continue still triggers the continue path. + const expectContinue = await request( + 'POST / HTTP/1.1\r\nHost: example.test\r\n' + + 'EXPECT: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n', + ); + assert.match(expectContinue, /^HTTP\/1\.1 100 Continue\r\n/); + assert.match(expectContinue, /HTTP\/1\.1 200 /); + + // Missing Host is still rejected. + const missingHost = await request( + 'GET / HTTP/1.1\r\nConnection: close\r\n\r\n', + ); + assert.match(missingHost, /^HTTP\/1\.1 400 /); + + server.close(); + })().then(common.mustCall()); +}));