Skip to content

Instantly share code, notes, and snippets.

@perpil
Created February 7, 2024 19:21
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 perpil/9eef495eddc686db377f77b7836efc2a to your computer and use it in GitHub Desktop.
Save perpil/9eef495eddc686db377f77b7836efc2a to your computer and use it in GitHub Desktop.
viewer request code for sigv4 signing in a cloudfront function to furl
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