Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Created April 29, 2020 12:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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);
});
@sujnanarai
Copy link

sujnanarai commented May 8, 2020

Hi Dino,

I am getting cookie value like below from backend service. The parsing failed with error whitespace in name at position 354. Is there any issue in cookie?

AWSALB=x8C2TBnkGNlmjDU+qbcWrzGO6DMdcH3LaLv4ltIeahenQx4+16eTF8Pnz1wcDsAoQfZIMpHoX4nwRfVhEEy0y6aoUTD7hz6hHsuaEEWIuwem0q/kqyuBsVZKuMpu; Max-Age=604800; Path=/digital, AWSALBCORS=x7C2TBnkGNlmjDU+qbcWrzGO6DMdcH2LaLv6ltIeahenQx2+15eTF8Pnz5wcDsAoQfZIMpHoX1nwRfVhEEy0y6aoUTD7hz6hHsuaEEWIuwem0q/kqyuBsVZKuMpu; Max-Age=604800; Path=/digital; SameSite=None; Secure, JSESSIONID=9C3A1D2E1DA5F83ECA01QC2A43C01W2D; Path=/digital; HttpOnly

Thank,
Sujnan,

@sujnanarai
Copy link

sujnanarai commented May 9, 2020

Modified the parseSetCookieHeader function to handle some additional combinations. Looks like it works fine. I have tested for following combinations

//Last cookie ends with HttpOnly
var H = "AWSALB=lnTSVTHpyba6/qH2QWVOVQWEuJjwwww5yt/l628OSKMZtEmZaGHFQojsda11Tl7cn2lOqmXAEWpTsaMyFcd6oPYNAmGZCKpc7BbGi4B+8ZQzoxw0SqbFqw; Expires=Thu,26 Jan 2020 12:21:46 GMT; Path=/,JSESSIONID=B1D80E6395A35D4210B9F254835CC627; Path=/ED; HttpOnly"

//First Cookie ends with HttpOnly
var H = "JSESSIONID=B1D80E6395A3ghfghfgnfgf54835CC627; Path=/ED; HttpOnly, AWSALB=lnTSVTHpyba6/qH2QWVjhghjghjghkBnpBP5yt/l628OSKMZtEmZaGHFQojsda11Tl7cn2lOqmXAEWpTsaMyFcd6oPYNAmGZCKpc7BbGi4B+8ZQzoxw0SqbFqw; Expires=Thu,26 Jan 2020 12:21:46 GMT; Path=/"

//First cookie ends with Expires
var H = "AWSALB=lnTSVTHpyba6/qH2QWVOVhkjhkkjhkBnpBP5yt/l628OSKMZtEmZaGHFQojsda11Tl7cn2lOqmXAEWpTsaMyFcd6oPYNAmGZCKpc7BbGi4B+8ZQzoxw0SqbFqw; Path=/; Expires=Thu,26 Jan 2020 12:21:46 GMT, JSESSIONID=B1D80E6395A35D4210B9F254835CC627; Path=/ED; HttpOnly"

//Last cookie ends with Expires
var H = "JSESSIONID=B1D80E6ASFSFSFSF0B9F254835CC627; Path=/ED; HttpOnly, AWSALB=lnTSVTHpyba6/qH2QWVOASfSFSFFi1kBnpBP5yt/l628OSKMZtEmZaGHFQojsda11Tl7cn2lOqmXAEWpTsaMyFcd6oPYNAmGZCKpc7BbGi4B+8ZQzoxw0SqbFqw; Path=/; Expires=Thu,26 Jan 2020 12:21:46 GMT"

//one of the cookie ends with secure
var H = "AWSALB=x8C2TBnkGNlmjDU+qbcWrzGO6DMdcDAddffgfghfhfg4+16eTF8Pnz1wcDsAoQfZIMpHoX4nwRfVhEEy0y6aoUTD7hz6hHsuaEEWIuwem0q/kqyuBsVZKuMpu; Max-Age=604800; Path=/digital, AWSALBCORS=x7C2TBnkGNlmjDU+qbcWrzGOdgdfdfdfgf6ltIeahenQx2+15eTF8Pnz5wcDsAoQfZIMpHoX1nwRfVhEEy0y6aoUTD7hz6hHsuaEEWIuwem0q/kqyuBsVZKuMpu; Max-Age=604800; Path=/digital; SameSite=None; Secure, JSESSIONID=9C3A1D2E1DA5FSAASASSADC2A43C01W2D; Path=/digital; HttpOnly"

Here is updated function--


var ParseState = {
      BEGIN : 0,
      NAME : 1,
      VALUE : 2,
      SEPARATOR : 3,
      VALUE_OR_OPENQUOTE : 4,
      VALUE_OR_CLOSEQUOTE : 5,
      EXPIRES : 6,
	  NO_VALUE : 7
    };

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(name + "---" + Number(state) + "----" + c);
        //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(name == "HttpOnly" || name == "Secure") {
				
				state = ParseState.NO_VALUE;
				
		  }
          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 if (c === ',' && value.includes(',')) { 
			parsed[name] = value;
			cookies.push(parsed);
            parsed = {};
            state = ParseState.NAME;
			name = '';
			value = '';
		  
		  } else {
            value += c;
          }
          break;
		  
		case ParseState.NO_VALUE:			
			parsed[name] = value;
			cookies.push(parsed);
            parsed = {};
            state = ParseState.NAME;
			name = '';
			value = '';
          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('; ');
      });
    };



Hope this is covered all the combination. Thank you very much.

@DinoChiesa
Copy link
Author

Thanks, helpful.

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