Created
February 7, 2024 19:21
-
-
Save perpil/9eef495eddc686db377f77b7836efc2a to your computer and use it in GitHub Desktop.
viewer request code for sigv4 signing in a cloudfront function to furl
This file contains 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
const crypto = require('crypto'); | |
const querystring = require('querystring'); | |
function hmac(key, string, encoding) { | |
return crypto | |
.createHmac('sha256', key) | |
.update(string, 'utf8') | |
.digest(encoding); | |
} | |
function hash(string, encoding) { | |
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding); | |
} | |
// This function assumes the string has already been percent encoded | |
function encodeRfc3986(urlEncodedString) { | |
return urlEncodedString.replace(/[!'()*]/g, function (c) { | |
return '%' + c.charCodeAt(0).toString(16).toUpperCase(); | |
}); | |
} | |
function encodeRfc3986Full(str) { | |
return encodeRfc3986(encodeURIComponent(str)); | |
} | |
var HEADERS_TO_IGNORE = [ | |
/*'authorization', | |
'connection', | |
'x-amzn-trace-id', | |
'user-agent', | |
'expect', | |
'presigned-expires', | |
'range',*/ | |
]; | |
// request: { path | body, [host], [method], [headers] } | |
// credentials: { accessKeyId, secretAccessKey, [sessionToken] } | |
let tRequest,tCredentials,tService,tRegion,tExtraHeadersToIgnore,tExtraHeadersToInclude,tParsedPath,tDatetime; | |
function RequestSigner(request, credentials) { | |
var headers = (request.headers = request.headers || {}); | |
tRequest = request; | |
tCredentials = credentials; | |
tService = 'lambda'; | |
tRegion = request.host.match(/^[^\.]+\.lambda-url\.(.*?)\.on\.aws$/)[1]; | |
if (!request.method && request.body) request.method = 'POST'; | |
if (!headers.Host && !headers.host) { | |
headers.Host = request.host; | |
} | |
tExtraHeadersToIgnore = | |
request.extraHeadersToIgnore || {}; | |
tExtraHeadersToInclude = | |
request.extraHeadersToInclude || {}; | |
} | |
function prepareRequest() { | |
parsePath(); | |
var request = tRequest, | |
headers = request.headers, | |
query; | |
if (!request.doNotModifyHeaders) { | |
if (request.body && !headers['Content-Type'] && !headers['content-type']) | |
headers['Content-Type'] = | |
'application/x-www-form-urlencoded; charset=utf-8'; | |
if ( | |
request.body && | |
!headers['Content-Length'] && | |
!headers['content-length'] | |
) | |
headers['Content-Length'] = Buffer.byteLength(request.body); | |
if ( | |
tCredentials.sessionToken && | |
!headers['X-Amz-Security-Token'] && | |
!headers['x-amz-security-token'] | |
) | |
headers['X-Amz-Security-Token'] = tCredentials.sessionToken; | |
headers['X-Amz-Date'] = getDateTime(); | |
} | |
//delete headers.Authorization; | |
//delete headers.authorization; | |
} | |
function sign() { | |
if (tParsedPath) prepareRequest(); | |
tRequest.headers.Authorization = authHeader(); | |
tRequest.path = formatPath(); | |
return tRequest; | |
} | |
function getDateTime() { | |
if (!tDatetime) { | |
var headers = tRequest.headers, | |
date = new Date(headers.Date || headers.date || new Date()); | |
tDatetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, ''); | |
} | |
return tDatetime; | |
} | |
function getDate() { | |
return getDateTime().substr(0, 8); | |
} | |
function authHeader() { | |
return `AWS4-HMAC-SHA256 Credential=${ | |
tCredentials.accessKeyId | |
}/${credentialString()}, SignedHeaders=${signedHeaders()}, Signature=${signature()}`; | |
} | |
function signature() { | |
var date = getDate(); | |
var kCredentials = hmac( | |
hmac( | |
hmac(hmac('AWS4' + tCredentials.secretAccessKey, date), tRegion), | |
tService | |
), | |
'aws4_request' | |
); | |
return hmac(kCredentials, stringToSign(), 'hex'); | |
} | |
function stringToSign() { | |
return [ | |
'AWS4-HMAC-SHA256', | |
getDateTime(), | |
credentialString(), | |
hash(canonicalString(), 'hex'), | |
].join('\n'); | |
} | |
function canonicalString() { | |
if (!tParsedPath) prepareRequest(); | |
var pathStr = tParsedPath.path, | |
query = tParsedPath.query, | |
headers = tRequest.headers, | |
queryStr = '', | |
decodePath = tRequest.doNotEncodePath, | |
bodyHash; | |
bodyHash = | |
headers['X-Amz-Content-Sha256'] || | |
headers['x-amz-content-sha256'] || | |
hash(tRequest.body || '', 'hex'); | |
if (query) { | |
var reducedQuery = Object.keys(query).reduce(function (obj, key) { | |
if (!key) return obj; | |
obj[encodeRfc3986Full(key)] = !Array.isArray(query[key]) | |
? query[key] | |
: query[key]; | |
return obj; | |
}, {}); | |
var encodedQueryPieces = []; | |
Object.keys(reducedQuery) | |
.sort() | |
.forEach(function (key) { | |
if (!Array.isArray(reducedQuery[key])) { | |
encodedQueryPieces.push( | |
key + '=' + encodeRfc3986Full(reducedQuery[key]) | |
); | |
} else { | |
reducedQuery[key] | |
.map(encodeRfc3986Full) | |
.sort() | |
.forEach(function (val) { | |
encodedQueryPieces.push(key + '=' + val); | |
}); | |
} | |
}); | |
queryStr = encodedQueryPieces.join('&'); | |
} | |
if (pathStr !== '/') { | |
pathStr = pathStr | |
.split('/') | |
.reduce(function (path, piece) { | |
if (decodePath) piece = decodeURIComponent(piece.replace(/\+/g, ' ')); | |
path.push(encodeRfc3986Full(piece)); | |
return path; | |
}, []) | |
.join('/'); | |
if (pathStr[0] !== '/') pathStr = '/' + pathStr; | |
} | |
return [ | |
tRequest.method || 'GET', | |
pathStr, | |
queryStr, | |
canonicalHeaders() + '\n', | |
signedHeaders(), | |
bodyHash, | |
].join('\n'); | |
} | |
function canonicalHeaders() { | |
var headers = tRequest.headers; | |
function trimAll(header) { | |
return header.toString().trim().replace(/\s+/g, ' '); | |
} | |
return Object.keys(headers) | |
.filter(function (key) { | |
return !HEADERS_TO_IGNORE.includes(key.toLowerCase()); | |
}) | |
.sort(function (a, b) { | |
return a.toLowerCase() < b.toLowerCase() ? -1 : 1; | |
}) | |
.map(function (key) { | |
return key.toLowerCase() + ':' + trimAll(headers[key]); | |
}) | |
.join('\n'); | |
} | |
function signedHeaders() { | |
var extraHeadersToInclude = tExtraHeadersToInclude, | |
extraHeadersToIgnore = tExtraHeadersToIgnore; | |
return Object.keys(tRequest.headers) | |
.map(function (key) { | |
return key.toLowerCase(); | |
}) | |
.filter(function (key) { | |
return ( | |
extraHeadersToInclude[key] || | |
(!HEADERS_TO_IGNORE.includes(key) && !extraHeadersToIgnore[key]) | |
); | |
}) | |
.sort() | |
.join(';'); | |
} | |
function credentialString() { | |
return [getDate(), tRegion, tService, 'aws4_request'].join('/'); | |
} | |
function parsePath() { | |
var path = tRequest.path || '/'; | |
if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) { | |
path = encodeURI(decodeURI(path)); | |
} | |
var queryIx = path.indexOf('?'), | |
query = null; | |
if (queryIx >= 0) { | |
query = querystring.parse(path.slice(queryIx + 1)); | |
path = path.slice(0, queryIx); | |
} | |
tParsedPath = { | |
path: path, | |
query: query, | |
}; | |
} | |
function formatPath() { | |
var path = tParsedPath.path, | |
query = tParsedPath.query; | |
if (!query) return path; | |
// Services don't support empty query string keys | |
if (query[''] != null) delete query['']; | |
return path + '?' + encodeRfc3986(querystring.stringify(query)); | |
} | |
function handler(event) { | |
const request = event.request; | |
const qs = request.querystring; | |
let qo = {}; | |
Object.keys(qs).forEach(k => qo[k] = qs[k].value); | |
let q = querystring.stringify(qo) | |
let requestOptions = { | |
host: request.headers.host.value, | |
path: request.uri + q?`?${q}`:'' | |
}; | |
RequestSigner(requestOptions,{secretAccessKey: 'FIXME', accessKeyId: 'FIXME'}); | |
sign(); | |
request.headers['authorization'] = {value:requestOptions.headers.Authorization}; | |
request.headers['x-amz-date'] = {value:requestOptions.headers['X-Amz-Date']}; | |
return request; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment