Lesson 35 - Get Compute Auth Token Working
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iomanip>
|
||||
#include <aws/core/http/HttpRequest.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <aws/core/http/HttpClient.h>
|
||||
#include <aws/core/utils/HashingUtils.h>
|
||||
#include <aws/core/utils/crypto/MD5.h>
|
||||
#include <aws/core/utils/logging/LogMacros.h>
|
||||
|
||||
|
||||
namespace smithy
|
||||
{
|
||||
namespace client
|
||||
{
|
||||
static const char AWS_SMITHY_CLIENT_UTILS_TAG[] = "AwsSmithyClientUtils";
|
||||
|
||||
class Utils
|
||||
{
|
||||
public:
|
||||
using HttpMethod = Aws::Http::HttpMethod;
|
||||
using HeaderValueCollection = Aws::Http::HeaderValueCollection;
|
||||
using DateTime = Aws::Utils::DateTime;
|
||||
using DateFormat = Aws::Utils::DateFormat;
|
||||
using ClientError = Aws::Client::AWSError<Aws::Client::CoreErrors>;
|
||||
|
||||
static void AppendHeaderValueToRequest(const std::shared_ptr<Aws::Http::HttpRequest>& httpRequest,
|
||||
const Aws::String& header,
|
||||
const Aws::String& value)
|
||||
{
|
||||
assert(httpRequest);
|
||||
assert(!header.empty());
|
||||
|
||||
if (!httpRequest->HasHeader(header.c_str()))
|
||||
{
|
||||
httpRequest->SetHeaderValue(header, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Aws::String contentEncoding = httpRequest->GetHeaderValue(header.c_str());
|
||||
contentEncoding.append(",").append(value);
|
||||
httpRequest->SetHeaderValue(header, contentEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
static void AddContentBodyToRequest(const std::shared_ptr<Aws::Http::HttpRequest>& httpRequest,
|
||||
const std::shared_ptr<Aws::IOStream>& body,
|
||||
const std::shared_ptr<Aws::Http::HttpClient>& httpClient,
|
||||
bool needsContentMd5,
|
||||
bool isChunked)
|
||||
{
|
||||
assert(httpRequest);
|
||||
|
||||
httpRequest->AddContentBody(body);
|
||||
|
||||
//If there is no body, we have a content length of 0
|
||||
//note: we also used to remove content-type, but S3 actually needs content-type on InitiateMultipartUpload and it isn't
|
||||
//forbidden by the spec. If we start getting weird errors related to this, make sure it isn't caused by this removal.
|
||||
if (!body)
|
||||
{
|
||||
AWS_LOGSTREAM_TRACE(AWS_SMITHY_CLIENT_UTILS_TAG, "No content body, content-length headers");
|
||||
|
||||
if (httpRequest->GetMethod() == HttpMethod::HTTP_POST || httpRequest->GetMethod() ==
|
||||
HttpMethod::HTTP_PUT)
|
||||
{
|
||||
httpRequest->SetHeaderValue(Aws::Http::CONTENT_LENGTH_HEADER, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
httpRequest->DeleteHeader(Aws::Http::CONTENT_LENGTH_HEADER);
|
||||
}
|
||||
}
|
||||
|
||||
//Add transfer-encoding:chunked to header
|
||||
if (body && isChunked && !httpRequest->HasHeader(Aws::Http::CONTENT_LENGTH_HEADER))
|
||||
{
|
||||
httpRequest->SetTransferEncoding(Aws::Http::CHUNKED_VALUE);
|
||||
}
|
||||
//in the scenario where we are adding a content body as a stream, the request object likely already
|
||||
//has a content-length header set and we don't want to seek the stream just to find this information.
|
||||
else if (body && !httpRequest->HasHeader(Aws::Http::CONTENT_LENGTH_HEADER))
|
||||
{
|
||||
assert(httpClient);
|
||||
if (!httpClient->SupportsChunkedTransferEncoding())
|
||||
{
|
||||
AWS_LOGSTREAM_WARN(AWS_SMITHY_CLIENT_UTILS_TAG,
|
||||
"This http client doesn't support transfer-encoding:chunked. " <<
|
||||
"The request may fail if it's not a seekable stream.");
|
||||
}
|
||||
AWS_LOGSTREAM_TRACE(AWS_SMITHY_CLIENT_UTILS_TAG,
|
||||
"Found body, but content-length has not been set, attempting to compute content-length");
|
||||
body->seekg(0, body->end);
|
||||
auto streamSize = body->tellg();
|
||||
body->seekg(0, body->beg);
|
||||
Aws::StringStream ss;
|
||||
ss << streamSize;
|
||||
httpRequest->SetContentLength(ss.str());
|
||||
}
|
||||
|
||||
if (needsContentMd5 && body && !httpRequest->HasHeader(Aws::Http::CONTENT_MD5_HEADER))
|
||||
{
|
||||
AWS_LOGSTREAM_TRACE(AWS_SMITHY_CLIENT_UTILS_TAG, "Found body, and content-md5 needs to be set" <<
|
||||
", attempting to compute content-md5");
|
||||
|
||||
//changing the internal state of the hash computation is not a logical state
|
||||
//change as far as constness goes for this class. Due to the platform specificness
|
||||
//of hash computations, we can't control the fact that computing a hash mutates
|
||||
//state on some platforms such as windows (but that isn't a concern of this class.
|
||||
|
||||
// TODO: check refactoring from:
|
||||
// auto md5HashResult = const_cast<AWSClient*>(this)->m_hash->Calculate(*body);
|
||||
Aws::Utils::Crypto::MD5 hash;
|
||||
auto md5HashResult = hash.Calculate(*body);
|
||||
body->clear();
|
||||
if (md5HashResult.IsSuccess())
|
||||
{
|
||||
httpRequest->SetHeaderValue(Aws::Http::CONTENT_MD5_HEADER,
|
||||
Aws::Utils::HashingUtils::Base64Encode(md5HashResult.GetResult()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool DoesResponseGenerateError(const std::shared_ptr<Aws::Http::HttpResponse>& response)
|
||||
{
|
||||
assert(response);
|
||||
if (response->HasClientError())
|
||||
return true;
|
||||
|
||||
static const int SUCCESS_RESPONSE_MIN = 200;
|
||||
static const int SUCCESS_RESPONSE_MAX = 299;
|
||||
|
||||
const int responseCode = static_cast<int>(response->GetResponseCode());
|
||||
return responseCode < SUCCESS_RESPONSE_MIN || responseCode > SUCCESS_RESPONSE_MAX;
|
||||
}
|
||||
|
||||
static Aws::Utils::DateTime GetServerTimeFromError(const ClientError& error)
|
||||
{
|
||||
const HeaderValueCollection& headers = error.GetResponseHeaders();
|
||||
auto awsDateHeaderIter = headers.find(Aws::Utils::StringUtils::ToLower(Aws::Http::AWS_DATE_HEADER));
|
||||
auto dateHeaderIter = headers.find(Aws::Utils::StringUtils::ToLower(Aws::Http::DATE_HEADER));
|
||||
if (awsDateHeaderIter != headers.end())
|
||||
{
|
||||
return DateTime(awsDateHeaderIter->second.c_str(), DateFormat::AutoDetect);
|
||||
}
|
||||
else if (dateHeaderIter != headers.end())
|
||||
{
|
||||
return DateTime(dateHeaderIter->second.c_str(), DateFormat::AutoDetect);
|
||||
}
|
||||
return DateTime();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <smithy/client/common/AwsSmithyClientUtils.h>
|
||||
#include <smithy/identity/auth/AuthSchemeOption.h>
|
||||
#include <smithy/identity/identity/AwsIdentity.h>
|
||||
#include <smithy/identity/resolver/AwsIdentityResolverBase.h>
|
||||
#include <smithy/identity/signer/AwsSignerBase.h>
|
||||
|
||||
#include <aws/core/utils/FutureOutcome.h>
|
||||
#include <aws/core/client/AWSError.h>
|
||||
#include <aws/core/http/HttpRequest.h>
|
||||
|
||||
#include <aws/crt/Variant.h>
|
||||
#include <aws/crt/Optional.h>
|
||||
#include <aws/core/utils/memory/stl/AWSMap.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
|
||||
namespace smithy
|
||||
{
|
||||
static const char AWS_SMITHY_CLIENT_SIGNING_TAG[] = "AwsClientRequestSigning";
|
||||
//4 Minutes
|
||||
static const std::chrono::milliseconds TIME_DIFF_MAX = std::chrono::minutes(4);
|
||||
//-4 Minutes
|
||||
static const std::chrono::milliseconds TIME_DIFF_MIN = std::chrono::minutes(-4);
|
||||
|
||||
template <typename AuthSchemesVariantT>
|
||||
class AwsClientRequestSigning
|
||||
{
|
||||
public:
|
||||
using HttpRequest = Aws::Http::HttpRequest;
|
||||
using SigningError = Aws::Client::AWSError<Aws::Client::CoreErrors>;
|
||||
using SigningOutcome = Aws::Utils::FutureOutcome<std::shared_ptr<HttpRequest>, SigningError>;
|
||||
using HttpResponseOutcome = Aws::Utils::Outcome<std::shared_ptr<Aws::Http::HttpResponse>, Aws::Client::AWSError<Aws::Client::CoreErrors>>;
|
||||
|
||||
static SigningOutcome SignRequest(std::shared_ptr<HttpRequest> HTTPRequest, const AuthSchemeOption& authSchemeOption,
|
||||
const Aws::UnorderedMap<Aws::String, AuthSchemesVariantT>& authSchemes)
|
||||
{
|
||||
|
||||
auto authSchemeIt = authSchemes.find(authSchemeOption.schemeId);
|
||||
if (authSchemeIt == authSchemes.end())
|
||||
{
|
||||
assert(!"Auth scheme has not been found for a given auth option!");
|
||||
return (SigningError(Aws::Client::CoreErrors::CLIENT_SIGNING_FAILURE,
|
||||
"",
|
||||
"Requested AuthSchemeOption was not found within client Auth Schemes",
|
||||
false/*retryable*/));
|
||||
}
|
||||
|
||||
const AuthSchemesVariantT& authScheme = authSchemeIt->second;
|
||||
|
||||
return SignWithAuthScheme(std::move(HTTPRequest), authScheme, authSchemeOption);
|
||||
}
|
||||
|
||||
static bool AdjustClockSkew(HttpResponseOutcome& outcome, const AuthSchemeOption& authSchemeOption,
|
||||
const Aws::UnorderedMap<Aws::String, AuthSchemesVariantT>& authSchemes)
|
||||
{
|
||||
assert(!outcome.IsSuccess());
|
||||
AWS_LOGSTREAM_WARN(AWS_SMITHY_CLIENT_SIGNING_TAG, "If the signature check failed. This could be because of a time skew. Attempting to adjust the signer.");
|
||||
|
||||
using DateTime = Aws::Utils::DateTime;
|
||||
DateTime serverTime = smithy::client::Utils::GetServerTimeFromError(outcome.GetError());
|
||||
|
||||
auto authSchemeIt = authSchemes.find(authSchemeOption.schemeId);
|
||||
if (authSchemeIt == authSchemes.end())
|
||||
{
|
||||
assert(!"Auth scheme has not been found for a given auth option!");
|
||||
return false;
|
||||
}
|
||||
AuthSchemesVariantT authScheme = authSchemeIt->second;
|
||||
|
||||
ClockSkewVisitor visitor(outcome, serverTime, authSchemeOption);
|
||||
authScheme.Visit(visitor);
|
||||
|
||||
return visitor.m_resultShouldWait;
|
||||
}
|
||||
|
||||
|
||||
protected:
|
||||
struct SignerVisitor
|
||||
{
|
||||
SignerVisitor(std::shared_ptr<HttpRequest> httpRequest, const AuthSchemeOption& targetAuthSchemeOption)
|
||||
: m_httpRequest(std::move(httpRequest)), m_targetAuthSchemeOption(targetAuthSchemeOption)
|
||||
{
|
||||
}
|
||||
|
||||
const std::shared_ptr<HttpRequest> m_httpRequest;
|
||||
const AuthSchemeOption& m_targetAuthSchemeOption;
|
||||
|
||||
Aws::Crt::Optional<SigningOutcome> result;
|
||||
|
||||
template <typename AuthSchemeAlternativeT>
|
||||
void operator()(AuthSchemeAlternativeT& authScheme)
|
||||
{
|
||||
// Auth Scheme Variant alternative contains the requested auth option
|
||||
assert(strcmp(authScheme.schemeId, m_targetAuthSchemeOption.schemeId) == 0);
|
||||
|
||||
using IdentityT = typename std::remove_reference<decltype(authScheme)>::type::IdentityT;
|
||||
using IdentityResolver = IdentityResolverBase<IdentityT>;
|
||||
using Signer = AwsSignerBase<IdentityT>;
|
||||
|
||||
std::shared_ptr<IdentityResolver> identityResolver = authScheme.identityResolver();
|
||||
if (!identityResolver)
|
||||
{
|
||||
result.emplace(SigningError(Aws::Client::CoreErrors::CLIENT_SIGNING_FAILURE,
|
||||
"",
|
||||
"Auth scheme provided a nullptr identityResolver",
|
||||
false/*retryable*/));
|
||||
return;
|
||||
}
|
||||
|
||||
auto identityResult = identityResolver->getIdentity(m_targetAuthSchemeOption.identityProperties(), m_targetAuthSchemeOption.identityProperties());
|
||||
|
||||
if (!identityResult.IsSuccess())
|
||||
{
|
||||
result.emplace(identityResult.GetError());
|
||||
return;
|
||||
}
|
||||
auto identity = std::move(identityResult.GetResultWithOwnership());
|
||||
|
||||
std::shared_ptr<Signer> signer = authScheme.signer();
|
||||
if (!signer)
|
||||
{
|
||||
result.emplace(SigningError(Aws::Client::CoreErrors::CLIENT_SIGNING_FAILURE,
|
||||
"",
|
||||
"Auth scheme provided a nullptr signer",
|
||||
false/*retryable*/));
|
||||
return;
|
||||
}
|
||||
|
||||
result.emplace(signer->sign(m_httpRequest, *identity, m_targetAuthSchemeOption.signerProperties()));
|
||||
}
|
||||
};
|
||||
|
||||
static
|
||||
SigningOutcome SignWithAuthScheme(std::shared_ptr<HttpRequest> httpRequest, const AuthSchemesVariantT& authSchemesVariant,
|
||||
const AuthSchemeOption& targetAuthSchemeOption)
|
||||
{
|
||||
SignerVisitor visitor(httpRequest, targetAuthSchemeOption);
|
||||
AuthSchemesVariantT authSchemesVariantCopy(authSchemesVariant); // TODO: allow const visiting
|
||||
authSchemesVariantCopy.Visit(visitor);
|
||||
|
||||
if (!visitor.result)
|
||||
{
|
||||
return (SigningError(Aws::Client::CoreErrors::CLIENT_SIGNING_FAILURE,
|
||||
"",
|
||||
"Failed to sign with an unknown error",
|
||||
false/*retryable*/));
|
||||
}
|
||||
return std::move(*visitor.result);
|
||||
}
|
||||
|
||||
struct ClockSkewVisitor
|
||||
{
|
||||
using DateTime = Aws::Utils::DateTime;
|
||||
using DateFormat = Aws::Utils::DateFormat;
|
||||
using ClientError = Aws::Client::AWSError<Aws::Client::CoreErrors>;
|
||||
|
||||
ClockSkewVisitor(HttpResponseOutcome& outcome, const DateTime& serverTime, const AuthSchemeOption& targetAuthSchemeOption)
|
||||
: m_outcome(outcome), m_serverTime(serverTime), m_targetAuthSchemeOption(targetAuthSchemeOption)
|
||||
{
|
||||
}
|
||||
|
||||
bool m_resultShouldWait = false;
|
||||
HttpResponseOutcome& m_outcome;
|
||||
const Aws::Utils::DateTime& m_serverTime;
|
||||
const AuthSchemeOption& m_targetAuthSchemeOption;
|
||||
|
||||
template <typename AuthSchemeAlternativeT>
|
||||
void operator()(AuthSchemeAlternativeT& authScheme)
|
||||
{
|
||||
// Auth Scheme Variant alternative contains the requested auth option
|
||||
assert(strcmp(authScheme.schemeId, m_targetAuthSchemeOption.schemeId) == 0);
|
||||
|
||||
using IdentityT = typename std::remove_reference<decltype(authScheme)>::type::IdentityT;
|
||||
using Signer = AwsSignerBase<IdentityT>;
|
||||
|
||||
std::shared_ptr<Signer> signer = authScheme.signer();
|
||||
if (!signer)
|
||||
{
|
||||
AWS_LOGSTREAM_ERROR(AWS_SMITHY_CLIENT_SIGNING_TAG, "Failed to adjust signing clock skew. Signer is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto signingTimestamp = signer->GetSigningTimestamp();
|
||||
if (!m_serverTime.WasParseSuccessful() || m_serverTime == DateTime())
|
||||
{
|
||||
AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_SIGNING_TAG, "Date header was not found in the response, can't attempt to detect clock skew");
|
||||
return;
|
||||
}
|
||||
|
||||
AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_SIGNING_TAG, "Server time is " << m_serverTime.ToGmtString(DateFormat::RFC822) << ", while client time is " << DateTime::Now().ToGmtString(DateFormat::RFC822));
|
||||
auto diff = DateTime::Diff(m_serverTime, signingTimestamp);
|
||||
//only try again if clock skew was the cause of the error.
|
||||
if (diff >= TIME_DIFF_MAX || diff <= TIME_DIFF_MIN)
|
||||
{
|
||||
diff = DateTime::Diff(m_serverTime, DateTime::Now());
|
||||
AWS_LOGSTREAM_INFO(AWS_SMITHY_CLIENT_SIGNING_TAG, "Computed time difference as " << diff.count() << " milliseconds. Adjusting signer with the skew.");
|
||||
signer->SetClockSkew(diff);
|
||||
ClientError newError(m_outcome.GetError());
|
||||
newError.SetRetryableType(Aws::Client::RetryableType::RETRYABLE);
|
||||
|
||||
m_outcome = std::move(newError);
|
||||
m_resultShouldWait = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user