diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index a5128b698fb..7d971a0f4df 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -704,6 +704,31 @@ set-body Sets the body to ````. 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 + +Will call ```` (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 ~~~~~~~~~~ :: diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc index 531f390f24c..346265f893f 100644 --- a/plugins/header_rewrite/factory.cc +++ b/plugins/header_rewrite/factory.cc @@ -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; diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index b22f04253a4..0bb87f89c32 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -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(TSContDataGet(cont)); + + switch (static_cast(event)) { + case OperatorSetBodyFrom::TS_EVENT_FETCHSM_SUCCESS: { + TSHttpTxn fetchsm_txn = static_cast(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: + // 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) @@ -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(res.txnp)); + + TSHttpTxnHookAdd(res.txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, fetchCont); + 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(req_buf), req_buf_size, reinterpret_cast(&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"); + } +} diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h index dc7b19ee4a1..2c4712a67bb 100644 --- a/plugins/header_rewrite/operators.h +++ b/plugins/header_rewrite/operators.h @@ -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; +}; diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold new file mode 100644 index 00000000000..3e37495f65b --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold @@ -0,0 +1 @@ +Custom body found diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold new file mode 100644 index 00000000000..70d40bddd16 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold @@ -0,0 +1,17 @@ + + +Unknown Host + + + +

Unknown Host

+
+ + +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. + +
+ diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold new file mode 100644 index 00000000000..8f069904f11 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold @@ -0,0 +1,15 @@ + + +Not Found on Accelerator + + + +

Not Found on Accelerator

+
+ + +Description: Your request on the specified host was not found. +Check the location and try again. + +
+ diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold new file mode 100644 index 00000000000..3e37495f65b --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold @@ -0,0 +1 @@ +Custom body found diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py new file mode 100644 index 00000000000..925458d5d8b --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py @@ -0,0 +1,161 @@ +# 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. + +Test.Summary = ''' +Test for successful response manipulation using set-body-from +''' +Test.ContinueOnFail = True + + +class HeaderRewriteSetBodyFromTest: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = Test.MakeOriginServer("server") + + # Response for original transaction + response_header = {"headers": "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n", "body": "404 Not Found"} + + # Request/response for original transaction where transaction returns a 200 status code + remap_success_request_header = {"headers": "GET /200 HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + ooo = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "200 OK"} + + self.server.addResponse("sessionfile.log", remap_success_request_header, ooo) + + # Request/response for original transaction with failed second tranasaction + remap_fail_1_request_header = {"headers": "GET /remap_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", remap_fail_1_request_header, response_header) + + plugin_fail_1_request_header = {"headers": "GET /plugin_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", plugin_fail_1_request_header, response_header) + + # Request/response for original successful transaction with successful second tranasaction + remap_success_1_request_header = {"headers": "GET /remap_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", remap_success_1_request_header, response_header) + + plugin_success_1_request_header = {"headers": "GET /plugin_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", plugin_success_1_request_header, response_header) + + # Request/response for custom body transaction that successfully retrieves body + success_2_request_header = {"headers": "GET /404.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + success_2_response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "Custom body found\n"} + self.server.addResponse("sessionfile.log", success_2_request_header, success_2_response_header) + + def setUpTS(self): + self.ts = Test.MakeATSProcess("ts") + + # Set header rewrite rules + self.ts.Setup.CopyAs('rules/rule_set_body_from_remap.conf', Test.RunDirectory) + self.ts.Setup.CopyAs('rules/rule_set_body_from_plugin.conf', Test.RunDirectory) + + self.ts.Disk.remap_config.AddLine( + """\ + map http://www.example.com/remap_success http://127.0.0.1:{0}/remap_success @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/200 http://127.0.0.1:{0}/200 @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/remap_fail http://127.0.0.1:{0}/remap_fail @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/plugin_success http://127.0.0.1:{0}/plugin_success + map http://www.example.com/plugin_fail http://127.0.0.1:{0}/plugin_fail + map http://www.example.com/404.html http://127.0.0.1:{0}/404.html + map http://www.example.com/plugin_no_server http://127.0.0.1::{2}/plugin_no_server + """.format(self.server.Variables.Port, Test.RunDirectory, Test.GetTcpPort("bad_port"))) + self.ts.Disk.plugin_config.AddLine('header_rewrite.so {0}/rule_set_body_from_plugin.conf'.format(Test.RunDirectory)) + + def test_setBodyFromFails_remap(self): + ''' + Test where set-body-from request fails + Triggered from remap file + This uses the case where no remap rule is provided + ''' + tr = Test.AddTestRun() + tr.Processes.Default.Command = ( + 'curl -s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_fail"'.format(self.ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_remap_fail.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_remap(self): + ''' + Test where set-body-from request succeeds + Triggered from remap file + ''' + tr = Test.AddTestRun() + tr.Processes.Default.Command = ( + 'curl -s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_success"'.format(self.ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_plugin(self): + ''' + Test where set-body-from request succeeds + Triggered from plugin file + ''' + tr = Test.AddTestRun() + tr.Processes.Default.Command = ( + 'curl -s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_success"'.format(self.ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromFails_plugin(self): + ''' + Test where set-body-from request fails + This uses the case where the second endpoint cannot connect to the requested server + Triggered from plugin file + ''' + tr = Test.AddTestRun() + tr.Processes.Default.Command = ( + 'curl -s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_fail"'.format(self.ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_conn_fail.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_200(self): + ''' + Test where set-body-from request succeeds and returns 200 OK + Triggered from remap file + This is tested because right now, TSHttpTxnErrorBodySet will change OK status codes to 500 INKApi Error + Ideally, this would not occur. + ''' + tr = Test.AddTestRun() + tr.Processes.Default.Command = ( + 'curl -s -v --proxy 127.0.0.1:{0} "http://www.example.com/200"'.format(self.ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_200.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("500 INKApi Error", "Expected 500 response") + tr.StillRunningAfter = self.server + + def runTraffic(self): + self.test_setBodyFromFails_remap() + self.test_setBodyFromSucceeds_remap() + self.test_setBodyFromSucceeds_plugin() + self.test_setBodyFromFails_plugin() + self.test_setBodyFromSucceeds_200() + + def run(self): + self.runTraffic() + + +HeaderRewriteSetBodyFromTest().run() diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf new file mode 100644 index 00000000000..c4c83abb17d --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf @@ -0,0 +1,26 @@ +# +# 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. + +# Test uses cond %{CLIENT-URL:PATH} to differentiate tests +# It is not needed to make set-body-from work +cond %{READ_RESPONSE_HDR_HOOK} +cond %{CLIENT-URL:PATH} = "plugin_success" +set-body-from http://www.example.com/404.html + +cond %{READ_RESPONSE_HDR_HOOK} +cond %{CLIENT-URL:PATH} = "plugin_fail" +set-body-from http://www.example.com/plugin_no_server diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf new file mode 100644 index 00000000000..351e17f7b8d --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf @@ -0,0 +1,24 @@ +# +# 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. +cond %{READ_RESPONSE_HDR_HOOK} +cond %{CLIENT-URL:PATH} = "remap_success" [OR] +cond %{CLIENT-URL:PATH} = "200" +set-body-from http://www.example.com/404.html + +cond %{READ_RESPONSE_HDR_HOOK} +cond %{CLIENT-URL:PATH} = "remap_fail" +set-body-from http://www.example.com/fail