blob: 5b381ab395fb31329488c0fe4b2c2bae1f383128 [file] [log] [blame]
/** @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 <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 "tscore/ink_config.h"
#include "aws_auth_v4.h"
///////////////////////////////////////////////////////////////////////////////
// Some constants.
//
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;
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;
// 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 / 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 (!_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()) {
TSDebug("[%s] headers are not being signed with AWS auth v2, included headers parameter ignored", PLUGIN_NAME);
}
if (_v4excludeHeaders_modified && !_v4excludeHeaders.empty()) {
TSDebug("[%s] headers are not being signed with AWS auth v2, excluded headers parameter ignored", PLUGIN_NAME);
}
if (_region_map_modified && !_region_map.empty()) {
TSDebug("[%s] region map is not used with AWS auth v2, parameter ignored", PLUGIN_NAME);
}
if (nullptr != _token || _token_len > 0) {
TSDebug("[%s] session token support with AWS auth v2 is not implemented, parameter ignored", PLUGIN_NAME);
}
} else {
/* 4 == _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 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++;
}
// 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)
{
_version = strtol(s, nullptr, 10);
_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;
}
// 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_NET);
}
std::shared_mutex reload_mutex;
std::atomic_bool reload_waiting = false;
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;
int _version = 2;
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;
};
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 {
char line[512]; // These are long lines ...
FILE *file = fopen(config_fname.c_str(), "r");
if (nullptr == file) {
TSError("[%s] unable to open %s", PLUGIN_NAME, config_fname.c_str());
return false;
}
while (fgets(line, sizeof(line), file) != nullptr) {
char *pos1, *pos2;
// Skip leading white spaces
pos1 = line;
while (*pos1 && isspace(*pos1)) {
++pos1;
}
if (!*pos1 || ('#' == *pos1)) {
continue;
}
// Skip trailing white spaces
pos2 = pos1;
pos1 = pos2 + strlen(pos2) - 1;
while ((pos1 > pos2) && isspace(*pos1)) {
*(pos1--) = '\0';
}
if (pos1 == pos2) {
continue;
}
// Identify the keys (and values if appropriate)
if (0 == strncasecmp(pos2, "secret_key=", 11)) {
set_secret(pos2 + 11);
} else if (0 == strncasecmp(pos2, "access_key=", 11)) {
set_keyid(pos2 + 11);
} else if (0 == strncasecmp(pos2, "session_token=", 14)) {
set_token(pos2 + 14);
} else if (0 == strncasecmp(pos2, "version=", 8)) {
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 if (0 == strncasecmp(pos2, "expiration=", 11)) {
set_expiration(pos2 + 11);
} else {
// ToDo: warnings?
}
}
fclose(file);
}
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)) {
TSDebug(PLUGIN_NAME, "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 {
TSDebug(PLUGIN_NAME, "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
TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s, version:%d", config_fname.c_str(), s3->version());
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 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);
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) {
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);
}
tmp = TSMimeHdrFieldNextDup(_bufp, _hdr_loc, field_loc);
TSHandleMLocRelease(_bufp, _hdr_loc, field_loc);
field_loc = tmp;
}
}
if (ret) {
TSDebug(PLUGIN_NAME, "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) {
strncat(dst, src, to_copy);
}
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(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::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;
int method_len = 0, path_len = 0, param_len = 0, host_len = 0, con_md5_len = 0, con_type_len = 0, date_len = 0;
const char *method = nullptr, *path = nullptr, *param = 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;
}
// get matrix parameters
param = TSUrlHttpParamsGet(_bufp, _url_loc, &param_len);
// 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 (TSIsDebugTagSet(PLUGIN_NAME)) {
TSDebug(PLUGIN_NAME, "Signature string is:");
// ToDo: This should include the Content-MD5 and Content-Type (for POST)
TSDebug(PLUGIN_NAME, "%.*s", method_len, method);
if (con_md5) {
TSDebug(PLUGIN_NAME, "%.*s", con_md5_len, con_md5);
}
if (con_type) {
TSDebug(PLUGIN_NAME, "%.*s", con_type_len, con_type);
}
TSDebug(PLUGIN_NAME, "%.*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);
}
loff += str_concat(&left[loff], (left_size - loff), path, path_len);
if (param) {
loff += str_concat(&left[loff], (left_size - loff), ";", 1);
str_concat(&left[loff], (left_size - loff), param, param_len);
}
TSDebug(PLUGIN_NAME, "%s", left);
}
// Produce the SHA1 MAC digest
#ifndef HAVE_HMAC_CTX_NEW
HMAC_CTX ctx[1];
#else
HMAC_CTX *ctx;
#endif
unsigned int hmac_len;
size_t hmac_b64_len;
unsigned char hmac[SHA_DIGEST_LENGTH];
char hmac_b64[SHA_DIGEST_LENGTH * 2];
#ifndef HAVE_HMAC_CTX_NEW
HMAC_CTX_init(ctx);
#else
ctx = HMAC_CTX_new();
#endif
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);
if (param) {
HMAC_Update(ctx, reinterpret_cast<const unsigned char *>(";"), 1); // TSUrlHttpParamsGet() does not include ';'
HMAC_Update(ctx, (unsigned char *)param, param_len);
}
HMAC_Final(ctx, hmac, &hmac_len);
#ifndef HAVE_HMAC_CTX_NEW
HMAC_CTX_cleanup(ctx);
#else
HMAC_CTX_free(ctx);
#endif
// 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;
}
///////////////////////////////////////////////////////////////////////////////
// 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));
S3Request request(txnp);
TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
TSEvent enable_event = TS_EVENT_HTTP_CONTINUE;
switch (event) {
case TS_EVENT_HTTP_SEND_REQUEST_HDR:
if (request.initialize()) {
while (s3->reload_waiting) {
std::this_thread::yield();
}
std::shared_lock lock(s3->reload_mutex);
status = request.authorize(s3);
}
if (TS_HTTP_STATUS_OK == status) {
TSDebug(PLUGIN_NAME, "Successfully signed the AWS S3 URL");
} else {
TSDebug(PLUGIN_NAME, "Failed to sign the AWS S3 URL, status = %d", status);
TSHttpTxnStatusSet(txnp, status);
enable_event = TS_EVENT_HTTP_ERROR;
}
break;
default:
TSError("[%s] Unknown event for this plugin", PLUGIN_NAME);
TSDebug(PLUGIN_NAME, "unknown event for this plugin");
break;
}
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, void *edata)
{
TSDebug(PLUGIN_NAME, "reloading configs");
S3Config *s3 = static_cast<S3Config *>(TSContDataGet(cont));
S3Config *file_config = gConfCache.get(s3->conf_fname());
if (!file_config || !file_config->valid()) {
TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME);
return TS_ERROR;
}
s3->reload_waiting = true;
{
std::unique_lock lock(s3->reload_mutex);
s3->copy_changes_from(file_config);
}
s3->reload_waiting = false;
if (s3->expiration() == 0) {
TSDebug(PLUGIN_NAME, "disabling auto config reload");
} 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);
TSDebug(PLUGIN_NAME, "scheduling config reload with %ld seconds delay", delay);
s3->reset_conf_reload_count();
s3->schedule_conf_reload(delay);
} else {
TSDebug(PLUGIN_NAME, "config expiration time is in the past, re-checking in 1 minute");
if (s3->incr_conf_reload_count() == 10) {
TSError("[%s] tried to reload config automatically but failed, please try manual reloading the config", PLUGIN_NAME);
}
s3->schedule_conf_reload(60);
}
}
return TS_SUCCESS;
}
///////////////////////////////////////////////////////////////////////////////
// Initialize the plugin.
//
TSReturnCode
TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
if (!api_info) {
strncpy(errbuf, "[tsremap_init] - Invalid TSRemapInterface argument", errbuf_size - 1);
return TS_ERROR;
}
if (api_info->tsremap_version < TSREMAP_VERSION) {
snprintf(errbuf, errbuf_size, "[TSRemapInit] - Incorrect API version %ld.%ld", api_info->tsremap_version >> 16,
(api_info->tsremap_version & 0xffff));
return TS_ERROR;
}
TSDebug(PLUGIN_NAME, "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);
}
// Make sure we got both the shared secret and the AWS secret
if (!s3->valid()) {
TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME);
*ih = nullptr;
return TS_ERROR;
}
if (s3->expiration() == 0) {
TSDebug(PLUGIN_NAME, "disabling auto config reload");
} 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);
TSDebug(PLUGIN_NAME, "scheduling config reload with %ld seconds delay", delay);
s3->reset_conf_reload_count();
s3->schedule_conf_reload(delay);
} else {
TSDebug(PLUGIN_NAME, "config expiration time is in the past, re-checking in 1 minute");
s3->schedule_conf_reload(60);
}
}
*ih = static_cast<void *>(s3);
TSDebug(PLUGIN_NAME, "New rule: access_key=%s, virtual_host=%s, version=%d", s3->keyid(), s3->virt_host() ? "yes" : "no",
s3->version());
return TS_SUCCESS;
}
void
TSRemapDeleteInstance(void *ih)
{
S3Config *s3 = static_cast<S3Config *>(ih);
delete s3;
}
///////////////////////////////////////////////////////////////////////////////
// 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 {
TSDebug(PLUGIN_NAME, "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;
}