Last active
September 14, 2018 18:50
-
-
Save pgriess/4557c56e319d841a3b8ccea4dcd8d6b4 to your computer and use it in GitHub Desktop.
Content negotiation with AWS Lambda@Edge
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
/* | |
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); | |
}; |
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
/* | |
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