From b5b38f9f57ccf5295231f923770d2a2d89d42ffe Mon Sep 17 00:00:00 2001 From: Brian White Date: Sat, 31 Dec 2016 16:24:54 -0500 Subject: [PATCH] http: try to avoid lowercasing incoming headers PR-URL: https://github.com/nodejs/node/pull/10558 Reviewed-By: Matteo Collina Reviewed-By: James M Snell Reviewed-By: Evan Lucas --- lib/_http_incoming.js | 206 +++++++++++++++++++++++++++++++++--------- 1 file changed, 161 insertions(+), 45 deletions(-) diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index d02f19424c0442..53419323f46b0d 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -119,6 +119,146 @@ function _addHeaderLines(headers, n) { } +// This function is used to help avoid the lowercasing of a field name if it +// matches a 'traditional cased' version of a field name. It then returns the +// lowercased name to both avoid calling toLowerCase() a second time and to +// indicate whether the field was a 'no duplicates' field. If a field is not a +// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception +// to this is the Set-Cookie header which is indicated by a `1` byte flag, since +// it is an 'array' field and thus is treated differently in _addHeaderLines(). +// TODO: perhaps http_parser could be returning both raw and lowercased versions +// of known header names to avoid us having to call toLowerCase() for those +// headers. +/* eslint-disable max-len */ +// 'array' header list is taken from: +// https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp +/* eslint-enable max-len */ +function matchKnownFields(field) { + var low = false; + while (true) { + switch (field) { + case 'Content-Type': + case 'content-type': + return 'content-type'; + case 'Content-Length': + case 'content-length': + return 'content-length'; + case 'User-Agent': + case 'user-agent': + return 'user-agent'; + case 'Referer': + case 'referer': + return 'referer'; + case 'Host': + case 'host': + return 'host'; + case 'Authorization': + case 'authorization': + return 'authorization'; + case 'Proxy-Authorization': + case 'proxy-authorization': + return 'proxy-authorization'; + case 'If-Modified-Since': + case 'if-modified-since': + return 'if-modified-since'; + case 'If-Unmodified-Since': + case 'if-unmodified-since': + return 'if-unmodified-since'; + case 'From': + case 'from': + return 'from'; + case 'Location': + case 'location': + return 'location'; + case 'Max-Forwards': + case 'max-forwards': + return 'max-forwards'; + case 'Retry-After': + case 'retry-after': + return 'retry-after'; + case 'ETag': + case 'etag': + return 'etag'; + case 'Last-Modified': + case 'last-modified': + return 'last-modified'; + case 'Server': + case 'server': + return 'server'; + case 'Age': + case 'age': + return 'age'; + case 'Expires': + case 'expires': + return 'expires'; + case 'Set-Cookie': + case 'set-cookie': + return '\u0001'; + // The fields below are not used in _addHeaderLine(), but they are common + // headers where we can avoid toLowerCase() if the mixed or lower case + // versions match the first time through. + case 'Transfer-Encoding': + case 'transfer-encoding': + return '\u0000transfer-encoding'; + case 'Date': + case 'date': + return '\u0000date'; + case 'Connection': + case 'connection': + return '\u0000connection'; + case 'Cache-Control': + case 'cache-control': + return '\u0000cache-control'; + case 'Vary': + case 'vary': + return '\u0000vary'; + case 'Content-Encoding': + case 'content-encoding': + return '\u0000content-encoding'; + case 'Cookie': + case 'cookie': + return '\u0000cookie'; + case 'Origin': + case 'origin': + return '\u0000origin'; + case 'Upgrade': + case 'upgrade': + return '\u0000upgrade'; + case 'Expect': + case 'expect': + return '\u0000expect'; + case 'If-Match': + case 'if-match': + return '\u0000if-match'; + case 'If-None-Match': + case 'if-none-match': + return '\u0000if-none-match'; + case 'Accept': + case 'accept': + return '\u0000accept'; + case 'Accept-Encoding': + case 'accept-encoding': + return '\u0000accept-encoding'; + case 'Accept-Language': + case 'accept-language': + return '\u0000accept-language'; + case 'X-Forwarded-For': + case 'x-forwarded-for': + return '\u0000x-forwarded-for'; + case 'X-Forwarded-Host': + case 'x-forwarded-host': + return '\u0000x-forwarded-host'; + case 'X-Forwarded-Proto': + case 'x-forwarded-proto': + return '\u0000x-forwarded-proto'; + default: + if (low) + return '\u0000' + field; + field = field.toLowerCase(); + low = true; + } + } +} // Add the given (field, value) pair to the message // // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the @@ -128,51 +268,27 @@ function _addHeaderLines(headers, n) { // always joined. IncomingMessage.prototype._addHeaderLine = _addHeaderLine; function _addHeaderLine(field, value, dest) { - field = field.toLowerCase(); - switch (field) { - // Array headers: - case 'set-cookie': - if (dest[field] !== undefined) { - dest[field].push(value); - } else { - dest[field] = [value]; - } - break; - - /* eslint-disable max-len */ - // list is taken from: - // https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp - /* eslint-enable max-len */ - case 'content-type': - case 'content-length': - case 'user-agent': - case 'referer': - case 'host': - case 'authorization': - case 'proxy-authorization': - case 'if-modified-since': - case 'if-unmodified-since': - case 'from': - case 'location': - case 'max-forwards': - case 'retry-after': - case 'etag': - case 'last-modified': - case 'server': - case 'age': - case 'expires': - // drop duplicates - if (dest[field] === undefined) - dest[field] = value; - break; - - default: - // make comma-separated list - if (typeof dest[field] === 'string') { - dest[field] += ', ' + value; - } else { - dest[field] = value; - } + field = matchKnownFields(field); + var flag = field.charCodeAt(0); + if (flag === 0) { + field = field.slice(1); + // Make comma-separated list + if (typeof dest[field] === 'string') { + dest[field] += ', ' + value; + } else { + dest[field] = value; + } + } else if (flag === 1) { + // Array header -- only Set-Cookie at the moment + if (dest['set-cookie'] !== undefined) { + dest['set-cookie'].push(value); + } else { + dest['set-cookie'] = [value]; + } + } else { + // Drop duplicates + if (dest[field] === undefined) + dest[field] = value; } }