You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2017/05/31 18:03:41 UTC
[trafficserver] branch 7.1.x updated: Issue #1605 AWS Signature
Version 4
This is an automated email from the ASF dual-hosted git repository.
zwoop pushed a commit to branch 7.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/7.1.x by this push:
new 024074b Issue #1605 AWS Signature Version 4
024074b is described below
commit 024074be22dc35a399debe59353743dd90c33eba
Author: Gancho Tenev <gt...@gmail.com>
AuthorDate: Tue Apr 25 13:02:44 2017 -0700
Issue #1605 AWS Signature Version 4
Signature Calculations for the Authorization Header:
Transferring Payload in a Single Chunk (Unsigned payload option)
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
(cherry picked from commit fa731369b474cb3c46b7b8ddf98490c198cd4605)
Conflicts:
plugins/s3_auth/s3_auth.cc
---
doc/admin-guide/plugins/s3_auth.en.rst | 114 ++++--
plugins/s3_auth/Makefile.inc | 2 +-
plugins/s3_auth/aws_auth_v4.cc | 691 +++++++++++++++++++++++++++++++++
plugins/s3_auth/aws_auth_v4.h | 207 ++++++++++
plugins/s3_auth/s3_auth.cc | 273 ++++++++++++-
5 files changed, 1247 insertions(+), 40 deletions(-)
diff --git a/doc/admin-guide/plugins/s3_auth.en.rst b/doc/admin-guide/plugins/s3_auth.en.rst
index d6f5709..7293188 100644
--- a/doc/admin-guide/plugins/s3_auth.en.rst
+++ b/doc/admin-guide/plugins/s3_auth.en.rst
@@ -27,57 +27,119 @@ to use ``S3`` as your origin server, yet want to avoid direct user access to
the content.
Using the plugin
-----------------
+================
-There are three configuration options for this plugin::
-
- --access_key <key>
- --secret_key <key>
- --virtual_host
- --config <config file>
+Using the plugin in a remap rule would be e.g.::
-Using the first two in a remap rule would be e.g.::
+ # remap.config
... @plugin=s3_auth @pparam=--access_key @pparam=my-key \
@pparam=--secret_key @pparam=my-secret \
@pparam=--virtual_host
-Alternatively, you can store the access key and secret in an external
-configuration file, and point the remap rule(s) to it:
+Alternatively, you can store the access key and secret in an external configuration file, and point the remap rule(s) to it::
- ... @plugin=s3_auth @pparam=--config @pparam=s3.config
+ # remap.config
+ ... @plugin=s3_auth @pparam=--config @pparam=s3_auth_v2.config
-Where s3.config would look like::
- # AWS S3 authentication
- access_key=my-key
- secret_key=my-secret
- virtual_host=yes
+Where ``s3.config`` could look like::
+ # s3_auth_v2.config
-For more details on the S3 auth, see::
+ access_key=my-key
+ secret_key=my-secret
+ version=2
+ virtual_host=yes
- http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
+Both ways could be combined as well
-ToDo
-----
+AWS Authentication version 4
+============================
-This is a pretty barebone start for the S3 services, it is missing a number of features:
+The s3_auth plugin fully implements: `AWS Signing Version 4 <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html>`_ / `Authorization Header <http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html>`_ / `Transferring Payload in a Single Chunk <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html>`_ / Unsigned Payload Option
-- It does not do UTF8 encoding (as required)
+Configuration options::
-- It only implements the v2 authentication mechanism. For details on v4, see
+ # Mandatory options
+ --access_key=<access_id>
+ --secret_key=<key>
+ --version=4
- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
+ # Optional
+ --v4-include-headers=<comma-separated-list-of-headers-to-be-signed>
+ --v4-exclude-headers=<comma-separated-list-of-headers-not-to-be-signed>
+ --v4-region-map=region_map.config
-- It does not deal with canonicalization of AMZ headers.
-- It does not handle POST requests (but do we need to ?)
+If the following option is used then the options could be specified in a file::
+
+ --config=s3_auth_v4.config
+
+
+The ``s3_auth_v4.config`` config file could look like this::
+
+ # s3_auth_v4.config
+
+ access_key=<access_id>
+ secret_key=<secret_key>
+ version=4
+ v4-include-headers=<comma-separated-list-of-headers-to-be-signed>
+ v4-exclude-headers=<comma-separated-list-of-headers-not-to-be-signed>
+ v4-region-map=region_map.config
+
+Where the ``region_map.config`` defines the entry-point hostname to region mapping i.e.::
+
+ # region_map.config
+
+ # "us-east-1"
+ s3.amazonaws.com : us-east-1
+ s3-external-1.amazonaws.com : us-east-1
+ s3.dualstack.us-east-1.amazonaws.com : us-east-1
+
+ # us-west-1
+ s3-us-west-1.amazonaws.com : us-west-1
+ s3.dualstack.us-west-1.amazonaws.com : us-west-1
+
+ # Default region if no entry-point matches:
+ : s3.amazonaws.com
+
+If ``--v4-region-map`` is not specified the plugin defaults to the mapping defined in `"Regions and Endpoints - S3" <http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region>`_
+According to `Transferring Payload in a Single Chunk <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html>`_ specification
+the ``CanonicalHeaders`` list *must* include the ``Host`` header, the ``Content-Type`` header if present in the request and all the ``x-amz-*`` headers
+so ``--v4-include-headers`` and ``--v4-exclude-headers`` do not impact those headers and they are *always* signed.
+
+The ``Via`` and ``X-Forwarded-For`` headers are *always* excluded from the signature since they are meant to be changed by the proxies and signing them could lead to invalidation of the signatue.
+
+If ``--v4-include-headers`` is not specified all headers except those specified in ``--v4-exclude-headers`` will be signed.
+
+If ``--v4-include-headers`` is specified only the headers specified will be signed except those specified in ``--v4-exclude-headers``
+
+
+AWS Authentication version 2
+============================
+
+For more details on the S3 auth version 2 , see: `Signing and Authenticating REST Requests <http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html>`_
+
+
+There are 4 plugin configuration options for version 2::
+
+ --access_key <access_id>
+ --secret_key <secret_key>
+ --virtual_host
+ --config <config file>
+ --version=2
+
+This is a pretty barebone start for the S3 services, it is missing a number of features:
+
+- It does not do UTF8 encoding (as required)
+- It does not deal with canonicalization of AMZ headers.
+- It does not handle POST requests (but do we need to ?)
- It does not incorporate query parameters.
diff --git a/plugins/s3_auth/Makefile.inc b/plugins/s3_auth/Makefile.inc
index eb63887..7865d5e 100644
--- a/plugins/s3_auth/Makefile.inc
+++ b/plugins/s3_auth/Makefile.inc
@@ -15,4 +15,4 @@
# limitations under the License.
pkglib_LTLIBRARIES += s3_auth/s3_auth.la
-s3_auth_s3_auth_la_SOURCES = s3_auth/s3_auth.cc
+s3_auth_s3_auth_la_SOURCES = s3_auth/s3_auth.cc s3_auth/aws_auth_v4.cc
diff --git a/plugins/s3_auth/aws_auth_v4.cc b/plugins/s3_auth/aws_auth_v4.cc
new file mode 100644
index 0000000..65c65b9
--- /dev/null
+++ b/plugins/s3_auth/aws_auth_v4.cc
@@ -0,0 +1,691 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+/**
+ * @file aws_auth_v4.cc
+ * @brief AWS Auth v4 signing utility.
+ * @see aws_auth_v4.h
+ */
+
+#include <cstring> /* strlen() */
+#include <ctime> /* strftime(), time(), gmtime_r() */
+#include <iomanip> /* std::setw */
+#include <sstream> /* std::stringstream */
+#include <openssl/sha.h> /* SHA(), sha256_Update(), SHA256_Final, etc. */
+#include <openssl/hmac.h> /* HMAC() */
+
+#undef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+#include <iostream>
+#endif
+
+#include "aws_auth_v4.h"
+
+/**
+ * @brief Lower-case Base16 encode a character string (hexadecimal format)
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ * Base16 RFC4648: https://tools.ietf.org/html/rfc4648#section-8
+ *
+ * @param in ptr to an input counted string to be base16 encoded.
+ * @param inLen input character string length
+ * @return base16 encoded string.
+ */
+String
+base16Encode(const char *in, size_t inLen)
+{
+ if (nullptr == in || inLen == 0) {
+ return {};
+ }
+
+ std::stringstream result;
+
+ const char *src = in;
+ const char *srcEnd = in + inLen;
+
+ while (src < srcEnd) {
+ result << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>((*src) & 0xFF);
+ src++;
+ }
+ return result.str();
+}
+
+/**
+ * @brief URI-encode a character string (AWS specific version, see spec)
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @param in string to be URI encoded
+ * @param isObjectName if true don't encode '/', keep it as it is.
+ * @return encoded string.
+ */
+String
+uriEncode(const String in, bool isObjectName)
+{
+ std::stringstream result;
+
+ for (std::string::size_type i = 0; i < in.length(); i++) {
+ if (isalnum(in[i]) || in[i] == '-' || in[i] == '_' || in[i] == '.' || in[i] == '~') {
+ /* URI encode every byte except the unreserved characters:
+ * 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'. */
+ result << in[i];
+ } else if (in[i] == ' ') {
+ /* The space character is a reserved character and must be encoded as "%20" (and not as "+"). */
+ result << "%20";
+ } else if (isObjectName && in[i] == '/') {
+ /* Encode the forward slash character, '/', everywhere except in the object key name. */
+ result << "/";
+ } else {
+ /* Letters in the hexadecimal value must be upper-case, for example "%1A". */
+ result << "%" << std::uppercase << std::setfill('0') << std::setw(2) << std::hex << (int)in[i];
+ }
+ }
+
+ return result.str();
+}
+
+/**
+ * @brief trim the white-space character from the beginning and the end of the string ("in-place", just moving pointers around)
+ *
+ * @param in ptr to an input string
+ * @param inLen input character count
+ * @param newLen trimmed string character count.
+ * @return pointer to the trimmed string.
+ */
+const char *
+trimWhiteSpaces(const char *in, size_t inLen, size_t &newLen)
+{
+ if (nullptr == in || inLen == 0) {
+ return in;
+ }
+
+ const char *first = in;
+ while (size_t(first - in) < inLen && isspace(*first)) {
+ first++;
+ }
+
+ const char *last = in + inLen - 1;
+ while (last > in && isspace(*last)) {
+ last--;
+ }
+
+ newLen = last - first + 1;
+ return first;
+}
+
+/**
+ * @brief Trim white spaces from beginning and end.
+ * @returns trimmed string
+ */
+String
+trimWhiteSpaces(const String &s)
+{
+ /* @todo do this better? */
+ static const String whiteSpace = " \t\n\v\f\r";
+ size_t start = s.find_first_not_of(whiteSpace);
+ if (String::npos == start) {
+ return String();
+ }
+ size_t stop = s.find_last_not_of(whiteSpace);
+ return s.substr(start, stop - start + 1);
+}
+
+/*
+ * Group of static inline helper function for less error prone parameter handling and unit test logging.
+ */
+inline static void
+sha256Update(SHA256_CTX *ctx, const char *in, size_t inLen)
+{
+ SHA256_Update(ctx, in, inLen);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << String(in, inLen);
+#endif
+}
+
+inline static void
+sha256Update(SHA256_CTX *ctx, const char *in)
+{
+ sha256Update(ctx, in, strlen(in));
+}
+
+inline static void
+sha256Update(SHA256_CTX *ctx, const String &in)
+{
+ sha256Update(ctx, in.c_str(), in.length());
+}
+
+inline static void
+sha256Final(unsigned char hex[SHA256_DIGEST_LENGTH], SHA256_CTX *ctx)
+{
+ SHA256_Final(hex, ctx);
+}
+
+/**
+ * @brief: Payload SHA 256 = Hex(SHA256Hash(<payload>) (no new-line char at end)
+ *
+ * @todo support for signing of PUSH, POST content / payload
+ * @param signPayload specifies whether the content / payload should be signed
+ * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
+ */
+String
+getPayloadSha256(bool signPayload)
+{
+ static const String UNSIGNED_PAYLOAD("UNSIGNED-PAYLOAD");
+
+ if (!signPayload) {
+ return UNSIGNED_PAYLOAD;
+ }
+
+ unsigned char payloadHash[SHA256_DIGEST_LENGTH];
+ SHA256((const unsigned char *)"", 0, payloadHash); /* empty content */
+
+ return base16Encode((char *)payloadHash, SHA256_DIGEST_LENGTH);
+}
+
+/**
+ * @brief Get Canonical Uri SHA256 Hash
+ *
+ * Hex(SHA256Hash(<CanonicalRequest>))
+ * AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @param api an TS API wrapper that will provide interface to HTTP request elements (method, path, query, headers, etc).
+ * @param signPayload specifies if the content / payload should be signed.
+ * @param includeHeaders headers that must be signed
+ * @param excludeHeaders headers that must not be signed
+ * @param signedHeaders a reference to a string to which the signed headers names will be appended
+ * @return SHA256 hash of the canonical request.
+ */
+String
+getCanonicalRequestSha256Hash(TsInterface &api, bool signPayload, const StringSet &includeHeaders, const StringSet &excludeHeaders,
+ String &signedHeaders)
+{
+ int length;
+ const char *str = nullptr;
+ unsigned char canonicalRequestSha256Hash[SHA256_DIGEST_LENGTH];
+ SHA256_CTX canonicalRequestSha256Ctx;
+
+ SHA256_Init(&canonicalRequestSha256Ctx);
+
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << "<CanonicalRequest>";
+#endif
+
+ /* <HTTPMethod>\n */
+ str = api.getMethod(&length);
+ sha256Update(&canonicalRequestSha256Ctx, str, length);
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+ /* URI Encoded Canonical URI
+ * <CanonicalURI>\n */
+ str = api.getPath(&length);
+ String path("/");
+ path.append(str, length);
+ String canonicalUri = uriEncode(path, /* isObjectName */ true);
+ sha256Update(&canonicalRequestSha256Ctx, canonicalUri);
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+ /* Sorted Canonical Query String
+ * <CanonicalQueryString>\n */
+ const char *query = api.getQuery(&length);
+
+ StringSet paramNames;
+ StringMap paramsMap;
+ std::istringstream istr(String(query, length));
+ String token;
+ StringSet container;
+
+ while (std::getline(istr, token, '&')) {
+ String::size_type pos(token.find_first_of('='));
+ String param(token.substr(0, pos == String::npos ? token.size() : pos));
+ String value(pos == String::npos ? "" : token.substr(pos + 1, token.size()));
+
+ String encodedParam = uriEncode(param, /* isObjectName */ false);
+
+ paramNames.insert(encodedParam);
+ paramsMap[encodedParam] = uriEncode(value, /* isObjectName */ false);
+ }
+
+ String queryStr;
+ for (StringSet::iterator it = paramNames.begin(); it != paramNames.end(); it++) {
+ if (!queryStr.empty()) {
+ queryStr.append("&");
+ }
+ queryStr.append(*it);
+ queryStr.append("=").append(paramsMap[*it]);
+ }
+ sha256Update(&canonicalRequestSha256Ctx, queryStr);
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+ /* Sorted Canonical Headers
+ * <CanonicalHeaders>\n */
+ StringSet signedHeadersSet;
+ StringMap headersMap;
+
+ for (HeaderIterator it = api.headerBegin(); it != api.headerEnd(); it++) {
+ int nameLen;
+ int valueLen;
+ const char *name = it.getName(&nameLen);
+ const char *value = it.getValue(&valueLen);
+
+ if (nullptr == name || 0 == nameLen) {
+ continue;
+ }
+
+ String lowercaseName(name, nameLen);
+ std::transform(lowercaseName.begin(), lowercaseName.end(), lowercaseName.begin(), ::tolower);
+
+ /* Host, content-type and x-amx-* headers are mandatory */
+ bool xAmzHeader = (lowercaseName.length() >= X_AMZ.length() && 0 == lowercaseName.compare(0, X_AMZ.length(), X_AMZ));
+ bool contentTypeHeader = (0 == CONTENT_TYPE.compare(lowercaseName));
+ bool hostHeader = (0 == HOST.compare(lowercaseName));
+ if (!xAmzHeader && !contentTypeHeader && !hostHeader) {
+ /* Skip internal headers (starting with '@'*/
+ if ('@' == name[0] /* exclude internal headers */) {
+ continue;
+ }
+
+ /* @todo do better here, since iterating over the headers in ATS is known to be less efficient,
+ * come up with a better way if include headers set is non-empty */
+ bool include =
+ (!includeHeaders.empty() && includeHeaders.end() != includeHeaders.find(lowercaseName)); /* requested to be included */
+ bool exclude =
+ (!excludeHeaders.empty() && excludeHeaders.end() != excludeHeaders.find(lowercaseName)); /* requested to be excluded */
+
+ if ((includeHeaders.empty() && exclude) || (!includeHeaders.empty() && (!include || exclude))) {
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << "ignore header: " << String(name, nameLen) << std::endl;
+#endif
+ continue;
+ }
+ }
+
+ size_t trimValueLen = 0;
+ const char *trimValue = trimWhiteSpaces(value, valueLen, trimValueLen);
+
+ signedHeadersSet.insert(lowercaseName);
+ headersMap[lowercaseName] = String(trimValue, trimValueLen);
+ }
+
+ for (StringSet::iterator it = signedHeadersSet.begin(); it != signedHeadersSet.end(); it++) {
+ sha256Update(&canonicalRequestSha256Ctx, *it);
+ sha256Update(&canonicalRequestSha256Ctx, ":");
+ sha256Update(&canonicalRequestSha256Ctx, headersMap[*it]);
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+ }
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+ for (StringSet::iterator it = signedHeadersSet.begin(); it != signedHeadersSet.end(); ++it) {
+ if (!signedHeaders.empty()) {
+ signedHeaders.append(";");
+ }
+ signedHeaders.append(*it);
+ }
+
+ sha256Update(&canonicalRequestSha256Ctx, signedHeaders);
+ sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+ /* Hex(SHA256Hash(<payload>) (no new-line char at end)
+ * @TODO support non-empty content, i.e. POST */
+ String payloadSha256Hash = getPayloadSha256(signPayload);
+ sha256Update(&canonicalRequestSha256Ctx, payloadSha256Hash);
+
+ /* Hex(SHA256Hash(<CanonicalRequest>)) */
+ sha256Final(canonicalRequestSha256Hash, &canonicalRequestSha256Ctx);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << "</CanonicalRequest>" << std::endl;
+#endif
+ return base16Encode((char *)canonicalRequestSha256Hash, SHA256_DIGEST_LENGTH);
+}
+
+/**
+ * @brief Default AWS entry-point host name to region based on (S3):
+ *
+ * @see http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+ * it is used to get the region programmatically w/o configuration
+ * parameters and can (meant to) be overwritten if necessary.
+ */
+const StringMap
+createDefaultRegionMap()
+{
+ StringMap m;
+ /* us-east-2 */
+ m["s3.us-east-2.amazonaws.com"] = "us-east-2";
+ m["s3-us-east-2.amazonaws.com"] = "us-east-2";
+ m["s3.dualstack.us-east-2.amazonaws.com"] = "us-east-2";
+ /* "us-east-1" */
+ m["s3.amazonaws.com"] = "us-east-1";
+ m["s3-external-1.amazonaws.com"] = "us-east-1";
+ m["s3.dualstack.us-east-1.amazonaws.com"] = "us-east-1";
+ /* us-west-1 */
+ m["s3-us-west-1.amazonaws.com"] = "us-west-1";
+ m["s3.dualstack.us-west-1.amazonaws.com"] = "us-west-1";
+ /* us-west-2 */
+ m["s3-us-west-2.amazonaws.com"] = "us-west-2";
+ m["s3.dualstack.us-west-2.amazonaws.com"] = "us-west-2";
+ /* ca-central-1 */
+ m["s3.ca-central-1.amazonaws.com"] = "ca-central-1";
+ m["s3-ca-central-1.amazonaws.com"] = "ca-central-1";
+ m["s3.dualstack.ca-central-1.amazonaws.com"] = "ca-central-1";
+ /* ap-south-1 */
+ m["s3.ap-south-1.amazonaws.com"] = "ap-south-1";
+ m["s3-ap-south-1.amazonaws.com"] = "ap-south-1";
+ m["s3.dualstack.ap-south-1.amazonaws.com"] = "ap-south-1";
+ /* ap-northeast-2 */
+ m["s3.ap-northeast-2.amazonaws.com"] = "ap-northeast-2";
+ m["s3-ap-northeast-2.amazonaws.com"] = "ap-northeast-2";
+ m["s3.dualstack.ap-northeast-2.amazonaws.com"] = "ap-northeast-2";
+ /* ap-southeast-1 */
+ m["s3-ap-southeast-1.amazonaws.com"] = "ap-southeast-1";
+ m["s3.dualstack.ap-southeast-1.amazonaws.com"] = "ap-southeast-1";
+ /* ap-southeast-2 */
+ m["s3-ap-southeast-2.amazonaws.com"] = "ap-southeast-2";
+ m["s3.dualstack.ap-southeast-2.amazonaws.com"] = "ap-southeast-2";
+ /* ap-northeast-1 */
+ m["s3-ap-northeast-1.amazonaws.com"] = "ap-northeast-1";
+ m["s3.dualstack.ap-northeast-1.amazonaws.com"] = "ap-northeast-1";
+ /* eu-central-1 */
+ m["s3.eu-central-1.amazonaws.com"] = "eu-central-1";
+ m["s3-eu-central-1.amazonaws.com"] = "eu-central-1";
+ m["s3.dualstack.eu-central-1.amazonaws.com"] = "eu-central-1";
+ /* eu-west-1 */
+ m["s3-eu-west-1.amazonaws.com"] = "eu-central-1";
+ m["s3.dualstack.eu-west-1.amazonaws.com"] = "eu-central-1";
+ /* eu-west-2 */
+ m["s3.eu-west-2.amazonaws.com"] = "eu-west-2";
+ m["s3-eu-west-2.amazonaws.com"] = "eu-west-2";
+ m["s3.dualstack.eu-west-2.amazonaws.com"] = "eu-west-2";
+ /* sa-east-1 */
+ m["s3-sa-east-1.amazonaws.com"] = "sa-east-1";
+ m["s3.dualstack.sa-east-1.amazonaws.com"] = "sa-east-1";
+ /* default "us-east-1" * */
+ m[""] = "us-east-1";
+ return m;
+}
+const StringMap defaultDefaultRegionMap = createDefaultRegionMap();
+
+/**
+ * @description default list of headers to be excluded from the signing
+ */
+const StringSet
+createDefaultExcludeHeaders()
+{
+ StringSet m;
+ /* exclude headers that are meant to be changed */
+ m.insert("x-forwarded-for");
+ m.insert("via");
+ return m;
+}
+const StringSet defaultExcludeHeaders = createDefaultExcludeHeaders();
+
+/**
+ * @description default list of headers to be included in the signing
+ */
+const StringSet
+createDefaultIncludeHeaders()
+{
+ StringSet m;
+ return m;
+}
+const StringSet defaultIncludeHeaders = createDefaultIncludeHeaders();
+
+/**
+ * @brief Get AWS (S3) region from the entry-point
+ *
+ * @see Implementation based on the following:
+ * http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
+ * http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+ *
+ * @param regionMap map containing entry-point to region mapping
+ * @param entryPoint entry-point name
+ * @param entryPointLen - entry point string length
+ */
+String
+getRegion(const StringMap ®ionMap, const char *entryPoint, size_t entryPointLen)
+{
+ String region;
+ size_t dot = String::npos;
+ String hostname(entryPoint, entryPointLen);
+
+ /* Start looking for a match from the top-level domain backwards to keep the mapping generic
+ * (so we can override it if we need later) */
+ do {
+ String name;
+ dot = hostname.rfind('.', dot - 1);
+ if (String::npos != dot) {
+ name = hostname.substr(dot + 1);
+ } else {
+ name = hostname;
+ }
+ if (regionMap.end() != regionMap.find(name)) {
+ region = regionMap.at(name);
+ break;
+ }
+ } while (String::npos != dot);
+
+ if (region.empty() && regionMap.end() != regionMap.find("")) {
+ region = regionMap.at(""); /* default region if nothing matches */
+ }
+
+ return region;
+}
+
+/**
+ * @brief Constructs the string to sign
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+
+ * @param entryPoint entry-point name
+ * @param entryPointLen entry-point name length
+ * @param dateTime - ISO 8601 time
+ * @param dateTimeLen - ISO 8601 time length
+ * @param region AWS region name
+ * @param region AWS region name length
+ * @param service service name
+ * @param serviceLen service name length
+ * @param sha256Hash canonical request SHA 256 hash
+ * @param sha256HashLen canonical request SHA 256 hash length
+ * @returns the string to sign
+ */
+String
+getStringToSign(const char *entryPoint, size_t EntryPointLen, const char *dateTime, size_t dateTimeLen, const char *region,
+ size_t regionLen, const char *service, size_t serviceLen, const char *sha256Hash, size_t sha256HashLen)
+{
+ String stringToSign;
+
+ /* AWS4-HMAC-SHA256\n (hard-coded, other values? */
+ stringToSign.append("AWS4-HMAC-SHA256\n");
+
+ /* time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>\n */
+ stringToSign.append(dateTime, dateTimeLen);
+ stringToSign.append("\n");
+
+ /* Scope: date.Format(<YYYYMMDD>) + "/" + <region> + "/" + <service> + "/aws4_request" */
+ stringToSign.append(dateTime, 8); /* Get only the YYYYMMDD */
+ stringToSign.append("/");
+ stringToSign.append(region, regionLen);
+ stringToSign.append("/");
+ stringToSign.append(service, serviceLen);
+ stringToSign.append("/aws4_request\n");
+ stringToSign.append(sha256Hash, sha256HashLen);
+
+ return stringToSign;
+}
+
+/**
+ * @brief Calculates the final signature based on the following parameters and base16 encodes it.
+ *
+ * signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<awsSecret>", <dateTime>),
+ * <awsRegion>), <awsService>),"aws4_request")
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @param awsSecret AWS secret
+ * @param awsSecretLen AWS secret length
+ * @param awsRegion AWS region
+ * @param awsRegionLen AWS region length
+ * @param awsService AWS Service name
+ * @param awsServiceLen AWS service name length
+ * @param dateTime ISO8601 date/time
+ * @param dateTimeLen ISO8601 date/time length
+ * @param stringToSign string to sign
+ * @param stringToSignLen length of the string to sign
+ * @param base16Signature output buffer where the base16 signature will be stored
+ * @param base16SignatureLen size of the signature buffer = EVP_MAX_MD_SIZE (at least)
+ *
+ * @return number of characters written to the output buffer
+ */
+size_t
+getSignature(const char *awsSecret, size_t awsSecretLen, const char *awsRegion, size_t awsRegionLen, const char *awsService,
+ size_t awsServiceLen, const char *dateTime, size_t dateTimeLen, const char *stringToSign, size_t stringToSignLen,
+ char *signature, size_t signatureLen)
+{
+ unsigned int dateKeyLen = EVP_MAX_MD_SIZE;
+ unsigned char dateKey[EVP_MAX_MD_SIZE];
+ unsigned int dateRegionKeyLen = EVP_MAX_MD_SIZE;
+ unsigned char dateRegionKey[EVP_MAX_MD_SIZE];
+ unsigned int dateRegionServiceKeyLen = EVP_MAX_MD_SIZE;
+ unsigned char dateRegionServiceKey[EVP_MAX_MD_SIZE];
+ unsigned int signingKeyLen = EVP_MAX_MD_SIZE;
+ unsigned char signingKey[EVP_MAX_MD_SIZE];
+
+ size_t keyLen = 4 + awsSecretLen;
+ char key[keyLen];
+ strncpy(key, "AWS4", 4);
+ strncpy(key + 4, awsSecret, awsSecretLen);
+
+ unsigned int len = signatureLen;
+ if (HMAC(EVP_sha256(), key, keyLen, (unsigned char *)dateTime, dateTimeLen, dateKey, &dateKeyLen) &&
+ HMAC(EVP_sha256(), dateKey, dateKeyLen, (unsigned char *)awsRegion, awsRegionLen, dateRegionKey, &dateRegionKeyLen) &&
+ HMAC(EVP_sha256(), dateRegionKey, dateRegionKeyLen, (unsigned char *)awsService, awsServiceLen, dateRegionServiceKey,
+ &dateRegionServiceKeyLen) &&
+ HMAC(EVP_sha256(), dateRegionServiceKey, dateRegionServiceKeyLen, (unsigned char *)"aws4_request", 12, signingKey,
+ &signingKeyLen) &&
+ HMAC(EVP_sha256(), signingKey, signingKeyLen, (unsigned char *)stringToSign, stringToSignLen, (unsigned char *)signature,
+ &len)) {
+ return len;
+ }
+
+ return 0;
+}
+
+/**
+ * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
+ */
+size_t
+getIso8601Time(time_t *now, char *dateTime, size_t dateTimeLen)
+{
+ struct tm tm;
+ return strftime(dateTime, dateTimeLen, "%Y%m%dT%H%M%SZ", gmtime_r(now, &tm));
+}
+
+/**
+ * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
+ */
+const char *
+AwsAuthV4::getDateTime(size_t *dateTimeLen)
+{
+ *dateTimeLen = sizeof(_dateTime) - 1;
+ return _dateTime;
+}
+
+/**
+ * @brief: HTTP content / payload SHA 256 = Hex(SHA256Hash(<payload>)
+ * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
+ */
+String
+AwsAuthV4::getPayloadHash()
+{
+ return getPayloadSha256(_signPayload);
+}
+
+/**
+ * @brief Get the value of the Authorization header (AWS authorization) v4
+ * @return the Authorization header value
+ */
+String
+AwsAuthV4::getAuthorizationHeader()
+{
+ String signedHeaders;
+ String canonicalReq = getCanonicalRequestSha256Hash(_api, _signPayload, _includedHeaders, _excludedHeaders, signedHeaders);
+
+ int hostLen = 0;
+ const char *host = _api.getHost(&hostLen);
+
+ String awsRegion = getRegion(_regionMap, host, hostLen);
+
+ String stringToSign = getStringToSign(host, hostLen, _dateTime, sizeof(_dateTime) - 1, awsRegion.c_str(), awsRegion.length(),
+ _awsService, _awsServiceLen, canonicalReq.c_str(), canonicalReq.length());
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << "<StringToSign>" << stringToSign << "</StringToSign>" << std::endl;
+#endif
+
+ char signature[EVP_MAX_MD_SIZE];
+ size_t signatureLen =
+ getSignature(_awsSecretAccessKey, _awsSecretAccessKeyLen, awsRegion.c_str(), awsRegion.length(), _awsService, _awsServiceLen,
+ _dateTime, 8, stringToSign.c_str(), stringToSign.length(), signature, EVP_MAX_MD_SIZE);
+
+ String base16Signature = base16Encode(signature, signatureLen);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+ std::cout << "<SignatureProvided>" << base16Signature << "</SignatureProvided>" << std::endl;
+#endif
+
+ std::stringstream authorizationHeader;
+ authorizationHeader << "AWS4-HMAC-SHA256 ";
+ authorizationHeader << "Credential=" << String(_awsAccessKeyId, _awsAccessKeyIdLen) << "/" << String(_dateTime, 8) << "/"
+ << awsRegion << "/" << String(_awsService, _awsServiceLen) << "/"
+ << "aws4_request"
+ << ",";
+ authorizationHeader << "SignedHeaders=" << signedHeaders << ",";
+ authorizationHeader << "Signature=" << base16Signature;
+
+ return authorizationHeader.str();
+}
+
+/**
+ * @brief Authorization v4 constructor
+ *
+ * @param api wrapper providing access to HTTP request elements (URI host, path, query, headers, etc.)
+ * @param now current time-stamp
+ * @param signPayload defines if the HTTP content / payload needs to be signed
+ * @param awsAccessKeyId AWS access key ID
+ * @param awsAccessKeyIdLen AWS access key ID length
+ * @param awsSecretAccessKey AWS secret
+ * @param awsSecretAccessKeyLen AWS secret length
+ * @param awsService AWS Service name
+ * @param awsServiceLen AWS service name length
+ * @param includeHeaders set of headers to be signed
+ * @param excludeHeaders set of headers not to be signed
+ * @param regionMap entry-point to AWS region mapping
+ */
+AwsAuthV4::AwsAuthV4(TsInterface &api, time_t *now, bool signPayload, const char *awsAccessKeyId, size_t awsAccessKeyIdLen,
+ const char *awsSecretAccessKey, size_t awsSecretAccessKeyLen, const char *awsService, size_t awsServiceLen,
+ const StringSet &includedHeaders, const StringSet &excludedHeaders, const StringMap ®ionMap)
+ : _api(api),
+ _signPayload(signPayload),
+ _awsAccessKeyId(awsAccessKeyId),
+ _awsAccessKeyIdLen(awsAccessKeyIdLen),
+ _awsSecretAccessKey(awsSecretAccessKey),
+ _awsSecretAccessKeyLen(awsSecretAccessKeyLen),
+ _awsService(awsService),
+ _awsServiceLen(awsServiceLen),
+ _includedHeaders(includedHeaders.empty() ? defaultIncludeHeaders : includedHeaders),
+ _excludedHeaders(excludedHeaders.empty() ? defaultExcludeHeaders : excludedHeaders),
+ _regionMap(regionMap.empty() ? defaultDefaultRegionMap : regionMap)
+{
+ getIso8601Time(now, _dateTime, sizeof(_dateTime));
+}
diff --git a/plugins/s3_auth/aws_auth_v4.h b/plugins/s3_auth/aws_auth_v4.h
new file mode 100644
index 0000000..1959ddf
--- /dev/null
+++ b/plugins/s3_auth/aws_auth_v4.h
@@ -0,0 +1,207 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+/**
+ * @file aws_auth_v4.h
+ * @brief AWS Auth v4 signing utility.
+ * @see aws_auth_v4.cc
+ */
+
+#ifndef PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_
+#define PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_
+
+#include <algorithm> /* transform() */
+#include <cstddef> /* soze_t */
+#include <string> /* std::string */
+#include <sstream> /* std::stringstream */
+#include <map> /* std::map */
+#include <set> /* std::set */
+
+#include <ts/ts.h>
+
+typedef std::string String;
+typedef std::set<std::string> StringSet;
+typedef std::map<std::string, std::string> StringMap;
+
+class HeaderIterator;
+
+class TsInterface
+{
+public:
+ virtual ~TsInterface(){};
+ virtual const char *getMethod(int *length) = 0;
+ virtual const char *getHost(int *length) = 0;
+ virtual const char *getPath(int *length) = 0;
+ virtual const char *getQuery(int *length) = 0;
+ virtual HeaderIterator headerBegin() = 0;
+ virtual HeaderIterator headerEnd() = 0;
+};
+
+/* Define a header iterator to be used in the plugin using ATS API */
+class HeaderIterator
+{
+public:
+ HeaderIterator() : _bufp(nullptr), _hdrs(TS_NULL_MLOC), _field(TS_NULL_MLOC) {}
+ HeaderIterator(TSMBuffer bufp, TSMLoc hdrs, TSMLoc field) : _bufp(bufp), _hdrs(hdrs), _field(field) {}
+ HeaderIterator(const HeaderIterator &it)
+ {
+ _bufp = it._bufp;
+ _hdrs = it._hdrs;
+ _field = it._field;
+ }
+ ~HeaderIterator() {}
+ HeaderIterator &
+ operator=(HeaderIterator &it)
+ {
+ _bufp = it._bufp;
+ _hdrs = it._hdrs;
+ _field = it._field;
+ return *this;
+ }
+ HeaderIterator &operator++()
+ {
+ /* @todo this is said to be slow in the API call comments, do something better here */
+ TSMLoc next = TSMimeHdrFieldNext(_bufp, _hdrs, _field);
+ TSHandleMLocRelease(_bufp, _hdrs, _field);
+ _field = next;
+ return *this;
+ }
+ HeaderIterator operator++(int)
+ {
+ HeaderIterator tmp(*this);
+ operator++();
+ return tmp;
+ }
+ bool
+ operator!=(const HeaderIterator &it)
+ {
+ return _bufp != it._bufp || _hdrs != it._hdrs || _field != it._field;
+ }
+ bool
+ operator==(const HeaderIterator &it)
+ {
+ return _bufp == it._bufp && _hdrs == it._hdrs && _field == it._field;
+ }
+ const char *
+ getName(int *len)
+ {
+ return TSMimeHdrFieldNameGet(_bufp, _hdrs, _field, len);
+ }
+ const char *
+ getValue(int *len)
+ {
+ return TSMimeHdrFieldValueStringGet(_bufp, _hdrs, _field, -1, len);
+ }
+ TSMBuffer _bufp;
+ TSMLoc _hdrs;
+ TSMLoc _field;
+};
+
+/* Define a API to be used in the plugin using ATS API */
+class TsApi : public TsInterface
+{
+public:
+ TsApi(TSMBuffer bufp, TSMLoc hdrs, TSMLoc url) : _bufp(bufp), _hdrs(hdrs), _url(url) {}
+ ~TsApi() {}
+ const char *
+ getMethod(int *len)
+ {
+ return TSHttpHdrMethodGet(_bufp, _hdrs, len);
+ }
+ const char *
+ getHost(int *len)
+ {
+ return TSHttpHdrHostGet(_bufp, _hdrs, len);
+ }
+ const char *
+ getPath(int *len)
+ {
+ return TSUrlPathGet(_bufp, _url, len);
+ }
+ const char *
+ getQuery(int *len)
+ {
+ return TSUrlHttpQueryGet(_bufp, _url, len);
+ }
+ HeaderIterator
+ headerBegin()
+ {
+ return HeaderIterator(_bufp, _hdrs, TSMimeHdrFieldGet(_bufp, _hdrs, 0));
+ }
+ HeaderIterator
+ headerEnd()
+ {
+ return HeaderIterator(_bufp, _hdrs, TS_NULL_MLOC);
+ }
+ TSMBuffer _bufp;
+ TSMLoc _hdrs;
+ TSMLoc _url;
+};
+
+/* S3 auth v4 utility API */
+
+static const String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256";
+static const String X_AMX_DATE = "x-amz-date";
+static const String X_AMZ = "x-amz-";
+static const String CONTENT_TYPE = "content-type";
+static const String HOST = "host";
+
+String trimWhiteSpaces(const String &s);
+
+template <typename ContainerType>
+void
+commaSeparateString(ContainerType &ss, const String &input, bool trim = true, bool lowerCase = true)
+{
+ std::istringstream istr(input);
+ String token;
+
+ while (std::getline(istr, token, ',')) {
+ token = trim ? trimWhiteSpaces(token) : token;
+ if (lowerCase) {
+ std::transform(token.begin(), token.end(), token.begin(), ::tolower);
+ }
+ ss.insert(ss.end(), token);
+ }
+}
+
+class AwsAuthV4
+{
+public:
+ AwsAuthV4(TsInterface &api, time_t *now, bool signPayload, const char *awsAccessKeyId, size_t awsAccessKeyIdLen,
+ const char *awsSecretAccessKey, size_t awsSecretAccessKeyLen, const char *awsService, size_t awsServiceLen,
+ const StringSet &includedHeaders, const StringSet &excludedHeaders, const StringMap ®ionMap);
+ const char *getDateTime(size_t *dateTimeLen);
+ String getPayloadHash();
+ String getAuthorizationHeader();
+
+private:
+ TsInterface &_api;
+ char _dateTime[sizeof "20170428T010203Z"];
+ bool _signPayload = false;
+ const char *_awsAccessKeyId = nullptr;
+ size_t _awsAccessKeyIdLen = 0;
+ const char *_awsSecretAccessKey = nullptr;
+ size_t _awsSecretAccessKeyLen = 0;
+ const char *_awsService = nullptr;
+ size_t _awsServiceLen = 0;
+
+ const StringSet &_includedHeaders;
+ const StringSet &_excludedHeaders;
+ const StringMap &_regionMap;
+};
+#endif /* PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_ */
diff --git a/plugins/s3_auth/s3_auth.cc b/plugins/s3_auth/s3_auth.cc
index 81eb80a..9b0c5cd 100644
--- a/plugins/s3_auth/s3_auth.cc
+++ b/plugins/s3_auth/s3_auth.cc
@@ -29,6 +29,7 @@
#include <ctype.h>
#include <sys/time.h>
+#include <fstream> /* std::ifstream */
#include <string>
#include <unordered_map>
@@ -41,6 +42,7 @@
// Special snowflake here, only availbale when building inside the ATS source tree.
#include "ts/ink_atomic.h"
+#include "aws_auth_v4.h"
///////////////////////////////////////////////////////////////////////////////
// Some constants.
@@ -48,6 +50,91 @@
static const char PLUGIN_NAME[] = "s3_auth";
static const char DATE_FMT[] = "%a, %d %b %Y %H:%M:%S %z";
+/**
+ * @brief Rebase a relative path onto the configuration directory.
+ */
+static String
+makeConfigPath(const String &path)
+{
+ if (path.empty() || path[0] == '/') {
+ return path;
+ }
+
+ return String(TSConfigDirGet()) + "/" + path;
+}
+
+/**
+ * @brief a helper function which loads the entry-point to region from files.
+ * @param args classname + filename in '<classname>:<filename>' format.
+ * @return true if successful, false otherwise.
+ */
+static bool
+loadRegionMap(StringMap &m, const String &filename)
+{
+ static const char *EXPECTED_FORMAT = "<s3-entry-point>:<s3-region>";
+
+ String path(makeConfigPath(filename));
+
+ std::ifstream ifstr;
+ String line;
+ unsigned lineno = 0;
+
+ ifstr.open(path.c_str());
+ if (!ifstr) {
+ TSError("[%s] failed to load s3-region map from '%s'", PLUGIN_NAME, path.c_str());
+ return false;
+ }
+
+ TSDebug(PLUGIN_NAME, "loading region mapping from '%s'", path.c_str());
+
+ m[""] = ""; /* set a default just in case if the user does not specify it */
+
+ while (std::getline(ifstr, line)) {
+ String::size_type pos;
+
+ ++lineno;
+
+ // Allow #-prefixed comments.
+ pos = line.find_first_of('#');
+ if (pos != String::npos) {
+ line.resize(pos);
+ }
+
+ if (line.empty()) {
+ continue;
+ }
+
+ std::size_t d = line.find(':');
+ if (String::npos == d) {
+ TSError("[%s] failed to parse region map string '%s', expected format: '%s'", PLUGIN_NAME, line.c_str(), EXPECTED_FORMAT);
+ return false;
+ }
+
+ String entrypoint(trimWhiteSpaces(String(line, 0, d)));
+ String region(trimWhiteSpaces(String(line, d + 1, String::npos)));
+
+ if (region.empty()) {
+ TSDebug(PLUGIN_NAME, "<s3-region> in '%s' cannot be empty (skipped), expected format: '%s'", line.c_str(), EXPECTED_FORMAT);
+ continue;
+ }
+
+ if (entrypoint.empty()) {
+ TSDebug(PLUGIN_NAME, "added default region %s", region.c_str());
+ } else {
+ TSDebug(PLUGIN_NAME, "added entry-point:%s, region:%s", entrypoint.c_str(), region.c_str());
+ }
+
+ m[entrypoint] = region;
+ }
+
+ if (m.at("").empty()) {
+ TSDebug(PLUGIN_NAME, "default region was not defined");
+ }
+
+ ifstr.close();
+ return true;
+}
+
///////////////////////////////////////////////////////////////////////////////
// Cache for the secrets file, to avoid reading / loding them repeatedly on
// a reload of remap.config. This gets cached for 60s (not configurable).
@@ -96,7 +183,29 @@ public:
bool
valid() const
{
- return _secret && (_secret_len > 0) && _keyid && (_keyid_len > 0) && (2 == _version);
+ /* Check mandatory parameters first */
+ if (!_secret || !(_secret_len > 0) || !_keyid || !(_keyid_len > 0) || (2 != _version && 4 != _version)) {
+ return false;
+ }
+
+ /* Optional parameters, issue warning if v2 parameters are used with v4 and vice-versa (wrong parameters are ignored anyways) */
+ if (2 == _version) {
+ if (_v4includeHeaders_modified && !_v4includeHeaders.empty()) {
+ TSError("[%s] headers are not being signed with AWS auth v2, included headers parameter ignored", PLUGIN_NAME);
+ }
+ if (_v4excludeHeaders_modified && !_v4excludeHeaders.empty()) {
+ TSError("[%s] headers are not being signed with AWS auth v2, excluded headers parameter ignored", PLUGIN_NAME);
+ }
+ if (_region_map_modified && !_region_map.empty()) {
+ TSError("[%s] region map is not used with AWS auth v2, parameter ignored", PLUGIN_NAME);
+ }
+ } else {
+ /* 4 == _version */
+ if (_virt_host_modified) {
+ TSError("[%s] virtual host not used with AWS auth v4, parameter ignored", PLUGIN_NAME);
+ }
+ }
+ return true;
}
void
@@ -131,10 +240,28 @@ public:
}
if (src->_version_modified) {
- _version = src->_version;
+ _version = src->_version;
+ _version_modified = true;
}
+
if (src->_virt_host_modified) {
- _virt_host = src->_virt_host;
+ _virt_host = src->_virt_host;
+ _virt_host_modified = true;
+ }
+
+ if (src->_v4includeHeaders_modified) {
+ _v4includeHeaders = src->_v4includeHeaders;
+ _v4includeHeaders_modified = true;
+ }
+
+ if (src->_v4excludeHeaders_modified) {
+ _v4excludeHeaders = src->_v4excludeHeaders;
+ _v4excludeHeaders_modified = true;
+ }
+
+ if (src->_region_map_modified) {
+ _region_map = src->_region_map;
+ _region_map_modified = true;
}
}
@@ -169,6 +296,30 @@ public:
return _keyid_len;
}
+ int
+ version() const
+ {
+ return _version;
+ }
+
+ const StringSet &
+ v4includeHeaders()
+ {
+ return _v4includeHeaders;
+ }
+
+ const StringSet &
+ v4excludeHeaders()
+ {
+ return _v4excludeHeaders;
+ }
+
+ const StringMap &
+ v4RegionMap()
+ {
+ return _region_map;
+ }
+
// Setters
void
set_secret(const char *s)
@@ -197,6 +348,31 @@ public:
_version_modified = true;
}
+ void
+ set_include_headers(const char *s)
+ {
+ ::commaSeparateString<StringSet>(_v4includeHeaders, s);
+ _v4includeHeaders_modified = true;
+ }
+
+ void
+ set_exclude_headers(const char *s)
+ {
+ ::commaSeparateString<StringSet>(_v4excludeHeaders, s);
+ _v4excludeHeaders_modified = true;
+
+ /* Exclude headers that are meant to be changed */
+ _v4excludeHeaders.insert("x-forwarded-for");
+ _v4excludeHeaders.insert("via");
+ }
+
+ void
+ set_region_map(const char *s)
+ {
+ loadRegionMap(_region_map, s);
+ _region_map_modified = true;
+ }
+
// Parse configs from an external file
bool parse_config(const std::string &filename);
@@ -220,6 +396,12 @@ private:
bool _virt_host_modified = false;
TSCont _cont = nullptr;
volatile int _ref_count = 1;
+ StringSet _v4includeHeaders;
+ bool _v4includeHeaders_modified = false;
+ StringSet _v4excludeHeaders;
+ bool _v4excludeHeaders_modified = false;
+ StringMap _region_map;
+ bool _region_map_modified = false;
};
bool
@@ -268,6 +450,12 @@ S3Config::parse_config(const std::string &config_fname)
set_version(pos2 + 8);
} else if (0 == strncasecmp(pos2, "virtual_host", 12)) {
set_virt_host();
+ } else if (0 == strncasecmp(pos2, "v4-include-headers=", 19)) {
+ set_include_headers(pos2 + 19);
+ } else if (0 == strncasecmp(pos2, "v4-exclude-headers=", 19)) {
+ set_exclude_headers(pos2 + 19);
+ } else if (0 == strncasecmp(pos2, "v4-region-map=", 14)) {
+ set_region_map(pos2 + 14);
} else {
// ToDo: warnings?
}
@@ -289,17 +477,12 @@ S3Config::parse_config(const std::string &config_fname)
S3Config *
ConfigCache::get(const char *fname)
{
- std::string config_fname;
struct timeval tv;
gettimeofday(&tv, nullptr);
// Make sure the filename is an absolute path, prepending the config dir if needed
- if (*fname != '/') {
- config_fname = TSConfigDirGet();
- config_fname += "/";
- }
- config_fname += fname;
+ std::string config_fname = makeConfigPath(fname);
auto it = _cache.find(config_fname);
@@ -328,7 +511,7 @@ ConfigCache::get(const char *fname)
if (s3->parse_config(config_fname)) {
_cache[config_fname] = std::make_pair(s3, tv.tv_sec);
- TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s", config_fname.c_str());
+ TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s, version:%d", config_fname.c_str(), s3->version());
} else {
s3->release();
return nullptr;
@@ -367,6 +550,8 @@ public:
return true;
}
+ TSHttpStatus authorizeV2(S3Config *s3);
+ TSHttpStatus authorizeV4(S3Config *s3);
TSHttpStatus authorize(S3Config *s3);
bool set_header(const char *header, int header_len, const char *val, int val_len);
@@ -439,6 +624,55 @@ str_concat(char *dst, size_t dst_len, const char *src, size_t src_len)
return to_copy;
}
+TSHttpStatus
+S3Request::authorize(S3Config *s3)
+{
+ TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+ switch (s3->version()) {
+ case 2:
+ status = authorizeV2(s3);
+ break;
+ case 4:
+ status = authorizeV4(s3);
+ break;
+ default:
+ break;
+ }
+ return status;
+}
+
+TSHttpStatus
+S3Request::authorizeV4(S3Config *s3)
+{
+ TsApi api(_bufp, _hdr_loc, _url_loc);
+ time_t now = time(0);
+
+ AwsAuthV4 util(api, &now, /* signPayload */ false, s3->keyid(), s3->keyid_len(), s3->secret(), s3->secret_len(), "s3", 2,
+ s3->v4includeHeaders(), s3->v4excludeHeaders(), s3->v4RegionMap());
+ String payloadHash = util.getPayloadHash();
+ if (!set_header(X_AMZ_CONTENT_SHA256.c_str(), X_AMZ_CONTENT_SHA256.length(), payloadHash.c_str(), payloadHash.length())) {
+ return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+ }
+
+ /* set x-amz-date header */
+ size_t dateTimeLen = 0;
+ const char *dateTime = util.getDateTime(&dateTimeLen);
+ if (!set_header(X_AMX_DATE.c_str(), X_AMX_DATE.length(), dateTime, dateTimeLen)) {
+ return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+ }
+
+ String auth = util.getAuthorizationHeader();
+ if (auth.empty()) {
+ return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+ }
+
+ if (!set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth.c_str(), auth.length())) {
+ return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+ }
+
+ return TS_HTTP_STATUS_OK;
+}
+
// Method to authorize the S3 request:
//
// StringToSign = HTTP-VERB + "\n" +
@@ -457,7 +691,7 @@ str_concat(char *dst, size_t dst_len, const char *src, size_t src_len)
// Note: This assumes that the URI path has been appropriately canonicalized by remapping
//
TSHttpStatus
-S3Request::authorize(S3Config *s3)
+S3Request::authorizeV2(S3Config *s3)
{
TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
TSMLoc host_loc = TS_NULL_MLOC, md5_loc = TS_NULL_MLOC, contype_loc = TS_NULL_MLOC;
@@ -620,6 +854,7 @@ event_handler(TSCont cont, TSEvent event, void *edata)
{
TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
S3Config *s3 = static_cast<S3Config *>(TSContDataGet(cont));
+
S3Request request(txnp);
TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
TSEvent enable_event = TS_EVENT_HTTP_CONTINUE;
@@ -684,6 +919,9 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
{const_cast<char *>("secret_key"), required_argument, nullptr, 's'},
{const_cast<char *>("version"), required_argument, nullptr, 'v'},
{const_cast<char *>("virtual_host"), no_argument, nullptr, 'h'},
+ {const_cast<char *>("v4-include-headers"), required_argument, nullptr, 'i'},
+ {const_cast<char *>("v4-exclude-headers"), required_argument, nullptr, 'e'},
+ {const_cast<char *>("v4-region-map"), required_argument, nullptr, 'm'},
{nullptr, no_argument, nullptr, '\0'},
};
@@ -720,6 +958,15 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
case 'v':
s3->set_version(optarg);
break;
+ case 'i':
+ s3->set_include_headers(optarg);
+ break;
+ case 'e':
+ s3->set_exclude_headers(optarg);
+ break;
+ case 'm':
+ s3->set_region_map(optarg);
+ break;
}
if (opt == -1) {
@@ -742,8 +989,8 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
// Note that we don't acquire() the s3 config, it's implicit that we hold at least one ref
*ih = static_cast<void *>(s3);
- TSDebug(PLUGIN_NAME, "New rule: secret_key=%s, access_key=%s, virtual_host=%s", s3->secret(), s3->keyid(),
- s3->virt_host() ? "yes" : "no");
+ TSDebug(PLUGIN_NAME, "New rule: secret_key=%s, access_key=%s, virtual_host=%s, version=%d", s3->secret(), s3->keyid(),
+ s3->virt_host() ? "yes" : "no", s3->version());
return TS_SUCCESS;
}
--
To stop receiving notification emails like this one, please contact
['"commits@trafficserver.apache.org" <co...@trafficserver.apache.org>'].