plugins/origin_server_auth/origin_server_auth.cc (973 lines of code) (raw):

/** @file This is a simple URL signature generator for AWS S3 services. @section license License 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. */ #include <ctime> #include <cstring> #include <getopt.h> #include <ios> #include <sys/time.h> #include <cstdio> #include <cstdlib> #include <climits> #include <cctype> #include <fstream> /* std::ifstream */ #include <string> #include <unordered_map> #include <openssl/sha.h> #include <openssl/hmac.h> #include <chrono> #include <atomic> #include <thread> #include <mutex> #include <shared_mutex> #include <ts/ts.h> #include <ts/remap.h> #include <ts/remap_version.h> #include <tsutil/TsSharedMutex.h> #include "ts/apidefs.h" #include "tscore/ink_config.h" #include "swoc/TextView.h" #include "aws_auth_v4.h" /////////////////////////////////////////////////////////////////////////////// // Some constants. // static const char PLUGIN_NAME[] = "origin_server_auth"; static const char DATE_FMT[] = "%a, %d %b %Y %H:%M:%S %z"; static DbgCtl dbg_ctl{PLUGIN_NAME}; /** * @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; ifstr.open(path.c_str()); if (!ifstr) { TSError("[%s] failed to load s3-region map from '%s'", PLUGIN_NAME, path.c_str()); return false; } Dbg(dbg_ctl, "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; // 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()) { Dbg(dbg_ctl, "<s3-region> in '%s' cannot be empty (skipped), expected format: '%s'", line.c_str(), EXPECTED_FORMAT); continue; } if (entrypoint.empty()) { Dbg(dbg_ctl, "added default region %s", region.c_str()); } else { Dbg(dbg_ctl, "added entry-point:%s, region:%s", entrypoint.c_str(), region.c_str()); } m[entrypoint] = region; } if (m.at("").empty()) { Dbg(dbg_ctl, "default region was not defined"); } ifstr.close(); return true; } /////////////////////////////////////////////////////////////////////////////// // Cache for the secrets file, to avoid reading / loading them repeatedly on // a reload of remap.config. This gets cached for 60s (not configurable). // class S3Config; class ConfigCache { public: S3Config *get(const char *fname); private: struct _ConfigData { // This is incremented before and after cnf and load_time are set. // Thus, an odd value indicates an update is in progress. std::atomic<unsigned> update_status{0}; // A config from a file and the last time it was loaded. // config should be written before load_time. That way, // if config is read after load_time, the load time will // never indicate config is fresh when it isn't. std::atomic<S3Config *> config; std::atomic<time_t> load_time; _ConfigData() {} _ConfigData(S3Config *config_, time_t load_time_) : config(config_), load_time(load_time_) {} _ConfigData(_ConfigData &&lhs) { update_status = lhs.update_status.load(); config = lhs.config.load(); load_time = lhs.load_time.load(); } }; std::unordered_map<std::string, _ConfigData> _cache; static const int _ttl = 60; }; ConfigCache gConfCache; /////////////////////////////////////////////////////////////////////////////// // One configuration setup // int event_handler(TSCont, TSEvent, void *); // Forward declaration int config_reloader(TSCont, TSEvent, void *); class S3Config { public: S3Config(bool get_cont = true) { if (get_cont) { _cont = TSContCreate(event_handler, nullptr); TSContDataSet(_cont, static_cast<void *>(this)); _conf_rld = TSContCreate(config_reloader, TSMutexCreate()); TSContDataSet(_conf_rld, static_cast<void *>(this)); } } ~S3Config() { _secret_len = _keyid_len = _token_len = 0; TSfree(_secret); TSfree(_keyid); TSfree(_token); TSfree(_conf_fname); if (_conf_rld_act) { TSActionCancel(_conf_rld_act); } if (_conf_rld) { TSContDestroy(_conf_rld); } if (_cont) { TSContDestroy(_cont); } } // Is this configuration usable? bool valid() const { /* Check mandatory parameters first */ if (versions::gcpv1 == _version && (!_token || !(_token_len > 0))) { Dbg(dbg_ctl, "version = %s; keyid = %s", versionString(), _keyid); TSWarning("[%s] missing mandatory configs for version: %s %s", PLUGIN_NAME, versionString(), _conf_fname); return false; } else if ((versions::awsv2 == _version || versions::awsv4 == _version) && (!_secret || !(_secret_len > 0) || !_keyid || !(_keyid_len > 0))) { Dbg(dbg_ctl, "version = %s; keyid = %s; secret = %s", versionString(), _keyid, _secret); TSWarning("[%s] missing mandatory configs for version: %s %s", PLUGIN_NAME, versionString(), _conf_fname); return false; } /* Optional parameters, issue warning if v2 parameters are used with v4 and vice-versa (wrong parameters are ignored anyways) */ if (versions::awsv2 == _version) { if (_v4includeHeaders_modified && !_v4includeHeaders.empty()) { Dbg(dbg_ctl, "headers are not being signed with AWS auth v2, included headers parameter ignored"); } if (_v4excludeHeaders_modified && !_v4excludeHeaders.empty()) { Dbg(dbg_ctl, "headers are not being signed with AWS auth v2, excluded headers parameter ignored"); } if (_region_map_modified && !_region_map.empty()) { Dbg(dbg_ctl, "region map is not used with AWS auth v2, parameter ignored"); } if (nullptr != _token || _token_len > 0) { Dbg(dbg_ctl, "session token support with AWS auth v2 is not implemented, parameter ignored"); } } else { /* awsv4 == _version || gcpv1 == _version */ // NOTE: virtual host not used with AWS auth v4, parameter ignored } return true; } // Used to copy relevant configurations that can be configured in a config file. Note: we intentionally // don't override/use the assignment operator, since we only copy things IF they have been modified. void copy_changes_from(const S3Config *src) { if (src->_secret) { TSfree(_secret); _secret = TSstrdup(src->_secret); _secret_len = src->_secret_len; } if (src->_keyid) { TSfree(_keyid); _keyid = TSstrdup(src->_keyid); _keyid_len = src->_keyid_len; } if (src->_token) { TSfree(_token); _token = TSstrdup(src->_token); _token_len = src->_token_len; } if (src->_version_modified) { _version = src->_version; _version_modified = true; } if (src->_virt_host_modified) { _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; } _expiration = src->_expiration; if (src->_conf_fname) { TSfree(_conf_fname); _conf_fname = TSstrdup(src->_conf_fname); } } // Getters bool virt_host() const { return _virt_host; } const char * secret() const { return _secret; } const char * keyid() const { return _keyid; } const char * token() const { return _token; } int secret_len() const { return _secret_len; } int keyid_len() const { return _keyid_len; } int token_len() const { return _token_len; } int version() const { return _version; } const char * versionString() const { switch (_version) { case versions::awsv2: return "awsv2"; break; case versions::awsv4: return "awsv4"; break; case versions::gcpv1: return "gcpv1"; break; default: return "unknown"; break; } } const StringSet & v4includeHeaders() { return _v4includeHeaders; } const StringSet & v4excludeHeaders() { return _v4excludeHeaders; } const StringMap & v4RegionMap() { return _region_map; } long expiration() const { return _expiration; } const char * conf_fname() const { return _conf_fname; } int incr_conf_reload_count() { return _conf_reload_count++; } int incr_invalid_file_count() { return _invalid_file_count++; } // Setters void set_secret(const char *s) { TSfree(_secret); _secret = TSstrdup(s); _secret_len = strlen(s); } void set_keyid(const char *s) { TSfree(_keyid); _keyid = TSstrdup(s); _keyid_len = strlen(s); } void set_token(const char *s) { TSfree(_token); _token = TSstrdup(s); _token_len = strlen(s); } void set_virt_host(bool f = true) { _virt_host = f; _virt_host_modified = true; } void set_version(const char *s) { // Translate package and version to int // Supports old configuration if (strcmp("awsv2", s) == 0 || strcmp("2", s) == 0) { _version = versions::awsv2; } else if (strcmp("awsv4", s) == 0 || strcmp("4", s) == 0) { _version = versions::awsv4; } else if (strcmp("gcpv1", s) == 0) { _version = versions::gcpv1; } else { Dbg(dbg_ctl, "Invalid version"); } _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("forwarded"); _v4excludeHeaders.insert("via"); } void set_region_map(const char *s) { loadRegionMap(_region_map, s); _region_map_modified = true; } void set_expiration(const char *s) { _expiration = strtol(s, nullptr, 10); } void set_conf_fname(const char *s) { TSfree(_conf_fname); _conf_fname = TSstrdup(s); } void reset_conf_reload_count() { _conf_reload_count = 0; } void reset_invalid_file_count() { _invalid_file_count = 0; } // Parse configs from an external file bool parse_config(const std::string &filename); // This should be called from the remap plugin, to setup the TXN hook for // SEND_REQUEST_HDR, such that we always attach the appropriate S3 auth. void schedule(TSHttpTxn txnp) const { TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, _cont); } void schedule_conf_reload(long delay) { if (_conf_rld_act != nullptr && !TSActionDone(_conf_rld_act)) { TSActionCancel(_conf_rld_act); } _conf_rld_act = TSContScheduleOnPool(_conf_rld, delay * 1000, TS_THREAD_POOL_TASK); } /** Clear _conf_rld_act if the event handler is handling the action */ void check_current_action(void *edata) { // Following what's TSContScheduleOnPool does before returning TSAction if (_conf_rld_act == ((TSAction)((uintptr_t)edata | 0x1))) { _conf_rld_act = nullptr; } } TSMutex config_reloader_mutex() { return TSContMutexGet(_conf_rld); } ts::shared_mutex reload_mutex; enum versions { awsv2 = 0, awsv4 = 1, gcpv1 = 2 }; private: char *_secret = nullptr; size_t _secret_len = 0; char *_keyid = nullptr; size_t _keyid_len = 0; char *_token = nullptr; size_t _token_len = 0; bool _virt_host = false; versions _version = versions::awsv2; bool _version_modified = false; bool _virt_host_modified = false; TSCont _cont = nullptr; TSCont _conf_rld = nullptr; TSAction _conf_rld_act = nullptr; StringSet _v4includeHeaders; bool _v4includeHeaders_modified = false; StringSet _v4excludeHeaders; bool _v4excludeHeaders_modified = false; StringMap _region_map; bool _region_map_modified = false; long _expiration = 0; char *_conf_fname = nullptr; int _conf_reload_count = 0; int _invalid_file_count = 0; }; bool S3Config::parse_config(const std::string &config_fname) { if (0 == config_fname.size()) { TSError("[%s] called without a config file, this is broken", PLUGIN_NAME); return false; } else { std::ifstream file; file.open(config_fname, std::ios_base::in); if (!file.is_open()) { TSError("[%s] unable to open %s", PLUGIN_NAME, config_fname.c_str()); return false; } for (std::string buf; std::getline(file, buf);) { swoc::TextView line{buf}; // Skip leading/trailing white spaces swoc::TextView key_val = line.trim_if(&isspace); // Skip empty or comment lines if (key_val.empty() || ('#' == key_val[0])) { continue; } // Identify the keys (and values if appropriate) std::string key_str{key_val.take_prefix_at('=').trim_if(&isspace)}; std::string val_str{key_val.trim_if(&isspace)}; if (key_str == "secret_key") { set_secret(val_str.c_str()); } else if (key_str == "access_key") { set_keyid(val_str.c_str()); } else if (key_str == "session_token") { set_token(val_str.c_str()); } else if (key_str == "version") { set_version(val_str.c_str()); } else if (key_str == "virtual_host") { set_virt_host(); } else if (key_str == "v4-include-headers") { set_include_headers(val_str.c_str()); } else if (key_str == "v4-exclude-headers") { set_exclude_headers(val_str.c_str()); } else if (key_str == "v4-region-map") { set_region_map(val_str.c_str()); } else if (key_str == "expiration") { set_expiration(val_str.c_str()); } else { TSWarning("[%s] unknown config key: %s %s", PLUGIN_NAME, key_str.c_str(), config_fname.c_str()); } } } return true; } /////////////////////////////////////////////////////////////////////////////// // Implementation for the ConfigCache, it has to go here since we have a sort // of circular dependency. Note that we always parse / get the configuration // for the file, either from cache or by making one. The user of this just // has to copy the relevant portions, but should not use the returned object // directly (i.e. it must be copied). // S3Config * ConfigCache::get(const char *fname) { S3Config *s3; struct timeval tv; gettimeofday(&tv, nullptr); // Make sure the filename is an absolute path, prepending the config dir if needed std::string config_fname = makeConfigPath(fname); auto it = _cache.find(config_fname); if (it != _cache.end()) { unsigned update_status = it->second.update_status; if (tv.tv_sec > (it->second.load_time + _ttl)) { if (!(update_status & 1) && it->second.update_status.compare_exchange_strong(update_status, update_status + 1)) { Dbg(dbg_ctl, "Configuration from %s is stale, reloading", config_fname.c_str()); s3 = new S3Config(false); // false == this config does not get the continuation if (s3->parse_config(config_fname)) { s3->set_conf_fname(fname); } else { // Failed the configuration parse... Set the cache response to nullptr delete s3; s3 = nullptr; TSAssert(!"Configuration parsing / caching failed"); } delete it->second.config; it->second.config = s3; it->second.load_time = tv.tv_sec; // Update is complete. ++it->second.update_status; } else { // This thread lost the race with another thread that is also reloading // the config for this file. Wait for the other thread to finish reloading. while (it->second.update_status & 1) { // Hopefully yielding will sleep the thread at least until the next // scheduler interrupt, preventing a busy wait. std::this_thread::yield(); } s3 = it->second.config; } } else { Dbg(dbg_ctl, "Configuration from %s is fresh, reusing", config_fname.c_str()); s3 = it->second.config; } } else { // Create a new cached file. s3 = new S3Config(false); // false == this config does not get the continuation Dbg(dbg_ctl, "Parsing and caching configuration from %s, version:%s", config_fname.c_str(), s3->versionString()); if (s3->parse_config(config_fname)) { s3->set_conf_fname(fname); _cache.emplace(config_fname, _ConfigData(s3, tv.tv_sec)); } else { delete s3; s3 = nullptr; TSAssert(!"Configuration parsing / caching failed"); } } return s3; } /////////////////////////////////////////////////////////////////////////////// // This class is used to perform the S3 auth generation. // class S3Request { public: S3Request(TSHttpTxn txnp) : _txnp(txnp), _bufp(nullptr), _hdr_loc(TS_NULL_MLOC), _url_loc(TS_NULL_MLOC) {} ~S3Request() { TSHandleMLocRelease(_bufp, _hdr_loc, _url_loc); TSHandleMLocRelease(_bufp, TS_NULL_MLOC, _hdr_loc); } bool initialize() { if (TS_SUCCESS != TSHttpTxnServerReqGet(_txnp, &_bufp, &_hdr_loc)) { return false; } if (TS_SUCCESS != TSHttpHdrUrlGet(_bufp, _hdr_loc, &_url_loc)) { return false; } return true; } TSHttpStatus authorizeAwsV2(S3Config *s3); TSHttpStatus authorizeAwsV4(S3Config *s3); TSHttpStatus authorizeGcp(S3Config *s3); TSHttpStatus authorize(S3Config *s3); bool set_header(const char *header, int header_len, const char *val, int val_len); private: TSHttpTxn _txnp; TSMBuffer _bufp; TSMLoc _hdr_loc, _url_loc; }; /////////////////////////////////////////////////////////////////////////// // Set a header to a specific value. This will avoid going to through a // remove / add sequence in case of an existing header. // but clean. bool S3Request::set_header(const char *header, int header_len, const char *val, int val_len) { if (!header || header_len <= 0 || !val || val_len <= 0) { return false; } bool ret = false; TSMLoc field_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, header, header_len); if (!field_loc) { // No existing header, so create one if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(_bufp, _hdr_loc, header, header_len, &field_loc)) { if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(_bufp, _hdr_loc, field_loc, -1, val, val_len)) { TSMimeHdrFieldAppend(_bufp, _hdr_loc, field_loc); ret = true; } TSHandleMLocRelease(_bufp, _hdr_loc, field_loc); } } else { TSMLoc tmp = nullptr; bool first = true; while (field_loc) { tmp = TSMimeHdrFieldNextDup(_bufp, _hdr_loc, field_loc); if (first) { first = false; if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(_bufp, _hdr_loc, field_loc, -1, val, val_len)) { ret = true; } } else { TSMimeHdrFieldDestroy(_bufp, _hdr_loc, field_loc); } TSHandleMLocRelease(_bufp, _hdr_loc, field_loc); field_loc = tmp; } } if (ret) { Dbg(dbg_ctl, "Set the header %.*s: %.*s", header_len, header, val_len, val); } return ret; } // dst points to starting offset of dst buffer // dst_len remaining space in buffer static size_t str_concat(char *dst, size_t dst_len, const char *src, size_t src_len) { size_t to_copy = std::min(dst_len, src_len); if (to_copy > 0) { TSstrlcat(dst, src, to_copy); } return to_copy; } TSHttpStatus S3Request::authorize(S3Config *s3) { TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; switch (s3->version()) { case S3Config::versions::awsv2: status = authorizeAwsV2(s3); break; case S3Config::versions::awsv4: status = authorizeAwsV4(s3); break; case S3Config::versions::gcpv1: status = authorizeGcp(s3); break; default: break; } return status; } TSHttpStatus S3Request::authorizeAwsV4(S3Config *s3) { TsApi api(_bufp, _hdr_loc, _url_loc); time_t now = time(nullptr); 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; } /* set X-Amz-Security-Token if we have a token */ if (nullptr != s3->token() && '\0' != *(s3->token()) && !set_header(X_AMZ_SECURITY_TOKEN.data(), X_AMZ_SECURITY_TOKEN.size(), s3->token(), s3->token_len())) { 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" + // Content-MD5 + "\n" + // Content-Type + "\n" + // Date + "\n" + // CanonicalizedAmzHeaders + // CanonicalizedResource; // // ToDo: // ----- // 1) UTF8 // 2) Support POST type requests // 3) Canonicalize the Amz headers // // Note: This assumes that the URI path has been appropriately canonicalized by remapping // TSHttpStatus S3Request::authorizeAwsV2(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; int method_len = 0, path_len = 0, host_len = 0, con_md5_len = 0, con_type_len = 0, date_len = 0; const char *method = nullptr, *path = nullptr, *host = nullptr, *con_md5 = nullptr, *con_type = nullptr, *host_endp = nullptr; char date[128]; // Plenty of space for a Date value time_t now = time(nullptr); struct tm now_tm; // Start with some request resources we need if (nullptr == (method = TSHttpHdrMethodGet(_bufp, _hdr_loc, &method_len))) { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } if (nullptr == (path = TSUrlPathGet(_bufp, _url_loc, &path_len))) { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } // Next, setup the Date: header, it's required. if (nullptr == gmtime_r(&now, &now_tm)) { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } if ((date_len = strftime(date, sizeof(date) - 1, DATE_FMT, &now_tm)) <= 0) { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } // Add the Date: header to the request (this overwrites any existing Date header) set_header(TS_MIME_FIELD_DATE, TS_MIME_LEN_DATE, date, date_len); // If the configuration is a "virtual host" (foo.s3.aws ...), extract the // first portion into the Host: header. if (s3->virt_host()) { host_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_HOST, TS_MIME_LEN_HOST); if (host_loc) { host = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, host_loc, -1, &host_len); host_endp = static_cast<const char *>(memchr(host, '.', host_len)); } else { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } } // Just in case we add Content-MD5 if present md5_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_CONTENT_MD5, TS_MIME_LEN_CONTENT_MD5); if (md5_loc) { con_md5 = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, md5_loc, -1, &con_md5_len); } // get the Content-Type if available - (buggy) clients may send it // for GET requests too contype_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE); if (contype_loc) { con_type = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, contype_loc, -1, &con_type_len); } // For debugging, lets produce some nice output if (dbg_ctl.on()) { Dbg(dbg_ctl, "Signature string is:"); // ToDo: This should include the Content-MD5 and Content-Type (for POST) Dbg(dbg_ctl, "%.*s", method_len, method); if (con_md5) { Dbg(dbg_ctl, "%.*s", con_md5_len, con_md5); } if (con_type) { Dbg(dbg_ctl, "%.*s", con_type_len, con_type); } Dbg(dbg_ctl, "%.*s", date_len, date); const size_t left_size = 1024; char left[left_size + 1] = "/"; size_t loff = 1; // ToDo: What to do with the CanonicalizedAmzHeaders ... if (host && host_endp) { loff += str_concat(&left[loff], (left_size - loff), host, static_cast<int>(host_endp - host)); loff += str_concat(&left[loff], (left_size - loff), "/", 1); } str_concat(&left[loff], (left_size - loff), path, path_len); Dbg(dbg_ctl, "%s", left); } // Produce the SHA1 MAC digest HMAC_CTX *ctx; unsigned int hmac_len; size_t hmac_b64_len; unsigned char hmac[SHA_DIGEST_LENGTH]; char hmac_b64[SHA_DIGEST_LENGTH * 2]; ctx = HMAC_CTX_new(); HMAC_Init_ex(ctx, s3->secret(), s3->secret_len(), EVP_sha1(), nullptr); HMAC_Update(ctx, (unsigned char *)method, method_len); HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1); HMAC_Update(ctx, (unsigned char *)con_md5, con_md5_len); HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1); HMAC_Update(ctx, (unsigned char *)con_type, con_type_len); HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1); HMAC_Update(ctx, reinterpret_cast<unsigned char *>(date), date_len); HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n/"), 2); if (host && host_endp) { HMAC_Update(ctx, (unsigned char *)host, host_endp - host); HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("/"), 1); } HMAC_Update(ctx, (unsigned char *)path, path_len); HMAC_Final(ctx, hmac, &hmac_len); HMAC_CTX_free(ctx); // Do the Base64 encoding and set the Authorization header. if (TS_SUCCESS == TSBase64Encode(reinterpret_cast<const char *>(hmac), hmac_len, hmac_b64, sizeof(hmac_b64) - 1, &hmac_b64_len)) { char auth[256]; // This is way bigger than any string we can think of. int auth_len = snprintf(auth, sizeof(auth), "AWS %s:%.*s", s3->keyid(), static_cast<int>(hmac_b64_len), hmac_b64); if ((auth_len > 0) && (auth_len < static_cast<int>(sizeof(auth)))) { set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth, auth_len); status = TS_HTTP_STATUS_OK; } } // Cleanup TSHandleMLocRelease(_bufp, _hdr_loc, contype_loc); TSHandleMLocRelease(_bufp, _hdr_loc, md5_loc); TSHandleMLocRelease(_bufp, _hdr_loc, host_loc); return status; } TSHttpStatus S3Request::authorizeGcp(S3Config *s3) { char auth[2048]; int auth_len = snprintf(auth, sizeof(auth), "Bearer %s", s3->token()); if (!set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth, auth_len)) { return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; } return TS_HTTP_STATUS_OK; } /////////////////////////////////////////////////////////////////////////////// // This is the main continuation. int event_handler(TSCont cont, TSEvent event, void *edata) { TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); S3Config *s3 = static_cast<S3Config *>(TSContDataGet(cont)); TSEvent enable_event = TS_EVENT_HTTP_CONTINUE; { S3Request request(txnp); TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; switch (event) { case TS_EVENT_HTTP_SEND_REQUEST_HDR: if (request.initialize()) { std::shared_lock lock(s3->reload_mutex); status = request.authorize(s3); } if (TS_HTTP_STATUS_OK == status) { Dbg(dbg_ctl, "Successfully signed the URL"); } else { Dbg(dbg_ctl, "Failed to sign the URL, status = %d", status); TSHttpTxnStatusSet(txnp, status); enable_event = TS_EVENT_HTTP_ERROR; } break; default: TSError("[%s] Unknown event for this plugin", PLUGIN_NAME); Dbg(dbg_ctl, "unknown event for this plugin"); break; } // Most get S3Request out of scope in case the later plugins invalidate the TSAPI // objects it references. Some cases were causing asserts from the destructor } TSHttpTxnReenable(txnp, enable_event); return 0; } // If the token has more than one hour to expire, reload is scheduled one hour before expiration. // If the token has less than one hour to expire, reload is scheduled 15 minutes before expiration. // If the token has less than 15 minutes to expire, reload is scheduled at the expiration time. static long cal_reload_delay(long time_diff) { if (time_diff > 3600) { return time_diff - 3600; } else if (time_diff > 900) { return time_diff - 900; } else { return time_diff; } } int config_reloader(TSCont cont, TSEvent /* event ATS_UNUSED */, void *edata) { Dbg(dbg_ctl, "reloading configs"); S3Config *s3 = static_cast<S3Config *>(TSContDataGet(cont)); s3->check_current_action(edata); S3Config *file_config = gConfCache.get(s3->conf_fname()); std::string config_fname = makeConfigPath(s3->conf_fname() == nullptr ? "" : s3->conf_fname()); if (!file_config || !file_config->valid()) { TSError("[%s] invalid configuration for version: %s %s. Check mandatory fields. Scheduling reload", PLUGIN_NAME, s3->versionString(), config_fname.c_str()); long delay = 1 << s3->incr_invalid_file_count(); s3->schedule_conf_reload(delay); return TS_ERROR; } s3->reset_invalid_file_count(); { std::unique_lock lock(s3->reload_mutex); s3->copy_changes_from(file_config); } if (s3->expiration() == 0) { Dbg(dbg_ctl, "disabling auto config reload for version: %s %s", s3->versionString(), config_fname.c_str()); } else { // auto reload is scheduled to be 5 minutes before the expiration time to get some headroom long time_diff = s3->expiration() - std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count(); if (time_diff > 0) { long delay = cal_reload_delay(time_diff); TSNote("scheduling config reload with %ld seconds delay for version: %s %s", delay, s3->versionString(), config_fname.c_str()); s3->reset_conf_reload_count(); s3->schedule_conf_reload(delay); } else { Dbg(dbg_ctl, "config expiration time for version: %s %s is in the past, re-checking in 1 minute", s3->versionString(), config_fname.c_str()); if (s3->incr_conf_reload_count() % 10 == 0) { TSError("[%s] tried to reload config automatically but failed, please try manual reloading the config file: %s", PLUGIN_NAME, config_fname.c_str()); } s3->schedule_conf_reload(60); } } return TS_SUCCESS; } /////////////////////////////////////////////////////////////////////////////// // Initialize the plugin. // TSReturnCode TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) { CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size); Dbg(dbg_ctl, "plugin is successfully initialized"); return TS_SUCCESS; } /////////////////////////////////////////////////////////////////////////////// // One instance per remap.config invocation. // TSReturnCode TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */) { static const struct option longopt[] = { {const_cast<char *>("access_key"), required_argument, nullptr, 'a' }, {const_cast<char *>("config"), required_argument, nullptr, 'c' }, {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' }, {const_cast<char *>("session_token"), required_argument, nullptr, 't' }, {nullptr, no_argument, nullptr, '\0'}, }; S3Config *s3 = new S3Config(true); // true == this config gets the continuation S3Config *file_config = nullptr; // argv contains the "to" and "from" URLs. Skip the first so that the // second one poses as the program name. --argc; ++argv; while (true) { int opt = getopt_long(argc, static_cast<char *const *>(argv), "", longopt, nullptr); switch (opt) { case 'c': file_config = gConfCache.get(optarg); // Get cached, or new, config object, from a file if (!file_config) { TSError("[%s] invalid configuration file, %s", PLUGIN_NAME, optarg); *ih = nullptr; return TS_ERROR; } break; case 'a': s3->set_keyid(optarg); break; case 's': s3->set_secret(optarg); break; case 't': s3->set_token(optarg); break; case 'h': s3->set_virt_host(); break; 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) { break; } } // Copy the config file secret into our instance of the configuration. if (file_config) { s3->copy_changes_from(file_config); } std::string config_fname = makeConfigPath(s3->conf_fname() == nullptr ? "" : s3->conf_fname()); // Make sure the configuration is valid if (!s3->valid()) { TSError("[%s] invalid configuration %s. Check mandatory fields.", PLUGIN_NAME, config_fname.c_str()); *ih = nullptr; return TS_ERROR; } if (s3->expiration() == 0) { Dbg(dbg_ctl, "disabling auto config reload for version: %s %s", s3->versionString(), config_fname.c_str()); } else { // auto reload is scheduled to be 5 minutes before the expiration time to get some headroom long time_diff = s3->expiration() - std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count(); if (time_diff > 0) { long delay = cal_reload_delay(time_diff); TSNote("[%s] scheduling config reload with %ld seconds delay for version: %s %s", PLUGIN_NAME, delay, s3->versionString(), config_fname.c_str()); s3->reset_conf_reload_count(); s3->schedule_conf_reload(delay); } else { Dbg(dbg_ctl, "config expiration time for version %s %s is in the past, re-checking in 1 minute", s3->versionString(), config_fname.c_str()); s3->schedule_conf_reload(60); } } *ih = static_cast<void *>(s3); Dbg(dbg_ctl, "New rule: access_key=%s, virtual_host=%s, version=%s", s3->keyid(), s3->virt_host() ? "yes" : "no", s3->versionString()); return TS_SUCCESS; } void TSRemapDeleteInstance(void *ih) { S3Config *s3 = static_cast<S3Config *>(ih); TSMutex s3_mutex_ptr = s3->config_reloader_mutex(); TSMutexLock(s3_mutex_ptr); delete s3; TSMutexUnlock(s3_mutex_ptr); } /////////////////////////////////////////////////////////////////////////////// // This is the main "entry" point for the plugin, called for every request. // TSRemapStatus TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */) { S3Config *s3 = static_cast<S3Config *>(ih); if (s3) { TSAssert(s3->valid()); // Now schedule the continuation to update the URL when going to origin. // Note that in most cases, this is a No-Op, assuming you have reasonable // cache hit ratio. However, the scheduling is next to free (very cheap). // Another option would be to use a single global hook, and pass the "s3" // configs via a TXN argument. s3->schedule(txnp); } else { Dbg(dbg_ctl, "Remap context is invalid"); TSError("[%s] No remap context available, check code / config", PLUGIN_NAME); TSHttpTxnStatusSet(txnp, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR); } // This plugin actually doesn't do anything with remapping. Ever. return TSREMAP_NO_REMAP; }