http: join authorization headers

PR-URL: https://github.com/nodejs/node/pull/45982
Fixes: https://github.com/nodejs/node/issues/45699
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
This commit is contained in:
Marco Ippolito 2023-01-03 11:43:21 +01:00 committed by GitHub
parent e35e893d26
commit 4080bada1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 2 deletions

View File

@ -2426,6 +2426,13 @@ as an argument to any listeners on the event.
<!-- YAML <!-- YAML
added: v0.1.5 added: v0.1.5
changes: changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/45982
description: >-
The `joinDuplicateHeaders` option in the `http.request()`
and `http.createServer()` functions ensures that duplicate
headers are not discarded, but rather combined using a
comma separator, in accordance with RFC 9110 Section 5.3.
- version: v15.1.0 - version: v15.1.0
pr-url: https://github.com/nodejs/node/pull/35281 pr-url: https://github.com/nodejs/node/pull/35281
description: >- description: >-
@ -2455,6 +2462,10 @@ header name:
`etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`, `etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`,
`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`, `last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`,
`retry-after`, `server`, or `user-agent` are discarded. `retry-after`, `server`, or `user-agent` are discarded.
To allow duplicate values of the headers listed above to be joined,
use the option `joinDuplicateHeaders` in [`http.request()`][]
and [`http.createServer()`][]. See RFC 9110 Section 5.3 for more
information.
* `set-cookie` is always an array. Duplicates are added to the array. * `set-cookie` is always an array. Duplicates are added to the array.
* For duplicate `cookie` headers, the values are joined together with `; `. * For duplicate `cookie` headers, the values are joined together with `; `.
* For all other headers, the values are joined together with `, `. * For all other headers, the values are joined together with `, `.
@ -3186,6 +3197,10 @@ changes:
a 400 (Bad Request) status code to any HTTP/1.1 request message a 400 (Bad Request) status code to any HTTP/1.1 request message
that lacks a Host header (as mandated by the specification). that lacks a Host header (as mandated by the specification).
**Default:** `true`. **Default:** `true`.
* `joinDuplicateHeaders` {boolean} It joins the field line values of multiple
headers in a request with `, ` instead of discarding the duplicates.
See [`message.headers`][] for more information.
**Default:** `false`.
* `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class * `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class
to be used. Useful for extending the original `ServerResponse`. **Default:** to be used. Useful for extending the original `ServerResponse`. **Default:**
`ServerResponse`. `ServerResponse`.
@ -3441,6 +3456,10 @@ changes:
* `uniqueHeaders` {Array} A list of request headers that should be sent * `uniqueHeaders` {Array} A list of request headers that should be sent
only once. If the header's value is an array, the items will be joined only once. If the header's value is an array, the items will be joined
using `; `. using `; `.
* `joinDuplicateHeaders` {boolean} It joins the field line values of
multiple headers in a request with `, ` instead of discarding
the duplicates. See [`message.headers`][] for more information.
**Default:** `false`.
* `callback` {Function} * `callback` {Function}
* Returns: {http.ClientRequest} * Returns: {http.ClientRequest}
@ -3754,6 +3773,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`.
[`http.IncomingMessage`]: #class-httpincomingmessage [`http.IncomingMessage`]: #class-httpincomingmessage
[`http.ServerResponse`]: #class-httpserverresponse [`http.ServerResponse`]: #class-httpserverresponse
[`http.Server`]: #class-httpserver [`http.Server`]: #class-httpserver
[`http.createServer()`]: #httpcreateserveroptions-requestlistener
[`http.get()`]: #httpgetoptions-callback [`http.get()`]: #httpgetoptions-callback
[`http.globalAgent`]: #httpglobalagent [`http.globalAgent`]: #httpglobalagent
[`http.request()`]: #httprequestoptions-callback [`http.request()`]: #httprequestoptions-callback

View File

@ -82,6 +82,7 @@ const {
} = codes; } = codes;
const { const {
validateInteger, validateInteger,
validateBoolean,
} = require('internal/validators'); } = require('internal/validators');
const { getTimerDuration } = require('internal/timers'); const { getTimerDuration } = require('internal/timers');
const { const {
@ -229,6 +230,12 @@ function ClientRequest(input, options, cb) {
} }
this.insecureHTTPParser = insecureHTTPParser; this.insecureHTTPParser = insecureHTTPParser;
if (options.joinDuplicateHeaders !== undefined) {
validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
}
this.joinDuplicateHeaders = options.joinDuplicateHeaders;
this.path = options.path || '/'; this.path = options.path || '/';
if (cb) { if (cb) {
this.once('response', cb); this.once('response', cb);
@ -811,6 +818,8 @@ function tickOnSocket(req, socket) {
parser.maxHeaderPairs = req.maxHeadersCount << 1; parser.maxHeaderPairs = req.maxHeadersCount << 1;
} }
parser.joinDuplicateHeaders = req.joinDuplicateHeaders;
parser.onIncoming = parserOnIncomingClient; parser.onIncoming = parserOnIncomingClient;
socket.on('error', socketErrorListener); socket.on('error', socketErrorListener);
socket.on('data', socketOnData); socket.on('data', socketOnData);

View File

@ -94,6 +94,8 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
incoming.httpVersionMajor = versionMajor; incoming.httpVersionMajor = versionMajor;
incoming.httpVersionMinor = versionMinor; incoming.httpVersionMinor = versionMinor;
incoming.httpVersion = `${versionMajor}.${versionMinor}`; incoming.httpVersion = `${versionMajor}.${versionMinor}`;
incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
parser.joinDuplicateHeaders;
incoming.url = url; incoming.url = url;
incoming.upgrade = upgrade; incoming.upgrade = upgrade;

View File

@ -75,7 +75,7 @@ function IncomingMessage(socket) {
this[kTrailers] = null; this[kTrailers] = null;
this[kTrailersCount] = 0; this[kTrailersCount] = 0;
this.rawTrailers = []; this.rawTrailers = [];
this.joinDuplicateHeaders = false;
this.aborted = false; this.aborted = false;
this.upgrade = null; this.upgrade = null;
@ -400,6 +400,16 @@ function _addHeaderLine(field, value, dest) {
} else { } else {
dest['set-cookie'] = [value]; dest['set-cookie'] = [value];
} }
} else if (this.joinDuplicateHeaders) {
// RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
// https://github.com/nodejs/node/issues/45699
// allow authorization multiple fields
// Make a delimited list
if (dest[field] === undefined) {
dest[field] = value;
} else {
dest[field] += ', ' + value;
}
} else if (dest[field] === undefined) { } else if (dest[field] === undefined) {
// Drop duplicates // Drop duplicates
dest[field] = value; dest[field] = value;

View File

@ -482,6 +482,12 @@ function storeHTTPOptions(options) {
} else { } else {
this.requireHostHeader = true; this.requireHostHeader = true;
} }
const joinDuplicateHeaders = options.joinDuplicateHeaders;
if (joinDuplicateHeaders !== undefined) {
validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
}
this.joinDuplicateHeaders = joinDuplicateHeaders;
} }
function setupConnectionsTracking(server) { function setupConnectionsTracking(server) {

View File

@ -52,7 +52,8 @@ let maxHeaderSize;
* ServerResponse?: ServerResponse; * ServerResponse?: ServerResponse;
* insecureHTTPParser?: boolean; * insecureHTTPParser?: boolean;
* maxHeaderSize?: number; * maxHeaderSize?: number;
* requireHostHeader?: boolean * requireHostHeader?: boolean;
* joinDuplicateHeaders?: boolean;
* }} [opts] * }} [opts]
* @param {Function} [requestListener] * @param {Function} [requestListener]
* @returns {Server} * @returns {Server}

View File

@ -0,0 +1,83 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
{
const server = http.createServer({
requireHostHeader: false,
joinDuplicateHeaders: true
}, common.mustCall((req, res) => {
assert.strictEqual(req.headers.authorization, '1, 2');
assert.strictEqual(req.headers.cookie, 'foo; bar');
res.writeHead(200, ['authorization', '3', 'authorization', '4', 'cookie', 'foo', 'cookie', 'bar']);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({
port: server.address().port,
headers: ['authorization', '1', 'authorization', '2', 'cookie', 'foo', 'cookie', 'bar'],
joinDuplicateHeaders: true
}, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.authorization, '3, 4');
assert.strictEqual(res.headers.cookie, 'foo; bar');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
// Server joinDuplicateHeaders false
const server = http.createServer({
requireHostHeader: false,
joinDuplicateHeaders: false
}, common.mustCall((req, res) => {
assert.strictEqual(req.headers.authorization, '1'); // non joined value
res.writeHead(200, ['authorization', '3', 'authorization', '4']);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({
port: server.address().port,
headers: ['authorization', '1', 'authorization', '2'],
joinDuplicateHeaders: true
}, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.authorization, '3, 4');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
// Client joinDuplicateHeaders false
const server = http.createServer({
requireHostHeader: false,
joinDuplicateHeaders: true
}, common.mustCall((req, res) => {
assert.strictEqual(req.headers.authorization, '1, 2');
res.writeHead(200, ['authorization', '3', 'authorization', '4']);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({
port: server.address().port,
headers: ['authorization', '1', 'authorization', '2'],
joinDuplicateHeaders: false
}, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.authorization, '3'); // non joined value
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}