Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Created April 29, 2020 12:20
Show Gist options
  • Save DinoChiesa/cad04fc62be91e6c9a31469b09c4e1b8 to your computer and use it in GitHub Desktop.
Save DinoChiesa/cad04fc62be91e6c9a31469b09c4e1b8 to your computer and use it in GitHub Desktop.
// fixupCookies.js
// ------------------------------------------------------------------
//
// Intended to modify cookies within an API Proxy to
// add SameSite=None; Secure to each cookie.
//
// Implements a parser to extract cookies from a header.
//
// This is necessary because Apigee folds all the
// Set-Cookie header values into one, which isn't right
// according to RFC 6265.
//
// created: Tue Apr 21 15:59:55 2020
// last saved: <2020-April-22 11:09:08>
/* jshint node:false, esversion:6 */
/* global context, print */
var ParseState = {
BEGIN : 0,
NAME : 1,
VALUE : 2,
SEPARATOR : 3,
VALUE_OR_OPENQUOTE : 4,
VALUE_OR_CLOSEQUOTE : 5,
EXPIRES : 6
};
var parseSetCookieHeader = function(str, wantRaw) {
var content = str.trim(),
state = ParseState.NAME,
name = '',
value = '',
parsed = {},
cookies = [],
i = 0;
do {
var c = content.charAt(i);
//console.log('state: ' + Number(state));
switch (Number(state)) {
case ParseState.NAME:
if (c === '=') {
if (parsed[name])
throw new Error('duplicate param at position ' + i);
state = (name == 'Expires') ? ParseState.EXPIRES : ParseState.VALUE_OR_OPENQUOTE;
value = '';
//console.log('name: ' + name);
//console.log('state: ' + state);
}
else if (c === ';') {
//console.log('value end, value: ' + value);
parsed[name] = '';
name = '';
value = '';
state = ParseState.NAME;
}
else if (c === ' ') { // allow ws
if (name != '') {
throw new Error('whitespace in name at position ' + i);
}
}
else {
name += c;
}
break;
case ParseState.EXPIRES:
if (c === ';') {
parsed[name] = value;
value = '';
name = '';
state = ParseState.NAME;
} else {
value += c;
}
break;
case ParseState.VALUE_OR_OPENQUOTE:
value = c;
if (c === '"') {
//console.log('open quote');
state = ParseState.VALUE_OR_CLOSEQUOTE;
} else {
state = ParseState.VALUE;
}
break;
case ParseState.VALUE:
if (name.length == 0) {
throw new Error('bad param name at posn ' + i);
}
if (c === ';') {
//console.log('value end, value: ' + value);
parsed[name] = value;
name = '';
value = '';
state = ParseState.NAME;
} else if (c === ',') {
parsed[name] = value;
name = '';
value = '';
cookies.push(parsed);
parsed = {};
state = ParseState.NAME;
} else {
value += c;
}
break;
case ParseState.VALUE_OR_CLOSEQUOTE:
if (name.length == 0) {
throw new Error('bad param name at posn ' + i);
}
if (c === '"') {
value += c;
//console.log('close quote, value: ' + value);
parsed[name] = value;
name = '';
value = '';
state = ParseState.SEPARATOR;
} else {
value += c;
}
break;
case ParseState.SEPARATOR:
if (c === ';') {
state = ParseState.NAME;
} else if (c === ',') {
cookies.push(parsed);
parsed = {};
state = ParseState.NAME;
} else if (c === ' ') {
// do nothing with WS
} else {
throw new Error('bad param format');
}
break;
default:
throw new Error('Invalid format at posn ' + i);
}
i++;
} while (i < content.length);
if (name != '') {
parsed[name] = value;
cookies.push(parsed);
}
if (wantRaw) {
var raw = {};
cookies.forEach(function(c) {
var keys = Object.keys(c);
raw[keys[0]] = c;
});
return raw;
}
return cookies.map(function(c) {
return Object.keys(c).map(function(key){
var v = c[key];
return (v == '') ? key : (key + '=' + v);
}).join('; ');
});
};
var headerName = 'response.header.Set-Cookie.values.string';
var H = context.getVariable(headerName);
var cookies = parseSetCookieHeader(H, true);
// cookies now contains parsed cookie objects
// Now we can manipulate those as appropriate:
// - remapping paths or domains.
// - modifying expiry
// - adding or removing cookies.
// - adding properties to each cookie
//
// The following gives an example.
var newcookies = [];
Object.keys(cookies).forEach(function(key) {
// Add SameSite=None and Secure to each cookie,
// but exclude AppDynamics cookies.
if ( ! key.startsWith('ADRUM')) {
var c = cookies[key];
//console.log('cookie: ' + JSON.stringify(c));
var keys = Object.keys(c);
//console.log('keys: ' + JSON.stringify(keys));
if ( Object.keys(c).indexOf('Secure') == -1 ) {
c.Secure = '';
}
c.SameSite = 'None';
keys = Object.keys(c);
keys.sort(function(e1, e2) {
// order Path last and Secure next to last.
if (e1 == 'Path') return 1;
if (e2 == 'Path') return -1;
if (e1 == 'Secure') return 1;
if (e2 == 'Secure') return -1;
return 0;
});
newcookies.push(keys.map(function(key){
var v = c[key];
return (v == '') ? key : (key + '=' + v);
}).join('; '));
}
});
// RFC 6265, Section 3:
//
// An origin server can include multiple Set-Cookie header fields in a single
// response. The presence of a Cookie or a Set-Cookie header field does not
// preclude HTTP caches from storing and reusing a response.
//
// Origin servers SHOULD NOT fold multiple Set-Cookie header fields into a
// single header field. The usual mechanism for folding HTTP headers fields
// (i.e., as defined in [RFC2616]) might change the semantics of the Set-Cookie
// header field because the %x2C (",") character is used by Set-Cookie in a way
// that conflicts with such folding.
// NO
// context.setVariable('updated_set_cookie', newcookies.join(', '));
newcookies.forEach(function(c, ix){
context.setVariable('updated_cookie_' + ix, c);
// context.setVariable('message.header.Set-Cookie.' + (ix+1), c);
});
@DinoChiesa
Copy link
Author

Thanks, helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment