Skip to content

Commit

Permalink
http: join authorization headers
Browse files Browse the repository at this point in the history
PR-URL: #45982
Fixes: #45699
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
  • Loading branch information
marco-ippolito authored and juanarbol committed Jan 31, 2023
1 parent 41f5a29 commit 4df1fcc
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 1 deletion.
20 changes: 20 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,13 @@ as an argument to any listeners on the event.
<!-- YAML
added: v0.1.5
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
pr-url: https://github.com/nodejs/node/pull/35281
description: >-
Expand Down Expand Up @@ -2442,6 +2449,10 @@ header name:
`etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`,
`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`,
`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.
* For duplicate `cookie` headers, the values are joined together with `; `.
* For all other headers, the values are joined together with `, `.
Expand Down Expand Up @@ -3149,6 +3160,10 @@ changes:
* `requestTimeout`: Sets the timeout value in milliseconds for receiving
the entire request from the client.
See [`server.requestTimeout`][] for more information.
* `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
to be used. Useful for extending the original `ServerResponse`. **Default:**
`ServerResponse`.
Expand Down Expand Up @@ -3399,6 +3414,10 @@ changes:
* `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
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}
* Returns: {http.ClientRequest}

Expand Down Expand Up @@ -3710,6 +3729,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`.
[`http.IncomingMessage`]: #class-httpincomingmessage
[`http.ServerResponse`]: #class-httpserverresponse
[`http.Server`]: #class-httpserver
[`http.createServer()`]: #httpcreateserveroptions-requestlistener
[`http.get()`]: #httpgetoptions-callback
[`http.globalAgent`]: #httpglobalagent
[`http.request()`]: #httprequestoptions-callback
Expand Down
9 changes: 9 additions & 0 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const {
} = codes;
const {
validateInteger,
validateBoolean,
} = require('internal/validators');
const { getTimerDuration } = require('internal/timers');
const {
Expand Down Expand Up @@ -234,6 +235,12 @@ function ClientRequest(input, options, cb) {
}
this.insecureHTTPParser = insecureHTTPParser;

if (options.joinDuplicateHeaders !== undefined) {
validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
}

this.joinDuplicateHeaders = options.joinDuplicateHeaders;

this.path = options.path || '/';
if (cb) {
this.once('response', cb);
Expand Down Expand Up @@ -818,6 +825,8 @@ function tickOnSocket(req, socket) {
parser.maxHeaderPairs = req.maxHeadersCount << 1;
}

parser.joinDuplicateHeaders = req.joinDuplicateHeaders;

parser.onIncoming = parserOnIncomingClient;
socket.on('error', socketErrorListener);
socket.on('data', socketOnData);
Expand Down
2 changes: 2 additions & 0 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
incoming.httpVersionMajor = versionMajor;
incoming.httpVersionMinor = versionMinor;
incoming.httpVersion = `${versionMajor}.${versionMinor}`;
incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
parser.joinDuplicateHeaders;
incoming.url = url;
incoming.upgrade = upgrade;

Expand Down
12 changes: 11 additions & 1 deletion lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function IncomingMessage(socket) {
this[kTrailers] = null;
this[kTrailersCount] = 0;
this.rawTrailers = [];

this.joinDuplicateHeaders = false;
this.aborted = false;

this.upgrade = null;
Expand Down Expand Up @@ -400,6 +400,16 @@ function _addHeaderLine(field, value, dest) {
} else {
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) {
// Drop duplicates
dest[field] = value;
Expand Down
6 changes: 6 additions & 0 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ function storeHTTPOptions(options) {
} else {
this.connectionsCheckingInterval = 30_000; // 30 seconds
}

const joinDuplicateHeaders = options.joinDuplicateHeaders;
if (joinDuplicateHeaders !== undefined) {
validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
}
this.joinDuplicateHeaders = joinDuplicateHeaders;
}

function setupConnectionsTracking(server) {
Expand Down
1 change: 1 addition & 0 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ let maxHeaderSize;
* ServerResponse?: ServerResponse;
* insecureHTTPParser?: boolean;
* maxHeaderSize?: number;
* joinDuplicateHeaders?: boolean;
* }} [opts]
* @param {Function} [requestListener]
* @returns {Server}
Expand Down
83 changes: 83 additions & 0 deletions test/parallel/test-http-request-join-authorization-headers.js
Original file line number Diff line number Diff line change
@@ -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();
}));
});
}));
}

0 comments on commit 4df1fcc

Please sign in to comment.