Skip to content

Instantly share code, notes, and snippets.

@pgriess
Last active September 14, 2018 18:50
Show Gist options
  • Save pgriess/4557c56e319d841a3b8ccea4dcd8d6b4 to your computer and use it in GitHub Desktop.
Save pgriess/4557c56e319d841a3b8ccea4dcd8d6b4 to your computer and use it in GitHub Desktop.
Content negotiation with AWS Lambda@Edge
/*
MIT License
Copyright (c) 2018 Peter Griess
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
* Given an array of AWS Lambda header objects for headers that support
* ','-delimited list syntax, return a single array containing the values from
* all of these lists.
*
* Assumptions
*
* - HTTP headers arrive as an array of objects, each with a 'key' and 'value'
* property. We ignore the 'key' property as we assume the caller has supplied
* an array where these do not differ except by case.
*
* - The header objects specified have values which conform to section 7 of RFC
* 7230. For eample, Accept, Accept-Encoding support this. User-Agent does not.
*/
const splitHeaders = function(headers) {
return headers.map(function(ho) { return ho['value']; })
.reduce(
function(acc, val) {
return acc.concat(val.replace(/ +/g, '').split(','));
},
[]);
};
/*
* Parse an HTTP header value with optional attributes, returning a tuple of
* (value name, attributes dictionary).
*
* For example 'foo;a=1;b=2' would return ['foo', {'a': 1, 'b': 2}].
*/
const parseHeaderValue = function(v) {
const s = v.split(';');
if (s.length == 1) {
return [v, {}];
}
const attrs = {};
s.forEach(function(av, idx) {
if (idx === 0) {
return;
}
const kvp = av.split('=', 2)
attrs[kvp[0]] = kvp[1];
});
return [s[0], attrs];
};
/*
* Given an array of (value name, attribute dictionary) tuples, return a sorted
* array of (value name, q-value) tuples, ordered by the value of the 'q' attribute.
*
* If multiple instances of the same value are found, the last instance will
* override attributes of the earlier values. If no 'q' attribute is specified,
* a default value of 1 is assumed.
*
* For example given the below header values, the output of this function will
* be [['b', 3], ['a', 2]].
*
* [['a', {'q': '5'}], ['a', {'q': '2'}], ['b', {'q': '3'}]]
*/
const sortHeadersByQValue = function(headerValues) {
/* Parse q attributes, ensuring that all to 1 */
var headerValuesWithQValues = headerValues.map(function(vt) {
var vn = vt[0];
var va = vt[1];
if ('q' in va) {
return [vn, parseFloat(va['q'])];
} else {
return [vn, 1];
}
});
/* Filter out duplicates by name, preserving the last seen */
var seen = {};
const filteredValues = headerValuesWithQValues.reverse().filter(function(vt) {
const vn = vt[0];
if (vn in seen) {
return false;
}
seen[vn] = true;
return true;
});
/* Sort by values with highest 'q' attribute */
return filteredValues.sort(function(a, b) { return b[1] - a[1]; });
};
/*
* Perform content negotiation.
*
* Given sorted arrays of supported (value name, q-value) tuples, select a
* value that is mutuaully acceptable. Returns null is nothing could be found.
*/
const performNegotiation = function(clientValues, serverValues) {
var scores = [];
for (var i = 0; i < clientValues.length; ++i) {
const cv = clientValues[i];
const sv = serverValues.find(function(sv) { return sv[0] === cv[0]; });
if (sv === undefined) {
continue;
}
scores.push([cv[0], cv[1] * sv[1]]);
}
if (scores.length === 0) {
return null;
}
return scores.sort(function(a, b) { return b[1] - a[1]; })[0][0];
};
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
if ('accept-encoding' in headers &&
!request.uri.startsWith('/gzip/') &&
!request.uri.startsWith('/br/')) {
const SERVER_WEIGHTS = [
['br', 1],
['gzip', 0.9],
['identity', 0.1],
];
const sh = splitHeaders(headers['accept-encoding']);
const ph = sh.map(parseHeaderValue);
const qh = sortHeadersByQValue(ph);
const rep = performNegotiation(qh, SERVER_WEIGHTS);
if (rep && rep !== 'identity') {
request.uri = '/' + rep + request.uri;
}
}
callback(null, request);
};
/*
MIT License
Copyright (c) 2018 Peter Griess
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
headers['Vary'] = [{key: 'Vary', value: 'Accept-Encoding'}];
callback(null, response);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment