diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 2d57f64f973..d947fc33a7f 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1177,8 +1177,24 @@ set-body 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. +Sets the body to ````. Can also be used to delete a body with ``""``. + +For origin response replacement, ``set-body`` is supported at both +``READ_RESPONSE_HDR_HOOK`` and ``SEND_RESPONSE_HDR_HOOK``. Prefer +``READ_RESPONSE_HDR_HOOK`` when possible so body replacement happens before +response body tunneling starts. + +.. note:: + + When ``set-body`` replaces an origin response body, ATS emits the replacement + through its internal error-body path. ``Content-Type`` defaults to + ``text/html`` unless you override it with ``set-header Content-Type``. + ``set-body ""`` clears the internal replacement body, but does not suppress an + origin response body on this hook; use a non-empty replacement value when + sanitizing origin responses. + The gold tests cover origin replacement for both hooks with and without a + response transform plugin. The no-transform matrix runs with HTTP cache + disabled and includes repeated-URL cache-bypass probes. set-body-from ~~~~~~~~~~~~~ diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index 20207bd24b8..3e34efbe7ee 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -813,6 +813,7 @@ void OperatorSetBody::initialize_hooks() { add_allowed_hook(TS_REMAP_PSEUDO_HOOK); + add_allowed_hook(TS_HTTP_READ_RESPONSE_HDR_HOOK); add_allowed_hook(TS_HTTP_SEND_RESPONSE_HDR_HOOK); } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 2f62c45f755..423d26a043a 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -4948,6 +4948,9 @@ TSHttpTxnErrorBodySet(TSHttpTxn txnp, char *buf, size_t buflength, char *mimetyp s->internal_msg_buffer = buf; s->internal_msg_buffer_size = buf ? buflength : 0; s->internal_msg_buffer_fast_allocator_size = -1; + // TSHttpTxnErrorBodySet() and TSHttpTxnServerRequestBodySet() share the same buffer. + // Switching to an error/response body override must clear the request-body mode. + s->api_server_request_body_set = false; s->internal_msg_buffer_type = mimetype; } diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index c8a6b128dc8..e07dc8981e4 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -1687,9 +1687,30 @@ HttpSM::handle_api_return() switch (t_state.next_action) { case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { - HttpTunnelProducer *p = setup_transfer_from_transform(); - perform_transform_cache_write_action(); - tunnel.tunnel_run(p); + if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set && t_state.hdr_info.server_response.valid()) { + SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform for internal transfer"); + t_state.api_info.cache_untransformed = true; + if (tunnel.is_tunnel_active()) { + tunnel.kill_tunnel(); + } + if (transform_info.entry != nullptr) { + vc_table.cleanup_entry(transform_info.entry); + transform_info.entry = nullptr; + } + transform_info.vc = nullptr; + if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { + t_state.hdr_info.client_response.create(HTTPType::RESPONSE); + t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); + } + if (server_entry != nullptr && server_entry->in_tunnel == false) { + release_server_session(); + } + setup_internal_transfer(&HttpSM::tunnel_handler); + } else { + HttpTunnelProducer *p = setup_transfer_from_transform(); + perform_transform_cache_write_action(); + tunnel.tunnel_run(p); + } break; } case HttpTransact::StateMachineAction_t::SERVER_READ: { @@ -1722,6 +1743,17 @@ HttpSM::handle_api_return() } setup_blind_tunnel(true, initial_data); + } else if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set && t_state.hdr_info.server_response.valid() && + plugin_tunnel == nullptr && + (api_hooks.get(TS_HTTP_READ_RESPONSE_HDR_HOOK) != nullptr || + api_hooks.get(TS_HTTP_SEND_RESPONSE_HDR_HOOK) != nullptr)) { + // A plugin replaced the origin response body via TSHttpTxnErrorBodySet(). + // Serve the synthetic body before entering the response body tunnel. + SMDbg(dbg_ctl_http, "plugin set internal body, using internal transfer instead of server tunnel"); + if (server_entry != nullptr && server_entry->in_tunnel == false) { + release_server_session(); + } + setup_internal_transfer(&HttpSM::tunnel_handler); } else { HttpTunnelProducer *p = setup_server_transfer(); perform_cache_write_action(); @@ -7578,12 +7610,19 @@ HttpSM::setup_client_request_plugin_agents(HttpTunnelProducer *p, int num_header inline void HttpSM::transform_cleanup(TSHttpHookID hook, HttpTransformInfo *info) { + if (info->entry == nullptr) { + return; + } APIHook *t_hook = api_hooks.get(hook); if (t_hook && info->vc == nullptr) { do { - VConnection *t_vcon = t_hook->m_cont; - t_vcon->do_io_close(); - t_hook = t_hook->m_link.next; + APIHook *next = t_hook->m_link.next; + // Some transform hooks can already be detached by the time kill_this() runs. + // Guard against null continuations while still draining the remaining hooks. + if (auto *t_vcon = static_cast(t_hook->m_cont); t_vcon != nullptr) { + t_vcon->do_io_close(); + } + t_hook = next; } while (t_hook != nullptr); } } @@ -7664,7 +7703,11 @@ HttpSM::kill_this() // In that case, we need to manually close all the // transforms to prevent memory leaks (INKqa06147) if (hooks_set) { - transform_cleanup(TS_HTTP_RESPONSE_TRANSFORM_HOOK, &transform_info); + bool bypassed_response_transform = + t_state.api_info.cache_untransformed && t_state.internal_msg_buffer && !t_state.api_server_request_body_set; + if (!bypassed_response_transform) { + transform_cleanup(TS_HTTP_RESPONSE_TRANSFORM_HOOK, &transform_info); + } transform_cleanup(TS_HTTP_REQUEST_TRANSFORM_HOOK, &post_transform_info); plugin_agents_cleanup(); } @@ -8230,6 +8273,21 @@ HttpSM::set_next_state() case HttpTransact::StateMachineAction_t::SERVER_READ: { t_state.source = HttpTransact::Source_t::HTTP_ORIGIN_SERVER; + if (transform_info.vc && t_state.internal_msg_buffer && !t_state.api_server_request_body_set && + t_state.hdr_info.server_response.valid()) { + SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform"); + t_state.api_info.cache_untransformed = true; + if (transform_info.entry != nullptr) { + vc_table.cleanup_entry(transform_info.entry); + transform_info.entry = nullptr; + } + transform_info.vc = nullptr; + if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { + t_state.hdr_info.client_response.create(HTTPType::RESPONSE); + t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); + } + } + if (transform_info.vc) { ink_assert(t_state.hdr_info.client_response.valid() == 0); ink_assert((t_state.hdr_info.transform_response.valid() ? true : false) == true); diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml new file mode 100644 index 00000000000..8363f020ef5 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml @@ -0,0 +1,347 @@ +# 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. + +meta: + version: "1.0" + +autest: + description: 'Test set-body origin replacement without response transforms' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + process_config: + other_args: '--thread-limit 1' + + ats: + name: 'ts' + + plugin_config: + - null_transform.so + + copy_to_config_dir: + - 'rules' + + records_config: + proxy.config.http.insert_response_via_str: 0 + proxy.config.http.cache.http: 0 + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'http|header_rewrite' + + remap_config: + # Block 1: READ_RESPONSE hook replacement path (no transform plugin). + - from: "http://www.example.com/set_body_read_resp_403/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_read_403/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 2: SEND_RESPONSE hook replacement path (no transform plugin). + - from: "http://www.example.com/set_body_send_resp_403/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_send_403/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + + # Block 3a: cache-bypass probe for READ_RESPONSE hook (same URL twice). + - from: "http://www.example.com/cache_probe_read/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/cache_probe_read/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 3b: cache-bypass probe for SEND_RESPONSE hook (same URL twice). + - from: "http://www.example.com/cache_probe_send/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/cache_probe_send/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + + # Block 4a: READ_RESPONSE hook with response transform plugin active. + - from: "http://www.example.com/set_body_transform_read/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_transform_read/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 4b: SEND_RESPONSE hook with response transform plugin active. + - from: "http://www.example.com/set_body_transform_send/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_transform_send/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + +sessions: + +- transactions: + # Block 1 verification: READ_RESPONSE hook replacement. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_read_resp_403/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 1 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + - [ Content-Type, { value: "text/html", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 2 verification: SEND_RESPONSE hook replacement. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_send_resp_403/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 2 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + - [ Content-Type, { value: "text/html", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3a verification: cache-bypass probe for READ_RESPONSE. + # First response on repeated URL. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 3 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "5" ] + - [ Content-Type, "text/plain" ] + content: + size: 5 + data: "first" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3a verification: cache-bypass probe for READ_RESPONSE. + # Second response on repeated URL should still be replaced. + # Keep this as non-200 so null_transform does not engage. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 4 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "6" ] + - [ Content-Type, "text/plain" ] + content: + size: 6 + data: "second" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3b verification: cache-bypass probe for SEND_RESPONSE. + # First response on repeated URL. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 5 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "5" ] + - [ Content-Type, "text/plain" ] + content: + size: 5 + data: "first" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3b verification: cache-bypass probe for SEND_RESPONSE. + # Second response on repeated URL should still be replaced. + # Keep this as non-200 so null_transform does not engage. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 6 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "6" ] + - [ Content-Type, "text/plain" ] + content: + size: 6 + data: "second" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 4a verification: READ_RESPONSE with transform plugin active. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_transform_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 7 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 4b verification: SEND_RESPONSE with transform plugin active. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_transform_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 8 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py new file mode 100644 index 00000000000..0eb53765b2e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py @@ -0,0 +1,27 @@ +''' +Test header_rewrite set-body replacing origin server response bodies. +''' +# 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 set-body origin replacement matrix: +- no transform plugin (READ_RESPONSE_HDR and SEND_RESPONSE_HDR, with cache-bypass probes) +- null_transform plugin active (READ_RESPONSE_HDR and SEND_RESPONSE_HDR) +''' + +Test.SkipUnless(Condition.PluginExists('null_transform.so')) +Test.ATSReplayTest(replay_file="header_rewrite_set_body_origin.replay.yaml") diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf new file mode 100644 index 00000000000..3d024c2c89e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf @@ -0,0 +1,19 @@ +# +# 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} + set-body "Sanitized" diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf new file mode 100644 index 00000000000..4d4321dee6e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf @@ -0,0 +1,19 @@ +# +# 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 %{SEND_RESPONSE_HDR_HOOK} + set-body "Sanitized" diff --git a/tests/prepare_proxy_verifier.sh b/tests/prepare_proxy_verifier.sh index 3e853a30f1b..8c570efc7f8 100755 --- a/tests/prepare_proxy_verifier.sh +++ b/tests/prepare_proxy_verifier.sh @@ -40,7 +40,7 @@ pv_dir="${pv_name}-${pv_version}" pv_tar_filename="${pv_dir}.tar.gz" pv_tar="${pv_top_dir}/${pv_tar_filename}" pv_tar_url="https://ci.trafficserver.apache.org/bintray/${pv_tar_filename}" -expected_sha1="e11b5867a56c5ffd496b18c901f1273e9c120a47" +expected_sha1="0a60c646cbc9326abb2fbc397cb9efa8c08a807a" pv_client="${bin_dir}/verifier-client" pv_server="${bin_dir}/verifier-server" TAR=${TAR:-tar}