blob: 56c6ad8f805399599906b358ee9b70170eb766a5 [file]
/** @file
This plugin allows retrying requests against different destinations.
@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 "ts/apidefs.h"
#include <ts/ts.h>
#include <ts/remap.h>
#include <cstdlib>
#include <cstdio>
#include <getopt.h>
#include <cstring>
#include <string>
#include <string_view>
#include <iterator>
#include <map>
#include "swoc/IPEndpoint.h"
#include "swoc/TextView.h"
#include "tsutil/StringCompare.h"
// Constants and some declarations
const char PLUGIN_NAME[] = "escalate";
const char REDIRECT_HEADER[] = "x-escalate-redirect";
static DbgCtl dbg_ctl{PLUGIN_NAME};
static int EscalateResponse(TSCont, TSEvent, void *);
//////////////////////////////////////////////////////////////////////////////////////////
// Hold information about the escalation / retry states for a remap rule.
//
struct EscalationState {
enum RetryType {
RETRY_URL,
RETRY_HOST,
};
struct RetryInfo {
RetryType type;
std::string target;
};
using StatusMapType = std::map<unsigned int, RetryInfo>;
EscalationState()
{
cont = TSContCreate(EscalateResponse, nullptr);
TSContDataSet(cont, this);
}
~EscalationState() { TSContDestroy(cont); }
TSCont cont;
StatusMapType status_map;
bool use_pristine = false;
bool escalate_non_get_methods = false;
bool add_redirect_header = true;
};
// Little helper function, to update the Host portion of a URL, and stringify the result.
// Returns the URL string, and updates url_len with the length.
char *
MakeEscalateUrl(TSMBuffer mbuf, TSMLoc url, const char *host, size_t host_len, int &url_len)
{
swoc::TextView input_host_view{host, host_len};
std::string_view host_view;
std::string_view port_view;
swoc::IPEndpoint::tokenize(input_host_view, &host_view, &port_view);
// Update the request URL with the new Host to try.
TSUrlHostSet(mbuf, url, host_view.data(), host_view.size());
if (port_view.size()) {
int const port_int = swoc::svtou(port_view);
TSUrlPortSet(mbuf, url, port_int);
Dbg(dbg_ctl, "Setting port to %d", port_int);
}
char *url_str = TSUrlStringGet(mbuf, url, &url_len);
Dbg(dbg_ctl, "Setting new URL from configured %.*s to %.*s", (int)host_len, host, url_len, url_str);
return url_str;
}
/**
* Check if the method is GET.
*
* @param txn The transaction whose method is to be checked.
* @return True if @a txn's method is GET, false otherwise.
*/
static bool
MethodIsGet(TSHttpTxn txn)
{
TSMBuffer req_mbuf;
TSMLoc req_hdrp;
if (TSHttpTxnClientReqGet(txn, &req_mbuf, &req_hdrp) != TS_SUCCESS) {
return false;
}
int method_len = 0;
char const *method = TSHttpHdrMethodGet(req_mbuf, req_hdrp, &method_len);
if (method == nullptr) {
return false;
}
std::string_view method_view{method, static_cast<size_t>(method_len)};
bool const is_get = (method_view == TS_HTTP_METHOD_GET);
TSHandleMLocRelease(req_mbuf, TS_NULL_MLOC, req_hdrp);
return is_get;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Main continuation for the plugin, examining an origin response for a potential retry.
//
static int
EscalateResponse(TSCont cont, TSEvent event, void *edata)
{
TSHttpTxn txn = static_cast<TSHttpTxn>(edata);
EscalationState *es = static_cast<EscalationState *>(TSContDataGet(cont));
TSMBuffer mbuf;
TSMLoc hdrp, url;
TSAssert(event == TS_EVENT_HTTP_READ_RESPONSE_HDR || event == TS_EVENT_HTTP_SEND_RESPONSE_HDR);
bool const processing_connection_error = (event == TS_EVENT_HTTP_SEND_RESPONSE_HDR);
if (!es->escalate_non_get_methods && !MethodIsGet(txn)) {
Dbg(dbg_ctl, "Skipping escalation for non-GET method");
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
if (processing_connection_error) {
TSServerState const state = TSHttpTxnServerStateGet(txn);
if (state == TS_SRVSTATE_CONNECTION_ALIVE) {
// There is no connection error, so nothing to do.
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
}
int const tries = TSHttpTxnRedirectRetries(txn);
if (0 != tries) { // ToDo: Future support for more than one retry-URL
Dbg(dbg_ctl, "Not pursuing failover due previous redirect already, num tries: %d", tries);
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
int ret = 0;
if (processing_connection_error) {
ret = TSHttpTxnClientRespGet(txn, &mbuf, &hdrp);
} else {
ret = TSHttpTxnServerRespGet(txn, &mbuf, &hdrp);
}
if (TS_SUCCESS != ret) {
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
// Next, the response status ...
TSHttpStatus const status = TSHttpHdrStatusGet(mbuf, hdrp);
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdrp);
// See if we have an escalation retry config for this response code.
auto const entry = es->status_map.find(status);
if (entry == es->status_map.end()) {
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
EscalationState::RetryInfo const &retry_info = entry->second;
Dbg(dbg_ctl, "Handling failover redirect for HTTP status %d", status);
char const *url_str = nullptr;
int url_len = 0;
if (EscalationState::RETRY_URL == retry_info.type) {
url_str = TSstrdup(retry_info.target.c_str());
url_len = retry_info.target.size();
Dbg(dbg_ctl, "Setting new URL to %.*s", url_len, url_str);
} else if (EscalationState::RETRY_HOST == retry_info.type) {
if (es->use_pristine) {
if (TS_SUCCESS == TSHttpTxnPristineUrlGet(txn, &mbuf, &url)) {
url_str = MakeEscalateUrl(mbuf, url, retry_info.target.c_str(), retry_info.target.size(), url_len);
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, url);
}
} else {
if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &mbuf, &hdrp)) {
if (TS_SUCCESS == TSHttpHdrUrlGet(mbuf, hdrp, &url)) {
url_str = MakeEscalateUrl(mbuf, url, retry_info.target.c_str(), retry_info.target.size(), url_len);
}
// Release the request MLoc
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdrp);
}
}
Dbg(dbg_ctl, "Setting host URL to %.*s", url_len, url_str);
}
// Now update the Redirect URL, if set
if (url_str) {
TSHttpTxnRedirectUrlSet(txn, url_str, url_len); // Transfers ownership
// Add our x-escalate-redirect header marker if it doesn't already exist and the option is enabled.
if (es->add_redirect_header) {
TSMBuffer bufp;
TSMLoc hdr_loc, field_loc;
if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &bufp, &hdr_loc)) {
field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, REDIRECT_HEADER, sizeof(REDIRECT_HEADER) - 1);
if (field_loc == nullptr) {
if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, REDIRECT_HEADER, sizeof(REDIRECT_HEADER) - 1, &field_loc) == TS_SUCCESS) {
if (TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, field_loc, -1, "1", 1) == TS_SUCCESS) {
TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
Dbg(dbg_ctl, "Added x-escalate-redirect header to the client request.");
}
} else {
Dbg(dbg_ctl, "x-escalate-redirect header already exists, not adding.");
}
TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
} else {
TSError("[%s] Failed to get client request header to add x-escalate-redirect header", PLUGIN_NAME);
}
}
}
// Set the transaction free ...
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
TSReturnCode
TSRemapInit(TSRemapInterface * /* api */, char * /* errbuf */, int /* bufsz */)
{
return TS_SUCCESS;
}
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **instance, char *errbuf, int errbuf_size)
{
EscalationState *es = new EscalationState();
// The first two arguments are the "from" and "to" URL string. We can just
// skip those, since we only ever remap on the error path.
for (int i = 2; i < argc; ++i) {
char *sep, *token, *save;
// Ugly, but we set the precedence before with non-command line parsing of args
if (ts::iequals(argv[i], "--pristine")) {
es->use_pristine = true;
} else if (ts::iequals(argv[i], "--no-redirect-header")) {
es->add_redirect_header = false;
} else if (ts::iequals(argv[i], "--escalate-non-get-methods")) {
es->escalate_non_get_methods = true;
} else {
// Each token should be a status code then a URL, separated by ':'.
sep = strchr(argv[i], ':');
if (sep == nullptr) {
snprintf(errbuf, errbuf_size, "malformed status:target config: %s", argv[i]);
goto fail;
}
*sep = '\0';
++sep; // Skip over the ':' (which is now \0)
// OK, we have a valid status/URL pair.
EscalationState::RetryInfo info;
info.target = sep;
if (std::string::npos != info.target.find('/')) {
info.type = EscalationState::RETRY_URL;
Dbg(dbg_ctl, "Creating Redirect rule with URL = %s", sep);
} else {
info.type = EscalationState::RETRY_HOST;
Dbg(dbg_ctl, "Creating Redirect rule with Host = %s", sep);
}
for (token = strtok_r(argv[i], ",", &save); token; token = strtok_r(nullptr, ",", &save)) {
unsigned status = strtol(token, nullptr, 10);
if (status < 100 || status > 599) {
snprintf(errbuf, errbuf_size, "invalid status code: %.*s", static_cast<int>(std::distance(argv[i], sep)), argv[i]);
goto fail;
}
Dbg(dbg_ctl, " added status = %d to rule", status);
es->status_map[status] = info;
}
}
}
*instance = es;
return TS_SUCCESS;
fail:
delete es;
return TS_ERROR;
}
void
TSRemapDeleteInstance(void *instance)
{
delete static_cast<EscalationState *>(instance);
}
TSRemapStatus
TSRemapDoRemap(void *instance, TSHttpTxn txn, TSRemapRequestInfo * /* rri */)
{
EscalationState *es = static_cast<EscalationState *>(instance);
TSHttpTxnHookAdd(txn, TS_HTTP_READ_RESPONSE_HDR_HOOK, es->cont);
TSHttpTxnHookAdd(txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, es->cont);
return TSREMAP_NO_REMAP;
}