http: concatenate outgoing Cookie headers

This commit enables automatic concatenation of multiple Cookie header
values with a semicolon, except when 2D header arrays are used.

Fixes: https://github.com/nodejs/node/issues/11256
PR-URL: https://github.com/nodejs/node/pull/11259
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
This commit is contained in:
Brian White 2017-02-10 20:32:32 -05:00
parent 6b2cef65c9
commit d3480776c7
No known key found for this signature in database
GPG Key ID: 606D7358F94DA209
2 changed files with 112 additions and 58 deletions

View File

@ -20,6 +20,27 @@ var RE_FIELDS = new RegExp('^(?:Connection|Transfer-Encoding|Content-Length|' +
var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig; var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig;
var RE_TE_CHUNKED = common.chunkExpression; var RE_TE_CHUNKED = common.chunkExpression;
// isCookieField performs a case-insensitive comparison of a provided string
// against the word "cookie." This method (at least as of V8 5.4) is faster than
// the equivalent case-insensitive regexp, even if isCookieField does not get
// inlined.
function isCookieField(s) {
if (s.length !== 6) return false;
var ch = s.charCodeAt(0);
if (ch !== 99 && ch !== 67) return false;
ch = s.charCodeAt(1);
if (ch !== 111 && ch !== 79) return false;
ch = s.charCodeAt(2);
if (ch !== 111 && ch !== 79) return false;
ch = s.charCodeAt(3);
if (ch !== 107 && ch !== 75) return false;
ch = s.charCodeAt(4);
if (ch !== 105 && ch !== 73) return false;
ch = s.charCodeAt(5);
if (ch !== 101 && ch !== 69) return false;
return true;
}
var dateCache; var dateCache;
function utcDate() { function utcDate() {
if (!dateCache) { if (!dateCache) {
@ -275,13 +296,15 @@ function _storeHeader(firstLine, headers) {
value = entry[1]; value = entry[1];
if (value instanceof Array) { if (value instanceof Array) {
for (j = 0; j < value.length; j++) { if (value.length < 2 || !isCookieField(field)) {
for (j = 0; j < value.length; j++)
storeHeader(this, state, field, value[j], false); storeHeader(this, state, field, value[j], false);
continue;
}
value = value.join('; ');
} }
} else {
storeHeader(this, state, field, value, false); storeHeader(this, state, field, value, false);
} }
}
} else if (headers instanceof Array) { } else if (headers instanceof Array) {
for (i = 0; i < headers.length; i++) { for (i = 0; i < headers.length; i++) {
field = headers[i][0]; field = headers[i][0];
@ -302,14 +325,16 @@ function _storeHeader(firstLine, headers) {
value = headers[field]; value = headers[field];
if (value instanceof Array) { if (value instanceof Array) {
for (j = 0; j < value.length; j++) { if (value.length < 2 || !isCookieField(field)) {
for (j = 0; j < value.length; j++)
storeHeader(this, state, field, value[j], true); storeHeader(this, state, field, value[j], true);
continue;
}
value = value.join('; ');
} }
} else {
storeHeader(this, state, field, value, true); storeHeader(this, state, field, value, true);
} }
} }
}
// Are we upgrading the connection? // Are we upgrading the connection?
if (state.connUpgrade && state.upgrade) if (state.connUpgrade && state.upgrade)

View File

@ -1,84 +1,113 @@
'use strict'; 'use strict';
require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
let responses_sent = 0; const expectedRequests = ['/hello', '/there', '/world'];
let responses_recvd = 0;
let body0 = '';
let body1 = '';
const server = http.Server(function(req, res) { const server = http.Server(common.mustCall(function(req, res) {
if (responses_sent === 0) { assert.strictEqual(expectedRequests.shift(), req.url);
assert.strictEqual('GET', req.method);
assert.strictEqual('/hello', url.parse(req.url).pathname);
console.dir(req.headers); switch (req.url) {
assert.strictEqual(true, 'accept' in req.headers); case '/hello':
assert.strictEqual('*/*', req.headers['accept']); assert.strictEqual(req.method, 'GET');
assert.strictEqual(req.headers['accept'], '*/*');
assert.strictEqual(true, 'foo' in req.headers); assert.strictEqual(req.headers['foo'], 'bar');
assert.strictEqual('bar', req.headers['foo']); assert.strictEqual(req.headers.cookie, 'foo=bar; bar=baz; baz=quux');
break;
case '/there':
assert.strictEqual(req.method, 'PUT');
assert.strictEqual(req.headers.cookie, 'node=awesome; ta=da');
break;
case '/world':
assert.strictEqual(req.method, 'POST');
assert.deepStrictEqual(req.headers.cookie, 'abc=123; def=456; ghi=789');
break;
default:
assert(false, `Unexpected request for ${req.url}`);
} }
if (responses_sent === 1) { if (expectedRequests.length === 0)
assert.strictEqual('POST', req.method);
assert.strictEqual('/world', url.parse(req.url).pathname);
this.close(); this.close();
}
req.on('end', function() { req.on('end', function() {
res.writeHead(200, {'Content-Type': 'text/plain'}); res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('The path was ' + url.parse(req.url).pathname); res.write('The path was ' + url.parse(req.url).pathname);
res.end(); res.end();
responses_sent += 1;
}); });
req.resume(); req.resume();
}, 3));
//assert.strictEqual('127.0.0.1', res.connection.remoteAddress);
});
server.listen(0); server.listen(0);
server.on('listening', function() { server.on('listening', function() {
const agent = new http.Agent({ port: this.address().port, maxSockets: 1 }); const agent = new http.Agent({ port: this.address().port, maxSockets: 1 });
http.get({ const req = http.get({
port: this.address().port, port: this.address().port,
path: '/hello', path: '/hello',
headers: {'Accept': '*/*', 'Foo': 'bar'}, headers: {
Accept: '*/*',
Foo: 'bar',
Cookie: [ 'foo=bar', 'bar=baz', 'baz=quux' ]
},
agent: agent agent: agent
}, function(res) { }, common.mustCall(function(res) {
assert.strictEqual(200, res.statusCode); const cookieHeaders = req._header.match(/^Cookie: .+$/img);
responses_recvd += 1; assert.deepStrictEqual(cookieHeaders,
['Cookie: foo=bar; bar=baz; baz=quux']);
assert.strictEqual(res.statusCode, 200);
let body = '';
res.setEncoding('utf8'); res.setEncoding('utf8');
res.on('data', function(chunk) { body0 += chunk; }); res.on('data', function(chunk) { body += chunk; });
console.error('Got /hello response'); res.on('end', common.mustCall(function() {
}); assert.strictEqual(body, 'The path was /hello');
}));
}));
setTimeout(function() { setTimeout(common.mustCall(function() {
const req = http.request({
port: server.address().port,
method: 'PUT',
path: '/there',
agent: agent
}, common.mustCall(function(res) {
const cookieHeaders = req._header.match(/^Cookie: .+$/img);
assert.deepStrictEqual(cookieHeaders, ['Cookie: node=awesome; ta=da']);
assert.strictEqual(res.statusCode, 200);
let body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) { body += chunk; });
res.on('end', common.mustCall(function() {
assert.strictEqual(body, 'The path was /there');
}));
}));
req.setHeader('Cookie', ['node=awesome', 'ta=da']);
req.end();
}), 1);
setTimeout(common.mustCall(function() {
const req = http.request({ const req = http.request({
port: server.address().port, port: server.address().port,
method: 'POST', method: 'POST',
path: '/world', path: '/world',
headers: [ ['Cookie', 'abc=123'],
['Cookie', 'def=456'],
['Cookie', 'ghi=789'] ],
agent: agent agent: agent
}, function(res) { }, common.mustCall(function(res) {
assert.strictEqual(200, res.statusCode); const cookieHeaders = req._header.match(/^Cookie: .+$/img);
responses_recvd += 1; assert.deepStrictEqual(cookieHeaders,
['Cookie: abc=123',
'Cookie: def=456',
'Cookie: ghi=789']);
assert.strictEqual(res.statusCode, 200);
let body = '';
res.setEncoding('utf8'); res.setEncoding('utf8');
res.on('data', function(chunk) { body1 += chunk; }); res.on('data', function(chunk) { body += chunk; });
console.error('Got /world response'); res.on('end', common.mustCall(function() {
}); assert.strictEqual(body, 'The path was /world');
}));
}));
req.end(); req.end();
}, 1); }), 2);
});
process.on('exit', function() {
console.error('responses_recvd: ' + responses_recvd);
assert.strictEqual(2, responses_recvd);
console.error('responses_sent: ' + responses_sent);
assert.strictEqual(2, responses_sent);
assert.strictEqual('The path was /hello', body0);
assert.strictEqual('The path was /world', body1);
}); });