Skip to content

Commit 4080bad

Browse files
http: join authorization headers
PR-URL: #45982 Fixes: #45699 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Paolo Insogna <[email protected]>
1 parent e35e893 commit 4080bad

File tree

7 files changed

+133
-2
lines changed

7 files changed

+133
-2
lines changed

doc/api/http.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,6 +2426,13 @@ as an argument to any listeners on the event.
24262426
<!-- YAML
24272427
added: v0.1.5
24282428
changes:
2429+
- version: REPLACEME
2430+
pr-url: https://github.com/nodejs/node/pull/45982
2431+
description: >-
2432+
The `joinDuplicateHeaders` option in the `http.request()`
2433+
and `http.createServer()` functions ensures that duplicate
2434+
headers are not discarded, but rather combined using a
2435+
comma separator, in accordance with RFC 9110 Section 5.3.
24292436
- version: v15.1.0
24302437
pr-url: https://github.com/nodejs/node/pull/35281
24312438
description: >-
@@ -2455,6 +2462,10 @@ header name:
24552462
`etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`,
24562463
`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`,
24572464
`retry-after`, `server`, or `user-agent` are discarded.
2465+
To allow duplicate values of the headers listed above to be joined,
2466+
use the option `joinDuplicateHeaders` in [`http.request()`][]
2467+
and [`http.createServer()`][]. See RFC 9110 Section 5.3 for more
2468+
information.
24582469
* `set-cookie` is always an array. Duplicates are added to the array.
24592470
* For duplicate `cookie` headers, the values are joined together with `; `.
24602471
* For all other headers, the values are joined together with `, `.
@@ -3186,6 +3197,10 @@ changes:
31863197
a 400 (Bad Request) status code to any HTTP/1.1 request message
31873198
that lacks a Host header (as mandated by the specification).
31883199
**Default:** `true`.
3200+
* `joinDuplicateHeaders` {boolean} It joins the field line values of multiple
3201+
headers in a request with `, ` instead of discarding the duplicates.
3202+
See [`message.headers`][] for more information.
3203+
**Default:** `false`.
31893204
* `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class
31903205
to be used. Useful for extending the original `ServerResponse`. **Default:**
31913206
`ServerResponse`.
@@ -3441,6 +3456,10 @@ changes:
34413456
* `uniqueHeaders` {Array} A list of request headers that should be sent
34423457
only once. If the header's value is an array, the items will be joined
34433458
using `; `.
3459+
* `joinDuplicateHeaders` {boolean} It joins the field line values of
3460+
multiple headers in a request with `, ` instead of discarding
3461+
the duplicates. See [`message.headers`][] for more information.
3462+
**Default:** `false`.
34443463
* `callback` {Function}
34453464
* Returns: {http.ClientRequest}
34463465

@@ -3754,6 +3773,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`.
37543773
[`http.IncomingMessage`]: #class-httpincomingmessage
37553774
[`http.ServerResponse`]: #class-httpserverresponse
37563775
[`http.Server`]: #class-httpserver
3776+
[`http.createServer()`]: #httpcreateserveroptions-requestlistener
37573777
[`http.get()`]: #httpgetoptions-callback
37583778
[`http.globalAgent`]: #httpglobalagent
37593779
[`http.request()`]: #httprequestoptions-callback

lib/_http_client.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const {
8282
} = codes;
8383
const {
8484
validateInteger,
85+
validateBoolean,
8586
} = require('internal/validators');
8687
const { getTimerDuration } = require('internal/timers');
8788
const {
@@ -229,6 +230,12 @@ function ClientRequest(input, options, cb) {
229230
}
230231
this.insecureHTTPParser = insecureHTTPParser;
231232

233+
if (options.joinDuplicateHeaders !== undefined) {
234+
validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
235+
}
236+
237+
this.joinDuplicateHeaders = options.joinDuplicateHeaders;
238+
232239
this.path = options.path || '/';
233240
if (cb) {
234241
this.once('response', cb);
@@ -811,6 +818,8 @@ function tickOnSocket(req, socket) {
811818
parser.maxHeaderPairs = req.maxHeadersCount << 1;
812819
}
813820

821+
parser.joinDuplicateHeaders = req.joinDuplicateHeaders;
822+
814823
parser.onIncoming = parserOnIncomingClient;
815824
socket.on('error', socketErrorListener);
816825
socket.on('data', socketOnData);

lib/_http_common.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
9494
incoming.httpVersionMajor = versionMajor;
9595
incoming.httpVersionMinor = versionMinor;
9696
incoming.httpVersion = `${versionMajor}.${versionMinor}`;
97+
incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
98+
parser.joinDuplicateHeaders;
9799
incoming.url = url;
98100
incoming.upgrade = upgrade;
99101

lib/_http_incoming.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function IncomingMessage(socket) {
7575
this[kTrailers] = null;
7676
this[kTrailersCount] = 0;
7777
this.rawTrailers = [];
78-
78+
this.joinDuplicateHeaders = false;
7979
this.aborted = false;
8080

8181
this.upgrade = null;
@@ -400,6 +400,16 @@ function _addHeaderLine(field, value, dest) {
400400
} else {
401401
dest['set-cookie'] = [value];
402402
}
403+
} else if (this.joinDuplicateHeaders) {
404+
// RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
405+
// https://github.com/nodejs/node/issues/45699
406+
// allow authorization multiple fields
407+
// Make a delimited list
408+
if (dest[field] === undefined) {
409+
dest[field] = value;
410+
} else {
411+
dest[field] += ', ' + value;
412+
}
403413
} else if (dest[field] === undefined) {
404414
// Drop duplicates
405415
dest[field] = value;

lib/_http_server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,12 @@ function storeHTTPOptions(options) {
482482
} else {
483483
this.requireHostHeader = true;
484484
}
485+
486+
const joinDuplicateHeaders = options.joinDuplicateHeaders;
487+
if (joinDuplicateHeaders !== undefined) {
488+
validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
489+
}
490+
this.joinDuplicateHeaders = joinDuplicateHeaders;
485491
}
486492

487493
function setupConnectionsTracking(server) {

lib/http.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ let maxHeaderSize;
5252
* ServerResponse?: ServerResponse;
5353
* insecureHTTPParser?: boolean;
5454
* maxHeaderSize?: number;
55-
* requireHostHeader?: boolean
55+
* requireHostHeader?: boolean;
56+
* joinDuplicateHeaders?: boolean;
5657
* }} [opts]
5758
* @param {Function} [requestListener]
5859
* @returns {Server}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
6+
{
7+
const server = http.createServer({
8+
requireHostHeader: false,
9+
joinDuplicateHeaders: true
10+
}, common.mustCall((req, res) => {
11+
assert.strictEqual(req.headers.authorization, '1, 2');
12+
assert.strictEqual(req.headers.cookie, 'foo; bar');
13+
res.writeHead(200, ['authorization', '3', 'authorization', '4', 'cookie', 'foo', 'cookie', 'bar']);
14+
res.end();
15+
}));
16+
17+
server.listen(0, common.mustCall(() => {
18+
http.get({
19+
port: server.address().port,
20+
headers: ['authorization', '1', 'authorization', '2', 'cookie', 'foo', 'cookie', 'bar'],
21+
joinDuplicateHeaders: true
22+
}, (res) => {
23+
assert.strictEqual(res.statusCode, 200);
24+
assert.strictEqual(res.headers.authorization, '3, 4');
25+
assert.strictEqual(res.headers.cookie, 'foo; bar');
26+
res.resume().on('end', common.mustCall(() => {
27+
server.close();
28+
}));
29+
});
30+
}));
31+
}
32+
33+
{
34+
// Server joinDuplicateHeaders false
35+
const server = http.createServer({
36+
requireHostHeader: false,
37+
joinDuplicateHeaders: false
38+
}, common.mustCall((req, res) => {
39+
assert.strictEqual(req.headers.authorization, '1'); // non joined value
40+
res.writeHead(200, ['authorization', '3', 'authorization', '4']);
41+
res.end();
42+
}));
43+
44+
server.listen(0, common.mustCall(() => {
45+
http.get({
46+
port: server.address().port,
47+
headers: ['authorization', '1', 'authorization', '2'],
48+
joinDuplicateHeaders: true
49+
}, (res) => {
50+
assert.strictEqual(res.statusCode, 200);
51+
assert.strictEqual(res.headers.authorization, '3, 4');
52+
res.resume().on('end', common.mustCall(() => {
53+
server.close();
54+
}));
55+
});
56+
}));
57+
}
58+
59+
{
60+
// Client joinDuplicateHeaders false
61+
const server = http.createServer({
62+
requireHostHeader: false,
63+
joinDuplicateHeaders: true
64+
}, common.mustCall((req, res) => {
65+
assert.strictEqual(req.headers.authorization, '1, 2');
66+
res.writeHead(200, ['authorization', '3', 'authorization', '4']);
67+
res.end();
68+
}));
69+
70+
server.listen(0, common.mustCall(() => {
71+
http.get({
72+
port: server.address().port,
73+
headers: ['authorization', '1', 'authorization', '2'],
74+
joinDuplicateHeaders: false
75+
}, (res) => {
76+
assert.strictEqual(res.statusCode, 200);
77+
assert.strictEqual(res.headers.authorization, '3'); // non joined value
78+
res.resume().on('end', common.mustCall(() => {
79+
server.close();
80+
}));
81+
});
82+
}));
83+
}

0 commit comments

Comments
 (0)