This is an automated email from the ASF dual-hosted git repository. gancho pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push: new fa73136 Issue #1605 AWS Signature Version 4 fa73136 is described below commit fa731369b474cb3c46b7b8ddf98490c198cd4605 Author: Gancho Tenev <gtte...@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 --- 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 d00a87b..8213ad6 100644 --- a/plugins/s3_auth/s3_auth.cc +++ b/plugins/s3_auth/s3_auth.cc @@ -30,6 +30,7 @@ #include <climits> #include <cctype> +#include <fstream> /* std::ifstream */ #include <string> #include <unordered_map> @@ -42,6 +43,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. @@ -49,6 +51,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). @@ -97,7 +184,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 @@ -132,10 +241,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; } } @@ -170,6 +297,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) @@ -198,6 +349,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); @@ -221,6 +397,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 @@ -269,6 +451,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? } @@ -290,17 +478,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); @@ -329,7 +512,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; @@ -368,6 +551,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); @@ -440,6 +625,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" + @@ -458,7 +692,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; @@ -621,6 +855,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; @@ -685,6 +920,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'}, }; @@ -721,6 +959,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) { @@ -743,8 +990,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" <commits@trafficserver.apache.org>'].