GeezerVison is showing github.com — http: join authorization headers · nodejs/node@069a30b
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/http.md +20Lines changed: 20 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2426,6 +2426,13 @@ as an argument to any listeners on the event. | 2426 | 2426 | <!-- YAML | 2427 | 2427 | added: v0.1.5 | 2428 | 2428 | 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. | 2429 | 2436 | - version: v15.1.0 | 2430 | 2437 | pr-url: https://github.com/nodejs/node/pull/35281 | 2431 | 2438 | description: >- | @@ -2455,6 +2462,10 @@ header name: | 2455 | 2462 | `etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`, | 2456 | 2463 | `last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`, | 2457 | 2464 | `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. | 2458 | 2469 | * `set-cookie` is always an array. Duplicates are added to the array. | 2459 | 2470 | * For duplicate `cookie` headers, the values are joined together with `; `. | 2460 | 2471 | * For all other headers, the values are joined together with `, `. | @@ -3182,6 +3193,10 @@ changes: | 3182 | 3193 | * `requestTimeout`: Sets the timeout value in milliseconds for receiving | 3183 | 3194 | the entire request from the client. | 3184 | 3195 | See [`server.requestTimeout`][] for more information. | | 3196 | + * `joinDuplicateHeaders` {boolean} It joins the field line values of multiple | | 3197 | + headers in a request with `, ` instead of discarding the duplicates. | | 3198 | + See [`message.headers`][] for more information. | | 3199 | + **Default:** `false`. | 3185 | 3200 | * `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class | 3186 | 3201 | to be used. Useful for extending the original `ServerResponse`. **Default:** | 3187 | 3202 | `ServerResponse`. | @@ -3437,6 +3452,10 @@ changes: | 3437 | 3452 | * `uniqueHeaders` {Array} A list of request headers that should be sent | 3438 | 3453 | only once. If the header's value is an array, the items will be joined | 3439 | 3454 | using `; `. | | 3455 | + * `joinDuplicateHeaders` {boolean} It joins the field line values of | | 3456 | + multiple headers in a request with `, ` instead of discarding | | 3457 | + the duplicates. See [`message.headers`][] for more information. | | 3458 | + **Default:** `false`. | 3440 | 3459 | * `callback` {Function} | 3441 | 3460 | * Returns: {http.ClientRequest} | 3442 | 3461 |
| @@ -3750,6 +3769,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`. | 3750 | 3769 | [`http.IncomingMessage`]: #class-httpincomingmessage | 3751 | 3770 | [`http.ServerResponse`]: #class-httpserverresponse | 3752 | 3771 | [`http.Server`]: #class-httpserver | | 3772 | +[`http.createServer()`]: #httpcreateserveroptions-requestlistener | 3753 | 3773 | [`http.get()`]: #httpgetoptions-callback | 3754 | 3774 | [`http.globalAgent`]: #httpglobalagent | 3755 | 3775 | [`http.request()`]: #httprequestoptions-callback | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_client.js +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -82,6 +82,7 @@ const { | 82 | 82 | } = codes; | 83 | 83 | const { | 84 | 84 | validateInteger, | | 85 | + validateBoolean, | 85 | 86 | } = require('internal/validators'); | 86 | 87 | const { getTimerDuration } = require('internal/timers'); | 87 | 88 | const { | @@ -229,6 +230,12 @@ function ClientRequest(input, options, cb) { | 229 | 230 | } | 230 | 231 | this.insecureHTTPParser = insecureHTTPParser; | 231 | 232 |
| | 233 | + if (options.joinDuplicateHeaders !== undefined) { | | 234 | + validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders'); | | 235 | + } | | 236 | + | | 237 | + this.joinDuplicateHeaders = options.joinDuplicateHeaders; | | 238 | + | 232 | 239 | this.path = options.path || '/'; | 233 | 240 | if (cb) { | 234 | 241 | this.once('response', cb); | @@ -811,6 +818,8 @@ function tickOnSocket(req, socket) { | 811 | 818 | parser.maxHeaderPairs = req.maxHeadersCount << 1; | 812 | 819 | } | 813 | 820 |
| | 821 | + parser.joinDuplicateHeaders = req.joinDuplicateHeaders; | | 822 | + | 814 | 823 | parser.onIncoming = parserOnIncomingClient; | 815 | 824 | socket.on('error', socketErrorListener); | 816 | 825 | socket.on('data', socketOnData); | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_common.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -94,6 +94,8 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, | 94 | 94 | incoming.httpVersionMajor = versionMajor; | 95 | 95 | incoming.httpVersionMinor = versionMinor; | 96 | 96 | incoming.httpVersion = `${versionMajor}.${versionMinor}`; | | 97 | + incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders || | | 98 | + parser.joinDuplicateHeaders; | 97 | 99 | incoming.url = url; | 98 | 100 | incoming.upgrade = upgrade; | 99 | 101 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_incoming.js +11-1Lines changed: 11 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -75,7 +75,7 @@ function IncomingMessage(socket) { | 75 | 75 | this[kTrailers] = null; | 76 | 76 | this[kTrailersCount] = 0; | 77 | 77 | this.rawTrailers = []; | 78 | | - | | 78 | + this.joinDuplicateHeaders = false; | 79 | 79 | this.aborted = false; | 80 | 80 |
| 81 | 81 | this.upgrade = null; | @@ -400,6 +400,16 @@ function _addHeaderLine(field, value, dest) { | 400 | 400 | } else { | 401 | 401 | dest['set-cookie'] = [value]; | 402 | 402 | } | | 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 | + } | 403 | 413 | } else if (dest[field] === undefined) { | 404 | 414 | // Drop duplicates | 405 | 415 | dest[field] = value; | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_server.js +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -469,6 +469,12 @@ function storeHTTPOptions(options) { | 469 | 469 | } else { | 470 | 470 | this.connectionsCheckingInterval = 30_000; // 30 seconds | 471 | 471 | } | | 472 | + | | 473 | + const joinDuplicateHeaders = options.joinDuplicateHeaders; | | 474 | + if (joinDuplicateHeaders !== undefined) { | | 475 | + validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders'); | | 476 | + } | | 477 | + this.joinDuplicateHeaders = joinDuplicateHeaders; | 472 | 478 | } | 473 | 479 |
| 474 | 480 | function setupConnectionsTracking(server) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/http.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -52,6 +52,7 @@ let maxHeaderSize; | 52 | 52 | * ServerResponse?: ServerResponse; | 53 | 53 | * insecureHTTPParser?: boolean; | 54 | 54 | * maxHeaderSize?: number; | | 55 | + * joinDuplicateHeaders?: boolean; | 55 | 56 | * }} [opts] | 56 | 57 | * @param {Function} [requestListener] | 57 | 58 | * @returns {Server} | |
Collapse file Copy file name to clipboard +80Lines changed: 80 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | + joinDuplicateHeaders: true | | 9 | + }, common.mustCall((req, res) => { | | 10 | + assert.strictEqual(req.headers.authorization, '1, 2'); | | 11 | + assert.strictEqual(req.headers.cookie, 'foo; bar'); | | 12 | + res.writeHead(200, ['authorization', '3', 'authorization', '4', 'cookie', 'foo', 'cookie', 'bar']); | | 13 | + res.end(); | | 14 | + })); | | 15 | + | | 16 | + server.listen(0, common.mustCall(() => { | | 17 | + http.get({ | | 18 | + port: server.address().port, | | 19 | + headers: ['authorization', '1', 'authorization', '2', 'cookie', 'foo', 'cookie', 'bar'], | | 20 | + joinDuplicateHeaders: true | | 21 | + }, (res) => { | | 22 | + assert.strictEqual(res.statusCode, 200); | | 23 | + assert.strictEqual(res.headers.authorization, '3, 4'); | | 24 | + assert.strictEqual(res.headers.cookie, 'foo; bar'); | | 25 | + res.resume().on('end', common.mustCall(() => { | | 26 | + server.close(); | | 27 | + })); | | 28 | + }); | | 29 | + })); | | 30 | +} | | 31 | + | | 32 | +{ | | 33 | + // Server joinDuplicateHeaders false | | 34 | + const server = http.createServer({ | | 35 | + joinDuplicateHeaders: false | | 36 | + }, common.mustCall((req, res) => { | | 37 | + assert.strictEqual(req.headers.authorization, '1'); // non joined value | | 38 | + res.writeHead(200, ['authorization', '3', 'authorization', '4']); | | 39 | + res.end(); | | 40 | + })); | | 41 | + | | 42 | + server.listen(0, common.mustCall(() => { | | 43 | + http.get({ | | 44 | + port: server.address().port, | | 45 | + headers: ['authorization', '1', 'authorization', '2'], | | 46 | + joinDuplicateHeaders: true | | 47 | + }, (res) => { | | 48 | + assert.strictEqual(res.statusCode, 200); | | 49 | + assert.strictEqual(res.headers.authorization, '3, 4'); | | 50 | + res.resume().on('end', common.mustCall(() => { | | 51 | + server.close(); | | 52 | + })); | | 53 | + }); | | 54 | + })); | | 55 | +} | | 56 | + | | 57 | +{ | | 58 | + // Client joinDuplicateHeaders false | | 59 | + const server = http.createServer({ | | 60 | + joinDuplicateHeaders: true | | 61 | + }, common.mustCall((req, res) => { | | 62 | + assert.strictEqual(req.headers.authorization, '1, 2'); | | 63 | + res.writeHead(200, ['authorization', '3', 'authorization', '4']); | | 64 | + res.end(); | | 65 | + })); | | 66 | + | | 67 | + server.listen(0, common.mustCall(() => { | | 68 | + http.get({ | | 69 | + port: server.address().port, | | 70 | + headers: ['authorization', '1', 'authorization', '2'], | | 71 | + joinDuplicateHeaders: false | | 72 | + }, (res) => { | | 73 | + assert.strictEqual(res.statusCode, 200); | | 74 | + assert.strictEqual(res.headers.authorization, '3'); // non joined value | | 75 | + res.resume().on('end', common.mustCall(() => { | | 76 | + server.close(); | | 77 | + })); | | 78 | + }); | | 79 | + })); | | 80 | +} |
|