Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aws #11

Closed
wants to merge 11 commits into from
Closed

Add aws #11

wants to merge 11 commits into from

Conversation

mstoykov
Copy link
Contributor

This is very much WIP that is based on:
https://gist.github.com/MStoykov/38cc1293daa9080b11e26053589a6865

Unfortunately, S3 seems to be the one AWS service that is okay with query signing, the rest needs headers signing. Which is what is now ... suppose to be supported ?!?

I tried using localstack to test it, with mixed results - it definitely does check for some of the headers and stuff(probably not in the same way AWS does though), but it definitely doesn't check that I've signed the requests correctly .. as changing credentials still works.

The other ideas were:
https://aws.amazon.com/blogs/developer/mocking-out-then-aws-sdk-for-go-for-unit-testing/
https://www.npmjs.com/package/aws-sdk-mock

But both will probably need an even harder set up to be usable. So I am looking for ideas

Priorities:

  1. find a way to test different AWS services compatibility without needing to hit AWS with real credentials - there seems to be no developer API for tests or something like that. Bonus points if it can be run automatically in CI
  2. Fix the API and any bugs
  3. profit

@mstoykov mstoykov added the help wanted Extra attention is needed label Nov 19, 2020
@na--
Copy link
Member

na-- commented Jan 25, 2021

Given that we have versions in jslib, maybe we should merge this under the "better than nothing" v.0.0.1, with only the most necessary changes made? I haven't looked at the actual code, but similar to #20, we can always polish the API and implementation later.

@mstoykov
Copy link
Contributor Author

I was really hoping that someone will take interest and push it through with some more testing ...

@alanbrent
Copy link

Sorry this issue dropped off my radar a few months ago. I am definitely interested in this, and will try to take a look at it tomorrow.

@alanbrent
Copy link

Is there a doc somewhere that describes how to use this? I'm more of an SRE-type, and don't spend much time in JS-land. I'm sure I could just vendor the JS files in my branch for testing, but I'll want to know how to do it "right" :)

Copy link

@alanbrent alanbrent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still trying to get to the point where I can test, but I'm close. This is feedback from things I've encountered along the way.

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";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS_DEFAULT_REGION is also something I have seen in my day. Perhaps

options.region = options.region || __ENV.AWS_REGION || __ENV.AWS_DEFAULT_REGION

I also removed the fallback to us-east-, because it seems to me that it's better (i.e. less confusing) for the code flow to break here rather than having to reason about why a request that "should have" worked didn't.

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";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same feedback here as in core.js

return options;
}

function signWithHeaders(method, service, region, target, path="", body=null, query="", headers= {}) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. region="", so it can be filled by fillOptions when not supplied
  2. target="", because it's not always relevant to set X-Amz-Target header

var options = {headers: headers }
options = fillOptions(options);

options.headers["X-Amz-Target"] = target;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap in if (target)

options = fillOptions(options);

options.headers["X-Amz-Target"] = target;
options.headers["Content-Type"] = "application/x-amz-json-1.1";
Copy link

@alanbrent alanbrent Mar 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is always necessary. We send text/csv content type to many of our Sagemaker endpoints.

Perhaps content type should be left up to the K6 script?

@mstoykov
Copy link
Contributor Author

Hi @alanbrent , Sorry for the slow response and thanks for the help! 🎉

Unfortunately, I don't have all that much time to work on this currently (as always, this is why it has been postponed forever) case in point this is the third day I meant to look at this, and once again I am unlikely to have the time to do it.

So if you can directly make a PR against my branch (or even directly against master on top of my changes) I think that will speed things up a bit, as I can just review the changes instead of also having to make them ;). And you can use the code you write to both test and then commit to the repo.

Encapsulate AWS request signing feedback
@alanbrent
Copy link

alanbrent commented Apr 1, 2021

FYI I actually have NOT been able to successfully test this. I thought I had but then I realized all the requests were failing. I will open another PR when I get a chance to iron it out.

I am currently this far:

time="2021-04-01T00:52:05Z" level=info msg="{\"message\":\"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\\n\\nThe Canonical String for this request should have been\\n'POST\\n/endpoints/dev-winprob-v21-multi-endpoint-test-fan-unweighted-0328-mme-new/invocations\\n\\naccept:application/json\\ncontent-type:text/csv\\nhost:runtime.sagemaker.us-east-1.amazonaws.com\\nx-amz-date:20210401T005204Z\\nx-amzn-sagemaker-target-model:ironsource.tar.gz\\n\\naccept;content-type;host;x-amz-date;x-amzn-sagemaker-target-model\\n6116a9470b634ca3a99a96538caaf7af957dad351aea6ec6cdf975084ee998b8'\\n\\nThe String-to-Sign should have been\\n'AWS4-HMAC-SHA256\\n20210401T005204Z\\n20210401/us-east-1/sagemaker/aws4_request\\n1579351207c345bab267619220ea7b6a174a04321898cbb4538127275eb30778'\\n\"}\n" source=console

@tom-miseur
Copy link
Contributor

@alanbrent @mstoykov 👋

I've been having a play with AWS Signature Version 4 and was able to get it working with a (real) Lambda endpoint using a browserified version of the aws4 library.

I also saw the "The request signature we calculated does not match the signature you provided" error but for a different reason; in the end it was just a case of ensuring the correct object was passed to aws4.sign and ensuring the same parameters were being set against the request being sent through k6's http.post (in my case it was a POST request).

I'm now trying to repeat the process using this awsv4 library instead, but am encountering some issues:

  • region passed in to signWithHeaders is never set in options.region, so I get a undefined appearing in the resulting object returned by the function:
{"url":"https://lambda.undefined.amazonaws.com//2015-03-31/functions/hello-world/invocations","headers":{"Authorization":"AWS4-HMAC-SHA256 Credential=undefined/20210630/us-west-2/lambda/aws4_request SignedHeaders= Signature=41759f738ed4e2e6c17f300e1d6264391fcd36389c6fde3638f2a1a32039bc14"}}

That is resolved by simply adding it to the options object in signWithHeaders:

var options = { 
  headers: headers,
  region: region
  }

The second undefined was as a result of not passing in the AWS_ACCESS_KEY_ID via environment variable (we should probably output a warning when it and the associated AWS_SECRET_ACCESS_KEY haven't been specified).

  • SignedHeaders: this is empty by default, and from what I gather, at least the Host header needs to be in there, but it seems the list of headers needed can vary depending on the service (yuck). The browserified aws4 library generated SignedHeaders=content-length;content-type;host;x-amz-date behind-the-scenes; I did not have to explicitly set these anywhere (they were provided automatically by the object returned from aws4.sign()).

Ideally, we'd also generate the necessary (minimum) headers automatically.

Below is the eventual working example. Note I had to convert the Content-Length value to a string to avoid a TypeError: Object has no member 'replace' in core.js createCanonicalHeaders (line 105).

import http from "k6/http";
import { sleep } from "k6";
import { signWithHeaders } from "./header.js";

export const options = {};

export default function main() {
  const method = 'POST';
  const service = 'lambda';
  const region = 'us-west-2';
  
  const path = '/2015-03-31/functions/hello-world/invocations';
  const body = '{"key1":"value1","key2":"value2","key3":"value3"}';

  const headers = {
    'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
    'Content-Length': body.length.toString(),
    'Host': `${service}.${region}.amazonaws.com`,
    'X-Amz-Date': new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '')
  }

  console.log(JSON.stringify(headers));

  const signed = signWithHeaders(
    method, 
    service, 
    region, 
    "", 
    path, 
    body, 
    "", 
    headers
  );

  console.log(JSON.stringify(signed));

  let response;

  response = http.post(
    `https://${service}.${region}.amazonaws.com${path}`,
    body,
    {
      headers: headers
    }
  );

  console.log(response.body);
  console.log(JSON.stringify(response.request));
}

What slightly puzzles me here is that I'm not actually using the returned signed object anywhere when making the HTTP call. If I remove the signWithHeaders completely, I get a "Missing Authentication Token" (the Authorization header is indeed missing). Le confused!

@mstoykov
Copy link
Contributor Author

mstoykov commented Jul 1, 2021

I think https://github.com/k6io/jslib.k6.io/pull/29 solves most of those issues? Can you try to use it and if it works I am just going to merge it.

What slightly puzzles me here is that I'm not actually using the returned signed object anywhere when making the HTTP call. If I remove the signWithHeaders completely, I get a "Missing Authentication Token" (the Authorization header is indeed missing). Le confused!

signWithHeaders does set the header on the heders object/map you provide.
It additionally returns an object with url and headers keys that should be used.

I guess from UX perspective it's probably better to not change the original headers 🤔

@romanlv
Copy link

romanlv commented Feb 10, 2022

tried to use current version with execute-api on AWS and it didn't work, always returns

  "status": 403,
  "status_text": "403 Forbidden",

browserify'ed aws4 works fine. I with someone creates aws4 k6 optimized version (with exactly the same api)

@mstoykov
Copy link
Contributor Author

mstoykov commented Apr 8, 2022

closed in favor of #46

@mstoykov mstoykov closed this Apr 8, 2022
@mstoykov mstoykov deleted the addAWS branch April 8, 2022 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants