Skip to content

Instantly share code, notes, and snippets.

@mstoykov
Last active February 26, 2022 06:33

Revisions

  1. mstoykov revised this gist Oct 15, 2019. 1 changed file with 193 additions and 0 deletions.
    193 changes: 193 additions & 0 deletions test.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,193 @@
    import html from "k6/html";
    import crypto from "k6/crypto";
    import http from "k6/http";
    import { check } from "k6";

    import v4 from "./awsv4.js";


    var defaultBucket = "testing-awsv4";
    var defaultRegion = "eu-central-1"

    export function setup() {
    createS3Bucket(
    {
    bucket: defaultBucket,
    region: defaultRegion,
    });
    }

    export function teardown(data) {
    deleteS3Bucket(
    {
    bucket: defaultBucket,
    region: defaultRegion,
    });
    }


    function createS3Bucket(params) {
    // you always create the bucket in eu-west-1
    var createUrl = v4.createPresignedS3URL("",
    Object.assign({} , params, {method: "PUT"}));

    var region = params.region || __ENV.AWS_REGION || "us-east-1";
    var createBody = `
    <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <LocationConstraint>`+region+`</LocationConstraint>
    </CreateBucketConfiguration > `;

    console.log("s3 bucket create url ", createUrl);
    console.log("s3 bucket create body ", createBody);
    var res = http.put(createUrl, createBody);
    check(res, {
    "create s3 bucket returned 200": (r) => r.status == 200,
    });
    console.log(res.body);
    }

    function deleteS3Objects(objects, params) {

    var deleteBody = `<?xml version="1.0" encoding="UTF-8"?>
    <Delete>
    <Quiet>true</Quiet>\n`;
    for (let i = 0; i < objects.length; i++) {
    deleteBody += `<Object><Key>` + objects[i] + `</Key></Object>\n`
    }

    deleteBody += "</Delete>\n"
    var bodyMD5 = crypto.md5(deleteBody, "base64");
    var headers = {"Content-MD5": bodyMD5};
    var deleteUrl = v4.createPresignedS3URL("",
    Object.assign({} , params, {method: "POST", query: "delete", headers: headers}));
    console.log("multi object delete url ", deleteUrl);
    var res = http.post(deleteUrl, deleteBody, {"headers": headers});
    console.log("multi object delete body ", deleteBody);
    check(res,
    {"multi delete did not return 200": (r) => r.status == 200});
    }

    function listS3Objects(params) {
    var listURL = v4.createPresignedS3URL("",
    Object.assign({} , params, {method: "GET", query: "list-type=2"}));
    console.log("s3 bucket list url ", listURL);
    var res = http.get(listURL);
    check(res, {
    "list s3 bucket returned 200": (r) => r.status == 200,
    });
    var doc = html.parseHTML(res.body);
    var result = [];
    doc.find("Contents Key").each(function(idx, el) {
    result.push(el.innerHTML());
    })
    console.log(JSON.stringify(result));
    return result;
    }

    function deleteS3Bucket(params) {
    while(true) {
    var objects = listS3Objects(params);
    if (objects.length == 0) {
    break
    }
    deleteS3Objects(objects, params)
    break;
    }

    var deleteUrl = v4.createPresignedS3URL("",
    Object.assign({} , params, {method: "DELETE"}));


    console.log("s3 bucket delete url ", deleteUrl);
    var res = http.del(deleteUrl);
    check(res, {
    "delete s3 bucket returned 204": (r) => r.status == 204,
    });
    }

    function uploadFile(path, content, params) {
    var url = v4.createPresignedS3URL(
    path,
    Object.assign({}, params,
    {
    bucket: defaultBucket,
    region: defaultRegion,
    method: "PUT",
    }));

    console.log("uploadFile: "+ url);
    return http.put(url, content, {redirects: 20});
    }

    function changeFileACL(path, body, params) {
    var url = v4.createPresignedS3URL(
    path,
    Object.assign({}, params,
    {
    bucket: defaultBucket,
    region: defaultRegion,
    method: "PUT",
    query: "acl",
    }));

    console.log("changeFileACL: "+ url);
    var headers = params["headers"] || {};
    return http.put(url, body, {headers: headers});
    }

    function getFile(path, params) {
    var url = v4.createPresignedS3URL(
    path,
    Object.assign({}, params,
    {
    bucket: defaultBucket,
    region: defaultRegion,
    method: "GET",
    }));
    console.log("getFile: "+ url);

    return http.get(url);
    }

    function publicGetFile(path, bucket) {
    var url = "https://" + bucket + ".s3.amazonaws.com/" + path;

    console.log("publicGetFile: "+ url);
    return http.get(url);
    }

    export default function(data) {
    console.log(JSON.stringify(v4));
    var random = Math.floor(Math.random() * 100);
    var expectedBody = "a".repeat(random);
    console.log(expectedBody);
    var name = "randomPath" + random + "__" + __VU + "__"+ __ITER;
    var res = uploadFile(name, expectedBody);
    check(res, {
    "status code of upload is 200": (r) => r.status == 200,
    });
    console.log("upload response "+ res.body);

    res = getFile(name);
    check(res, {
    "status code get is 200": (r) => r.status == 200,
    "body is correct": (r) => r.body == expectedBody,
    });

    res = publicGetFile(name, defaultBucket);
    check(res, {
    "status code public get is 403 by default": (r) => r.status == 403,
    "body of public get is wrong by default": (r) => r.body != expectedBody,
    });

    res = changeFileACL(name, null, {headers: {"x-amz-acl": "public-read"}});
    check(res, {
    "status code acl change is 200": (r) => r.status == 200,
    });

    res = publicGetFile(name, defaultBucket);
    check(res, {
    "status code public get is 200 when public": (r) => r.status == 200,
    "body of public get is correct public": (r) => r.body == expectedBody,
    });
    }
  2. mstoykov created this gist Oct 15, 2019.
    279 changes: 279 additions & 0 deletions aws_k6.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,279 @@
    /* eslint-__ENV node */
    /* eslint no-use-before-define: [0, "nofunc"] */
    "use strict";

    // sources of inspiration:
    // https://web-identity-federation-playground.s3.amazonaws.com/js/sigv4.js
    // http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
    var crypto = require("k6/crypto");

    function createCanonicalRequest(
    method,
    pathname,
    query,
    headers,
    payload,
    doubleEscape
    ) {
    return [
    method.toUpperCase(),
    createCanonicalURI(
    doubleEscape
    ? pathname
    .split(/\//g)
    .map(v => encodeURIComponent(v))
    .join("/")
    : pathname
    ),
    createCanonicalQueryString(query),
    createCanonicalHeaders(headers),
    createSignedHeaders(headers),
    createCanonicalPayload(payload)
    ].join("\n");
    };
    exports.createCanonicalRequest = createCanonicalRequest;
    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("");
    };
    exports.createCanonicalHeaders = createCanonicalHeaders;

    function createSignedHeaders(headers) {
    return Object.keys(headers)
    .sort()
    .map(function(name) {
    return name.toLowerCase().trim();
    })
    .join(";");
    };
    exports.createSignedHeaders = createSignedHeaders;

    function createCredentialScope(time, region, service) {
    return [toDate(time), region, service, "aws4_request"].join("/");
    };

    exports.createCredentialScope = createCredentialScope;

    function createStringToSign(time, region, service, request) {
    return [
    "AWS4-HMAC-SHA256",
    toTime(time),
    createCredentialScope(time, region, service),
    hash(request, "hex")
    ].join("\n");
    };
    exports.createStringToSign = createStringToSign;

    function createAuthorizationHeader(
    key,
    scope,
    signedHeaders,
    signature
    ) {
    return [
    "AWS4-HMAC-SHA256 Credential=" + key + "/" + scope,
    "SignedHeaders=" + signedHeaders,
    "Signature=" + signature
    ].join(", ");
    };
    exports.createAuthorizationHeader = createAuthorizationHeader;
    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");
    };
    exports.createSignature = createSignature;


    function createPresignedS3URL(name, options) {
    options = options || {};
    options.method = options.method || "GET";
    options.bucket = options.bucket || __ENV.AWS_S3_BUCKET;
    options.signSessionToken = true;
    options.doubleEscape = false;
    return createPresignedURL(
    options.method,
    options.bucket + ".s3.amazonaws.com",
    "/" + name,
    "s3",
    "UNSIGNED-PAYLOAD",
    options
    );
    };

    exports.createPresignedS3URL = createPresignedS3URL;

    function createPresignedURL(
    method,
    host,
    path,
    service,
    payload,
    options
    ) {
    options = options || {};
    options.key = options.key || __ENV.AWS_ACCESS_KEY_ID;
    options.secret = options.secret || __ENV.AWS_SECRET_ACCESS_KEY;
    options.sessionToken = options.sessionToken || __ENV.AWS_SESSION_TOKEN;
    options.protocol = options.protocol || "https";
    options.timestamp = options.timestamp || Date.now();
    options.region = options.region || __ENV.AWS_REGION || "us-east-1";
    options.expires = options.expires || 86400; // 24 hours
    options.headers = options.headers || {};
    options.signSessionToken = options.signSessionToken || false;
    options.doubleEscape =
    options.doubleEscape !== undefined ? options.doubleEscape : true;

    // host is required
    options.headers.Host = host;

    var query = options.query ? queryParse(options.query) : {};
    query["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256";
    query["X-Amz-Credential"] =
    options.key +
    "/" +
    createCredentialScope(options.timestamp, options.region, service);
    query["X-Amz-Date"] = toTime(options.timestamp);
    query["X-Amz-Expires"] = options.expires;
    query["X-Amz-SignedHeaders"] = createSignedHeaders(options.headers);

    // when a session token must be "signed" into the canonical request
    // (needed for some services, such as s3)
    if (options.sessionToken && options.signSessionToken) {
    query["X-Amz-Security-Token"] = options.sessionToken;
    }

    var canonicalRequest = createCanonicalRequest(
    method,
    path,
    query,
    options.headers,
    payload,
    options.doubleEscape
    );
    var stringToSign = createStringToSign(
    options.timestamp,
    options.region,
    service,
    canonicalRequest
    );
    var signature = createSignature(
    options.secret,
    options.timestamp,
    options.region,
    service,
    stringToSign
    );
    query["X-Amz-Signature"] = signature;

    // when a session token must NOT be "signed" into the canonical request
    // (needed for some services, such as IoT)
    if (options.sessionToken && !options.signSessionToken) {
    query["X-Amz-Security-Token"] = options.sessionToken;
    } else {
    delete query["X-Amz-Security-Token"];
    }

    return (
    options.protocol + "://" + host + path + "?" + createCanonicalQueryString(query)
    );
    };

    exports.createPresignedURL = createPresignedURL;

    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);
    }