Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/admin-guide/plugins/header_rewrite.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,31 @@ set-body
Sets the body to ``<text>``. Can also be used to delete a body with ``""``. This is only useful when overriding the origin status, i.e.
intercepting/pre-empting a request so that you can override the body from the body-factory with your own.

set-body-from
~~~~~~~~~~~~~
::

set-body-from <URL>

Will call ``<URL>`` (see URL in `URL Parts`_) to retrieve a custom error response
and set the body with the result. Triggering this rule on an OK transaction will
send a 500 status code to the client with the desired response. If this is triggered
on any error status code, that original status code will be sent to the client.
**Note**: This config should only be set using READ_RESPONSE_HDR_HOOK

An example config would look like

cond %{READ_RESPONSE_HDR_HOOK}
set-body-from http://www.example.com/second

Where ``http://www.example.com/second`` is the destination to retrieve the custom response from.
This can be enabled per-mapping or globally.
Ensure there is a remap rule for the second endpoint as well!
An example remap config would look like

map /first http://www.example.com/first @plugin=header_rewrite.so @pparam=cond1.conf
map /second http://www.example.com/second

set-config
~~~~~~~~~~
::
Expand Down
3 changes: 2 additions & 1 deletion plugins/header_rewrite/factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ operator_factory(const std::string &op)
o = new OperatorSetHttpCntl();
} else if (op == "run-plugin") {
o = new OperatorRunPlugin();

} else if (op == "set-body-from") {
o = new OperatorSetBodyFrom();
} else {
TSError("[%s] Unknown operator: %s", PLUGIN_NAME, op.c_str());
return nullptr;
Expand Down
142 changes: 142 additions & 0 deletions plugins/header_rewrite/operators.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,91 @@
#include "operators.h"
#include "ts/apidefs.h"

namespace
{
const unsigned int LOCAL_IP_ADDRESS = 0x0100007f;
const unsigned int MAX_SIZE = 256;
const int LOCAL_PORT = 8080;

int
handleFetchEvents(TSCont cont, TSEvent event, void *edata)
{
TSHttpTxn http_txn = static_cast<TSHttpTxn>(TSContDataGet(cont));

switch (static_cast<int>(event)) {
case OperatorSetBodyFrom::TS_EVENT_FETCHSM_SUCCESS: {
TSHttpTxn fetchsm_txn = static_cast<TSHttpTxn>(edata);
int data_len;
const char *data_start = TSFetchRespGet(fetchsm_txn, &data_len);
if (data_start && (data_len > 0)) {
const char *data_end = data_start + data_len;
TSHttpParser parser = TSHttpParserCreate();
TSMBuffer hdr_buf = TSMBufferCreate();
TSMLoc hdr_loc = TSHttpHdrCreate(hdr_buf);
TSHttpHdrTypeSet(hdr_buf, hdr_loc, TS_HTTP_TYPE_RESPONSE);
if (TSHttpHdrParseResp(parser, hdr_buf, hdr_loc, &data_start, data_end) == TS_PARSE_DONE) {
TSHttpTxnErrorBodySet(http_txn, TSstrdup(data_start), (data_end - data_start), nullptr);
} else {
TSWarning("[%s] Unable to parse set-custom-body fetch response", __FUNCTION__);
}
TSHttpParserDestroy(parser);
TSHandleMLocRelease(hdr_buf, nullptr, hdr_loc);
TSMBufferDestroy(hdr_buf);
} else {
TSWarning("[%s] Successful set-custom-body fetch did not result in any content", __FUNCTION__);
}
TSHttpTxnReenable(http_txn, TS_EVENT_HTTP_ERROR);
} break;
case OperatorSetBodyFrom::TS_EVENT_FETCHSM_FAILURE: {
Dbg(pi_dbg_ctl, "OperatorSetBodyFrom: Error getting custom body");
TSHttpTxnReenable(http_txn, TS_EVENT_HTTP_CONTINUE);
} break;
case OperatorSetBodyFrom::TS_EVENT_FETCHSM_TIMEOUT: {
Dbg(pi_dbg_ctl, "OperatorSetBodyFrom: Timeout getting custom body");
TSHttpTxnReenable(http_txn, TS_EVENT_HTTP_CONTINUE);
} break;
case TS_EVENT_HTTP_TXN_CLOSE: {
TSContDestroy(cont);
TSHttpTxnReenable(http_txn, TS_EVENT_HTTP_CONTINUE);
} break;
case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of executing the continuation on this hook (since nothing is done)?

Copy link
Collaborator Author

@jasmine-nahrain jasmine-nahrain Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pauses the original transaction to allow time to get the response from the second endpoint. The second endpoint will reenable the first once it has completed. It is written in the code for readability

// Do nothing
// The transaction is reenabled with the FetchSM transaction
break;
default:
TSError("[%s] handleFetchEvents got unknown event: %d", PLUGIN_NAME, event);
break;
}
return 0;
}

TSReturnCode
createRequestString(const std::string_view &value, char (&req_buf)[MAX_SIZE], int *req_buf_size)
{
const char *start = value.data();
const char *end = start + value.size();
TSMLoc url_loc;
TSMBuffer url_buf = TSMBufferCreate();
int host_len, url_len = 0;

if (TSUrlCreate(url_buf, &url_loc) == TS_SUCCESS && TSUrlParse(url_buf, url_loc, &start, end) == TS_PARSE_DONE) {
const char *host = TSUrlHostGet(url_buf, url_loc, &host_len);
const char *url = TSUrlStringGet(url_buf, url_loc, &url_len);

*req_buf_size = snprintf(req_buf, MAX_SIZE, "GET %.*s HTTP/1.1\r\nHost: %.*s\r\n\r\n", url_len, url, host_len, host);

TSMBufferDestroy(url_buf);

return TS_SUCCESS;
} else {
Dbg(pi_dbg_ctl, "Failed to parse url %s", start);
TSMBufferDestroy(url_buf);
return TS_ERROR;
}
}

} // namespace

// OperatorConfig
void
OperatorSetConfig::initialize(Parser &p)
Expand Down Expand Up @@ -1219,3 +1304,60 @@ OperatorRunPlugin::exec(const Resources &res) const
_plugin->doRemap(res.txnp, res._rri);
}
}

// OperatorSetBody
void
OperatorSetBodyFrom::initialize(Parser &p)
{
Operator::initialize(p);
// we want the arg since body only takes one value
_value.set_value(p.get_arg());
require_resources(RSRC_SERVER_RESPONSE_HEADERS);
require_resources(RSRC_RESPONSE_STATUS);
}

void
OperatorSetBodyFrom::initialize_hooks()
{
add_allowed_hook(TS_HTTP_READ_RESPONSE_HDR_HOOK);
}

void
OperatorSetBodyFrom::exec(const Resources &res) const
{
if (TSHttpTxnIsInternal(res.txnp)) {
// If this is triggered by an internal transaction, a infinte loop may occur
// It should only be triggered by the original transaction sent by the client
Dbg(pi_dbg_ctl, "OperatorSetBodyFrom triggered by an internal transaction");
return;
}

char req_buf[MAX_SIZE];
int req_buf_size = 0;
if (createRequestString(_value.get_value(), req_buf, &req_buf_size) == TS_SUCCESS) {
TSCont fetchCont = TSContCreate(handleFetchEvents, TSMutexCreate());
TSContDataSet(fetchCont, static_cast<void *>(res.txnp));

TSHttpTxnHookAdd(res.txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, fetchCont);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose there is another continuation for the transaction, that is either after the continuation calling this exec func on the READ_RESPONSE_HDR_HOOK, or before fetchCont on the SEND_RESPONSE_HDR_HOOK And, suppose this continuation, like fetchEvent for the SEND_RESPONSE_HDR evert, does not call TxnReenable(). Seems like there could be a race condition, where the URL fetch body done event reenables for that other continuation by mistake.

Could you add a return value to the operator exec funtions, which would control whether the continuation running the exec functions called TxnReenable()? That way, no other continuations of transaction hooks would be run until the fetch of the body URL finished.

Copy link
Collaborator Author

@jasmine-nahrain jasmine-nahrain Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you are saying but I dont see how the exec func returning something would help. I think a return value would only have impact on header_rewrite rather than all plugins. This would also make changes to all of header_rewrite.

Copy link
Contributor

@ywkaras ywkaras Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I'm proposing, for the exec() function to return a flag that controls whether reenable is called: ywkaras@0ef4d24

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think it would be better to seperate these changes into 2 PRs because the change you are proposing impacts all of header rewrite.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I can put up a follow-on PR once this is merged.

TSHttpTxnHookAdd(res.txnp, TS_HTTP_TXN_CLOSE_HOOK, fetchCont);

TSFetchEvent event_ids;
event_ids.success_event_id = TS_EVENT_FETCHSM_SUCCESS;
event_ids.failure_event_id = TS_EVENT_FETCHSM_FAILURE;
event_ids.timeout_event_id = TS_EVENT_FETCHSM_TIMEOUT;

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = LOCAL_IP_ADDRESS;
addr.sin_port = LOCAL_PORT;
TSFetchUrl(static_cast<const char *>(req_buf), req_buf_size, reinterpret_cast<struct sockaddr const *>(&addr), fetchCont,
AFTER_BODY, event_ids);

// Forces original status code in event TSHttpTxnErrorBodySet changed
// the code or another condition was set conflicting with this one.
// Set here because res is the only structure that contains the original status code.
TSHttpTxnStatusSet(res.txnp, res.resp_status);
} else {
TSError(PLUGIN_NAME, "OperatorSetBodyFrom:exec:: Could not create request");
}
}
21 changes: 21 additions & 0 deletions plugins/header_rewrite/operators.h
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,24 @@ class OperatorRunPlugin : public Operator
private:
RemapPluginInst *_plugin = nullptr;
};

class OperatorSetBodyFrom : public Operator
{
public:
OperatorSetBodyFrom() { Dbg(pi_dbg_ctl, "Calling CTOR for OperatorSetBodyFrom"); }

// noncopyable
OperatorSetBodyFrom(const OperatorSetBodyFrom &) = delete;
void operator=(const OperatorSetBodyFrom &) = delete;

void initialize(Parser &p) override;

enum { TS_EVENT_FETCHSM_SUCCESS = 70000, TS_EVENT_FETCHSM_FAILURE = 70001, TS_EVENT_FETCHSM_TIMEOUT = 70002 };

protected:
void initialize_hooks() override;
void exec(const Resources &res) const override;

private:
Value _value;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Custom body found
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<HTML>
<HEAD>
<TITLE>Unknown Host</TITLE>
</HEAD>

<BODY BGCOLOR="white" FGCOLOR="black">
<H1>Unknown Host</H1>
<HR>

<FONT FACE="Helvetica,Arial"><B>
Description: Unable to locate the server requested ---
the server does not have a DNS entry. Perhaps there is a misspelling
in the server name, or the server no longer exists. Double-check the
name and try again.
</B></FONT>
<HR>
</BODY>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<HTML>
<HEAD>
<TITLE>Not Found on Accelerator</TITLE>
</HEAD>

<BODY BGCOLOR="white" FGCOLOR="black">
<H1>Not Found on Accelerator</H1>
<HR>

<FONT FACE="Helvetica,Arial"><B>
Description: Your request on the specified host was not found.
Check the location and try again.
</B></FONT>
<HR>
</BODY>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Custom body found
Loading