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

FTDCS-151 - Use aws-sdk official package to sign requests to AWS #93

Merged
merged 4 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 45 additions & 38 deletions lib/helpers/signed-aws-es-fetch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const aws4 = require('aws4');
const nodeFetch = require('node-fetch');
const { HttpRequest } = require('@aws-sdk/protocol-http');
const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-browser');
const logger = require('@financial-times/n-logger').default;
const resolveCname = require('util').promisify(require('dns').resolveCname);
const memoize = require('lodash/memoize');
Expand All @@ -12,9 +14,12 @@ const defaultAwsKeys = {
tokenKeys: ['ES_AWS_SESSION_TOKEN', 'AWS_SESSION_TOKEN']
};

module.exports = async function (url, opts, creds) {
opts = opts || {};
url = await resolveUrlAndHost(url);
// We capture the group before the extension es.amazonaws.com
const RE_AWS_ES_REGION = /\.(\w{2}-\w*?-\d)\.es\.amazonaws\.com/;
const defaultRegion = 'eu-west-1';

module.exports = async function (url, opts = {}, creds = {}) {
creds = setAwsCredentials(creds);
return signedFetch(url, opts, creds);
};

Expand Down Expand Up @@ -50,22 +55,14 @@ async function resolveValidHost(urlObject) {
}

function isActiveDnsResolution() {
return (
!process.env.AWS_SIGNED_FETCH_DISABLE_DNS_RESOLUTION ||
process.env.AWS_SIGNED_FETCH_DISABLE_DNS_RESOLUTION === 'false'
);
return process.env.AWS_SIGNED_FETCH_DISABLE_DNS_RESOLUTION !== 'true';
}

async function resolveUrlAndHost(url) {
const urlObject = getURLObject(url);
if (
!/\.es\.amazonaws\.com$/.test(urlObject.host) &&
async function resolveUrlAndHost(urlObject) {
return !/\.es\.amazonaws\.com$/.test(urlObject.host) &&
isActiveDnsResolution()
) {
const host = await resolveValidHost(urlObject);
url = url.replace(urlObject.host, host);
}
return url;
? resolveValidHost(urlObject)
: urlObject.host;
}

function defaultAwsCredentials(keys) {
Expand All @@ -82,7 +79,6 @@ function thereIsToken() {
}

function setAwsCredentials(creds) {
rowanmanning marked this conversation as resolved.
Show resolved Hide resolved
creds = creds || {};
creds.accessKeyId = creds.accessKeyId || defaultAwsCredentials('awsKeys');
creds.secretAccessKey =
creds.secretAccessKey || defaultAwsCredentials('awsSecretKeys');
Expand All @@ -97,28 +93,39 @@ function setAwsCredentials(creds) {
return creds;
}

function getSignableData(url, opts, creds) {
creds = setAwsCredentials(creds);
const { host, pathname, search, protocol } = getURLObject(url);
const path = `${pathname}${search}`;
const signable = {
method: opts.method,
host,
path,
body: opts.body,
headers: opts.headers
};
aws4.sign(signable, creds);
return { headers: signable.headers, path: signable.path, protocol };
async function getSignableData(url) {
const urlObject = getURLObject(url);
const domain = await resolveUrlAndHost(urlObject);
const matchRegion = domain.match(RE_AWS_ES_REGION);
const region = matchRegion[1] || defaultRegion;
const path = `${urlObject.pathname}${urlObject.search}`;
const protocol = urlObject.protocol;
return { domain, region, path, protocol };
}

function signedFetch(url, opts, creds) {
const { headers, path, protocol } = getSignableData(url, opts, creds);
opts.headers = headers;
async function signedFetch(url, opts, creds) {
creds = creds || {};
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: could this be a default param?

Suggested change
async function signedFetch(url, opts, creds) {
creds = creds || {};
async function signedFetch(url, opts, creds = {}) {

// I need to get domain, region and path
const { domain, region, path, protocol } = await getSignableData(url);
opts.hostname = domain;
opts.headers.host = domain;
opts.headers['Content-Type'] = 'application/json';
opts.path = path;

// Create the HTTP request
const request = new HttpRequest(opts);

// Sign the request
const signer = new SignatureV4({
credentials: async () => creds,
rowanmanning marked this conversation as resolved.
Show resolved Hide resolved
region: region,
service: 'es',
sha256: Sha256
});

const signedRequest = await signer.sign(request);
opts.headers = signedRequest.headers;
// Try to use a global fetch here if possible otherwise risk getting a handle
// on the wrong fetch reference (ie. not a mocked one if in a unit test)
return (global.fetch || nodeFetch)(
`${protocol}//${opts.headers.Host}${path}`,
opts
);
return (global.fetch || nodeFetch)(`${protocol}//${domain}${path}`, opts);
}
Loading