Created
January 5, 2023 10:43
-
-
Save huyphan/5cb684e901f93fb22b2ff5ddadc7f841 to your computer and use it in GitHub Desktop.
Helper library to allow Grafana K6 to sign SigV4 requests to API Gateway
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
/** Modified version of https://github.com/s0rc3r3r01/awsv4sign/blob/master/awsv4sign.js to make it work with API Gateway */ | |
/* eslint-__ENV node */ | |
/* eslint no-use-before-define: [0, "nofunc"] */ | |
"use strict"; | |
var crypto = require("k6/crypto"); | |
//v4Sign takes a standard http request object and returns the request signed with AWS V4 Signature | |
function v4Sign(req, awsCredentials, options) { | |
options = options || {}; | |
options.timestamp = options.timestamp || Date.now(); | |
options.region = options.region || awsCredentials.region || "us-west-2"; | |
options.key = awsCredentials.accessKeyId; | |
options.secret = awsCredentials.secretAccessKey; | |
options.sessionToken = awsCredentials.sessionToken; | |
options.doubleEscape = false; | |
//Appending required X-Amz-Date header | |
req.params = req.params || {}; | |
req.params.headers = req.params.headers || {}; | |
req.params.headers["host"] = req.hostname; | |
req.params.headers["x-amz-date"] = toTime(options.timestamp); | |
req.params.headers["x-amz-security-token"] = options.sessionToken; | |
req.params.headers['x-amz-content-sha256'] = hash(req.payload || '', 'hex') | |
var signedHeaders = createSignedHeaders(req.params.headers); | |
var canonicalRequest = createCanonicalRequest( | |
req.method, | |
req.path, | |
req.query, | |
req.params.headers, | |
signedHeaders, | |
req.payload, | |
options.doubleEscape | |
); | |
var scope = createCredentialScope( | |
options.timestamp, | |
options.region, | |
req.service | |
); | |
var stringToSign = createStringToSign( | |
options.timestamp, | |
scope, | |
canonicalRequest | |
); | |
var signature = createSignature( | |
options.secret, | |
options.timestamp, | |
options.region, | |
req.service, | |
stringToSign | |
); | |
req.params.headers["Authorization"] = createAuthorizationHeader( | |
options.key, | |
scope, | |
signedHeaders, | |
signature | |
); | |
return req; | |
} | |
exports.v4Sign = v4Sign; | |
function createCanonicalRequest( | |
method, | |
pathname, | |
query, | |
headers, | |
signedHeaders, | |
payload, | |
doubleEscape | |
) { | |
return [ | |
method.toUpperCase(), | |
createCanonicalURI( | |
doubleEscape | |
? pathname | |
.split(/\//g) | |
.map((v) => encodeURIComponent(v)) | |
.join("/") | |
: pathname | |
), | |
createCanonicalQueryString(query), | |
createCanonicalHeaders(headers), | |
signedHeaders, | |
createCanonicalPayload(payload), | |
].join("\n"); | |
} | |
function createCanonicalURI(uri) { | |
var url = uri; | |
if (uri[uri.length - 1] == "/" && url[url.length - 1] != "/") { | |
url += "/"; | |
} | |
return url; | |
} | |
function queryParse(qs) { | |
if (typeof qs !== "string" || qs.length === 0) { | |
return {}; | |
} | |
var result = {}; | |
var split = qs.split("&"); | |
for (let i = 0; i < split.length; i++) { | |
let parts = split[i].split("="); | |
if (parts.length === 2) { | |
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); | |
} else { | |
result[decodeURIComponent(split[i])] = ""; | |
} | |
} | |
return result; | |
} | |
function createCanonicalPayload(payload) { | |
if (payload == "UNSIGNED-PAYLOAD") { | |
return payload; | |
} | |
return hash(payload || "", "hex"); | |
} | |
function createCanonicalQueryString(params) { | |
if (!params) { | |
return ""; | |
} | |
if (typeof params == "string") { | |
params = queryParse(params); | |
} | |
return Object.keys(params) | |
.sort() | |
.map(function (key) { | |
var values = Array.isArray(params[key]) ? params[key] : [params[key]]; | |
return values | |
.sort() | |
.map(function (val) { | |
return encodeURIComponent(key) + "=" + encodeURIComponent(val); | |
}) | |
.join("&"); | |
}) | |
.join("&"); | |
} | |
createCanonicalQueryString = createCanonicalQueryString; | |
function createCanonicalHeaders(headers) { | |
return Object.keys(headers) | |
.sort() | |
.map(function (name) { | |
var values = Array.isArray(headers[name]) | |
? headers[name] | |
: [headers[name]]; | |
return ( | |
name.toLowerCase().trim() + | |
":" + | |
values | |
.map(function (v) { | |
return v.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); | |
}) | |
.join(",") + | |
"\n" | |
); | |
}) | |
.join(""); | |
} | |
function createSignedHeaders(headers) { | |
return Object.keys(headers) | |
.sort() | |
.map(function (name) { | |
return name.toLowerCase().trim(); | |
}) | |
.join(";"); | |
} | |
function createCredentialScope(time, region, service) { | |
return [toDate(time), region, service, "aws4_request"].join("/"); | |
} | |
exports.createCredentialScope = createCredentialScope; | |
function createStringToSign(time, scope, request) { | |
return ["AWS4-HMAC-SHA256", toTime(time), scope, hash(request, "hex")].join( | |
"\n" | |
); | |
} | |
function createAuthorizationHeader(key, scope, signedHeaders, signature) { | |
return [ | |
"AWS4-HMAC-SHA256 Credential=" + key + "/" + scope, | |
"SignedHeaders=" + signedHeaders, | |
"Signature=" + signature, | |
].join(", "); | |
} | |
function createSignature(secret, time, region, service, stringToSign) { | |
var h1 = hmac("AWS4" + secret, toDate(time), "binary"); // date-key | |
var h2 = hmac(h1, region, "binary"); // region-key | |
var h3 = hmac(h2, service, "binary"); // service-key | |
var h4 = hmac(h3, "aws4_request", "binary"); // signing-key | |
return hmac(h4, stringToSign, "hex"); | |
} | |
function toTime(time) { | |
return new Date(time).toISOString().replace(/[:\-]|\.\d{3}/g, ""); | |
} | |
function toDate(time) { | |
return toTime(time).substring(0, 8); | |
} | |
function hmac(key, data, encoding) { | |
return crypto.hmac("sha256", key, data, encoding); | |
} | |
function hash(string, encoding) { | |
return crypto.sha256(string, encoding); | |
} |
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
import { check } from 'k6'; | |
import http from 'k6/http'; | |
import awsv4sign from "./awsv4sign.js"; | |
var crypto = require("k6/crypto"); | |
const CREDENTIALS = { | |
region: '<your-region>', | |
accessKeyId: __ENV.AWS_ACCESS_KEY_ID, | |
secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, | |
sessionToken: __ENV.AWS_SESSION_TOKEN, | |
}; | |
export default function() { | |
const request = { | |
method: 'POST', | |
hostname: '<your-api-gateway>.execute-api.<your-region>.amazonaws.com', | |
path: '/prod/path/to/your/resource', | |
service: 'execute-api', | |
payload: JSON.stringify({ | |
"key": "value" | |
}), | |
} | |
const wrappedReq = awsv4sign.v4Sign(request, CREDENTIALS); | |
const res = http.post( | |
'https://<your-api-gateway>.execute-api.<your-region>.amazonaws.com/prod/path/to/your/resource', | |
wrappedReq.payload, | |
wrappedReq.params | |
); | |
check(res, { | |
'is status 200': (r) => r.status === 200, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment