Skip to content

Commit

Permalink
http: concatenate outgoing Cookie headers
Browse files Browse the repository at this point in the history
This commit enables automatic concatenation of multiple Cookie header
values with a semicolon, except when 2D header arrays are used.

Fixes: nodejs#11256
PR-URL: nodejs#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>
  • Loading branch information
mscdex authored and jungx098 committed Mar 21, 2017
1 parent 1b896a6 commit 7f26c8a
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 58 deletions.
41 changes: 33 additions & 8 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_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;
function utcDate() {
if (!dateCache) {
Expand Down Expand Up @@ -275,12 +296,14 @@ function _storeHeader(firstLine, headers) {
value = entry[1];

if (value instanceof Array) {
for (j = 0; j < value.length; j++) {
storeHeader(this, state, field, value[j], false);
if (value.length < 2 || !isCookieField(field)) {
for (j = 0; j < value.length; j++)
storeHeader(this, state, field, value[j], false);
continue;
}
} else {
storeHeader(this, state, field, value, false);
value = value.join('; ');
}
storeHeader(this, state, field, value, false);
}
} else if (headers instanceof Array) {
for (i = 0; i < headers.length; i++) {
Expand All @@ -302,12 +325,14 @@ function _storeHeader(firstLine, headers) {
value = headers[field];

if (value instanceof Array) {
for (j = 0; j < value.length; j++) {
storeHeader(this, state, field, value[j], true);
if (value.length < 2 || !isCookieField(field)) {
for (j = 0; j < value.length; j++)
storeHeader(this, state, field, value[j], true);
continue;
}
} else {
storeHeader(this, state, field, value, true);
value = value.join('; ');
}
storeHeader(this, state, field, value, true);
}
}

Expand Down
129 changes: 79 additions & 50 deletions test/parallel/test-http.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,113 @@
'use strict';
require('../common');
const common = require('../common');
const assert = require('assert');
const http = require('http');
const url = require('url');

let responses_sent = 0;
let responses_recvd = 0;
let body0 = '';
let body1 = '';
const expectedRequests = ['/hello', '/there', '/world'];

const server = http.Server(function(req, res) {
if (responses_sent === 0) {
assert.strictEqual('GET', req.method);
assert.strictEqual('/hello', url.parse(req.url).pathname);
const server = http.Server(common.mustCall(function(req, res) {
assert.strictEqual(expectedRequests.shift(), req.url);

console.dir(req.headers);
assert.strictEqual(true, 'accept' in req.headers);
assert.strictEqual('*/*', req.headers['accept']);

assert.strictEqual(true, 'foo' in req.headers);
assert.strictEqual('bar', req.headers['foo']);
switch (req.url) {
case '/hello':
assert.strictEqual(req.method, 'GET');
assert.strictEqual(req.headers['accept'], '*/*');
assert.strictEqual(req.headers['foo'], 'bar');
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) {
assert.strictEqual('POST', req.method);
assert.strictEqual('/world', url.parse(req.url).pathname);
if (expectedRequests.length === 0)
this.close();
}

req.on('end', function() {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('The path was ' + url.parse(req.url).pathname);
res.end();
responses_sent += 1;
});
req.resume();

//assert.strictEqual('127.0.0.1', res.connection.remoteAddress);
});
}, 3));
server.listen(0);

server.on('listening', function() {
const agent = new http.Agent({ port: this.address().port, maxSockets: 1 });
http.get({
const req = http.get({
port: this.address().port,
path: '/hello',
headers: {'Accept': '*/*', 'Foo': 'bar'},
headers: {
Accept: '*/*',
Foo: 'bar',
Cookie: [ 'foo=bar', 'bar=baz', 'baz=quux' ]
},
agent: agent
}, function(res) {
assert.strictEqual(200, res.statusCode);
responses_recvd += 1;
}, common.mustCall(function(res) {
const cookieHeaders = req._header.match(/^Cookie: .+$/img);
assert.deepStrictEqual(cookieHeaders,
['Cookie: foo=bar; bar=baz; baz=quux']);
assert.strictEqual(res.statusCode, 200);
let body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) { body0 += chunk; });
console.error('Got /hello response');
});
res.on('data', function(chunk) { body += chunk; });
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({
port: server.address().port,
method: 'POST',
path: '/world',
headers: [ ['Cookie', 'abc=123'],
['Cookie', 'def=456'],
['Cookie', 'ghi=789'] ],
agent: agent
}, function(res) {
assert.strictEqual(200, res.statusCode);
responses_recvd += 1;
}, common.mustCall(function(res) {
const cookieHeaders = req._header.match(/^Cookie: .+$/img);
assert.deepStrictEqual(cookieHeaders,
['Cookie: abc=123',
'Cookie: def=456',
'Cookie: ghi=789']);
assert.strictEqual(res.statusCode, 200);
let body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) { body1 += chunk; });
console.error('Got /world response');
});
res.on('data', function(chunk) { body += chunk; });
res.on('end', common.mustCall(function() {
assert.strictEqual(body, 'The path was /world');
}));
}));
req.end();
}, 1);
});

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);
}), 2);
});

0 comments on commit 7f26c8a

Please sign in to comment.