Instantly share code, notes, and snippets.
Created
April 29, 2020 12:20
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save DinoChiesa/cad04fc62be91e6c9a31469b09c4e1b8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks, helpful.