diff --git a/doc/admin-guide/plugins/cookie_remap.en.rst b/doc/admin-guide/plugins/cookie_remap.en.rst index 9e64c6fbd39..602daf2f89b 100644 --- a/doc/admin-guide/plugins/cookie_remap.en.rst +++ b/doc/admin-guide/plugins/cookie_remap.en.rst @@ -41,6 +41,7 @@ Cookie Based Routing Inside TrafficServer Using cookie_remap * :ref:`status: HTTP status-code ` * :ref:`else: url [optional] ` * :ref:`connector: and ` + * :ref:`disable_pristine_host_hdr: true|false [optional] ` * :ref:`Reserved path expressions ` @@ -216,6 +217,22 @@ connector: and 'and' is the only supported connector +.. _disable-pristine-host-hdr: + +disable_pristine_host_hdr: true|false [optional] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When set to ``true``, disables the ``proxy.config.url_remap.pristine_host_hdr`` +configuration for the matched transaction, allowing the Host header to be +updated to match the hostname in the ``sendto`` URL. This is useful when +downstream routing (such as a parent proxy or origin server selection) depends +on the Host header value matching the remapped destination. The default value +is ``false``, which preserves the pristine host header behavior. + +This option only affects the successful match (``sendto``) path. The ``else`` +path will continue to use the configured pristine host header setting (typically +enabled in production environments). + .. _reserved-path-expressions: Reserved path expressions @@ -389,6 +406,33 @@ An example configuration file sendto: http://cnn.com/$1 else: http://yahoo.com +Example with Pristine Host Header Control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example demonstrates using ``disable_pristine_host_hdr`` to route a +percentage of users to a canary server in an environment where +``proxy.config.url_remap.pristine_host_hdr`` is normally enabled. When the +cookie bucket matches, pristine host header is disabled for that transaction, +allowing the Host header to be updated to match the remapped destination:: + + op: + cookie: SessionID + operation: bucket + bucket: 1/10 + sendto: https://canary.example.com/app/$unmatched_path + disable_pristine_host_hdr: true + else: https://stable.example.com/app/$unmatched_path + +In this configuration: + +* 10% of users (based on the SessionID cookie) are sent to the canary server. +* For canary traffic, pristine host header is disabled, so the Host header is + updated to ``canary.example.com``. +* For regular traffic (else path), pristine host header remains enabled, so the + Host header stays as the original client value. +* This allows downstream routing (parent proxies or origin servers) to direct + canary traffic correctly based on the updated Host header. + .. _debugging-things: Debugging things diff --git a/plugins/experimental/cookie_remap/cookie_remap.cc b/plugins/experimental/cookie_remap/cookie_remap.cc index bcaa4d207ec..ce7f5caeb48 100644 --- a/plugins/experimental/cookie_remap/cookie_remap.cc +++ b/plugins/experimental/cookie_remap/cookie_remap.cc @@ -502,6 +502,18 @@ class op else_status = static_cast(atoi(s.c_str())); } + void + setDisablePristineHostHdr(bool val) + { + disable_pristine_host_hdr = val; + } + + bool + getDisablePristineHostHdr() const + { + return disable_pristine_host_hdr; + } + void printOp() const { @@ -515,10 +527,14 @@ class op if (else_sendto.size() > 0) { Dbg(dbg_ctl, "else: %s", else_sendto.c_str()); } + if (disable_pristine_host_hdr) { + Dbg(dbg_ctl, "disable_pristine_host_hdr: true"); + } } bool - process(CookieJar &jar, std::string &dest, TSHttpStatus &retstat, TSRemapRequestInfo *rri, UrlComponents &req_url) const + process(CookieJar &jar, std::string &dest, TSHttpStatus &retstat, TSRemapRequestInfo *rri, UrlComponents &req_url, + bool &used_sendto) const { if (sendto == "") { return false; // guessing every operation must have a @@ -774,12 +790,14 @@ class op if (status > 0) { retstat = status; } + used_sendto = true; // We took the sendto path return true; } else if (else_sendto.size() > 0 && retval == 0) { dest = else_sendto; if (else_status > 0) { retstat = else_status; } + used_sendto = false; // We took the else path return true; } else { dest = ""; @@ -791,8 +809,9 @@ class op SubOpQueue subops{}; std::string sendto{""}; std::string else_sendto{""}; - TSHttpStatus status = TS_HTTP_STATUS_NONE; - TSHttpStatus else_status = TS_HTTP_STATUS_NONE; + TSHttpStatus status = TS_HTTP_STATUS_NONE; + TSHttpStatus else_status = TS_HTTP_STATUS_NONE; + bool disable_pristine_host_hdr = false; }; using StringPair = std::pair; @@ -831,6 +850,10 @@ build_op(op &o, OpMap const &q) o.setStatus(val); } + if (key == "disable_pristine_host_hdr") { + o.setDisablePristineHostHdr(val == "true" || val == "1" || val == "yes"); + } + if (key == "operation") { sub->setOperation(val); } @@ -1183,7 +1206,8 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) for (auto &op : *ops) { Dbg(dbg_ctl, ">>> processing new operation"); - if (op->process(jar, rewrite_to, status, rri, req_url)) { + bool used_sendto = false; + if (op->process(jar, rewrite_to, status, rri, req_url, used_sendto)) { cr_substitutions(rewrite_to, req_url); size_t pos = 7; // 7 because we want to ignore the // in @@ -1244,6 +1268,16 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) TSError("can't parse substituted URL string"); goto error; } else { + // Disable pristine host header if configured to do so and we took the + // sendto path. This allows the Host header to be updated to match the + // remapped destination. The else path (i.e., the non-sendto one) + // always preserves the pristine host header configuration, whether + // enabled or disabled. + if (op->getDisablePristineHostHdr() && used_sendto) { + Dbg(dbg_ctl, "Disabling pristine_host_hdr for this transaction (sendto path)"); + TSHttpTxnConfigIntSet(txnp, TS_CONFIG_URL_REMAP_PRISTINE_HOST_HDR, 0); + } + if (field != nullptr) { TSHandleMLocRelease(rri->requestBufp, rri->requestHdrp, field); } diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt b/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt new file mode 100644 index 00000000000..a9ec744ffcd --- /dev/null +++ b/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt @@ -0,0 +1,10 @@ +# Test configuration for disable_pristine_host_hdr functionality +# Routes cookie bucket traffic to canary server WITHOUT updating Host header + +op: + cookie: SessionID + operation: bucket + bucket: 30/100 + sendto: http://canary.com:$CANARY_PORT/app/test + else: http://stable.com:$STABLE_PORT/app/test + diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt b/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt new file mode 100644 index 00000000000..fad59a5775d --- /dev/null +++ b/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt @@ -0,0 +1,11 @@ +# Test configuration for disable_pristine_host_hdr functionality +# Routes cookie bucket traffic to canary server with pristine host header disabled + +op: + cookie: SessionID + operation: bucket + bucket: 30/100 + sendto: http://canary.com:$CANARY_PORT/app/test + disable_pristine_host_hdr: true + else: http://stable.com:$STABLE_PORT/app/test + diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py new file mode 100644 index 00000000000..88489687984 --- /dev/null +++ b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py @@ -0,0 +1,127 @@ +''' +Verify cookie_remap plugin's disable_pristine_host_hdr functionality. +''' +# 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. + +import os + +Test.Summary = ''' +Test cookie_remap plugin's disable_pristine_host_hdr functionality. Verifies that +when disable_pristine_host_hdr is set to true, the pristine host header setting +is disabled for the matched transaction, allowing the Host header to be updated +to match the remapped destination. +''' +Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) +Test.ContinueOnFail = True + + +class TestUpdateHostHeader: + """ + Test the disable_pristine_host_hdr feature of cookie_remap plugin. + + This test verifies that: + 1. Cookie bucket routing works correctly + 2. When disable_pristine_host_hdr is enabled, the Host header is updated + to match the sendto URL destination + 3. When disable_pristine_host_hdr is not set, the Host header remains as + the original client value + """ + + # Counter for unique process names across multiple test instances + test_counter: int = 0 + + def __init__(self, disable_pristine_host_hdr=True): + """Initialize the test by setting up servers and ATS configuration. + :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr + in the cookie_remap configuration. + """ + self.test_id = TestUpdateHostHeader.test_counter + TestUpdateHostHeader.test_counter += 1 + self.disable_pristine_host_hdr = disable_pristine_host_hdr + self.replay_file = f'disable_pristine_host_hdr_{"true" if disable_pristine_host_hdr else "false"}.replay.yaml' + self._setupDns() + self._setupServers() + self._setupTS(disable_pristine_host_hdr) + self._setupClient() + + def _setupDns(self): + """Configure the DNS server.""" + self._dns = Test.MakeDNServer(f"dns_{self.test_id}", default='127.0.0.1') + + def _setupServers(self): + """ + Configure the origin servers using proxy-verifier. + + Creates two servers to simulate canary and stable environments. + """ + self._server_canary = Test.MakeVerifierServerProcess(f"server_canary_{self.test_id}", self.replay_file) + expected_host = 'canary.com' if self.disable_pristine_host_hdr else 'example.com' + self._server_canary.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') + + self._server_stable = Test.MakeVerifierServerProcess(f"server_stable_{self.test_id}", self.replay_file) + # The else path always preserves pristine host header (example.com). + expected_host = 'example.com' + self._server_stable.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') + + def _setupTS(self, disable_pristine_host_hdr): + """Configure Traffic Server with cookie_remap plugin. + :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr in cookie_remap. + """ + ts = Test.MakeATSProcess(f"ts_{self.test_id}", enable_cache=False) + self._ts = ts + + # Enable debug logging for cookie_remap and enable pristine_host_hdr + # (simulating production environment) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap|http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + # Read and configure the cookie_remap configuration file + config_filename = f'disable_pristine_host_hdr_config_{"true" if disable_pristine_host_hdr else "false"}.txt' + config_path = os.path.join(Test.TestDirectory, f"configs/{config_filename}") + with open(config_path, 'r') as config_file: + config_content = config_file.read() + config_content = config_content.replace("$CANARY_PORT", str(self._server_canary.Variables.http_port)) + config_content = config_content.replace("$STABLE_PORT", str(self._server_stable.Variables.http_port)) + ts.Disk.File(ts.Variables.CONFIGDIR + f"/{config_filename}", id="cookie_config") + ts.Disk.cookie_config.WriteOn(config_content) + + # Configure remap rule with cookie_remap plugin + ts.Disk.remap_config.AddLine( + 'map http://example.com http://shouldnothit.com ' + f'@plugin=cookie_remap.so @pparam=config/{config_filename}') + + def _setupClient(self): + """Setup the client for the test.""" + enabled_str = "enabled" if self.disable_pristine_host_hdr else "disabled" + tr = Test.AddTestRun(f'Test cookie bucket routing with disable_pristine_host_hdr {enabled_str}') + + p = tr.AddVerifierClientProcess(f'client_{self.test_id}', self.replay_file, http_ports=[self._ts.Variables.port]) + p.StartBefore(self._dns) + p.StartBefore(self._ts) + p.StartBefore(self._server_canary) + p.StartBefore(self._server_stable) + + +# Execute the test +TestUpdateHostHeader(disable_pristine_host_hdr=True) +TestUpdateHostHeader(disable_pristine_host_hdr=False) diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml new file mode 100644 index 00000000000..fb0ffe9ae6d --- /dev/null +++ b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml @@ -0,0 +1,80 @@ +# 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" + +sessions: + +# Transaction 1: Cookie in bucket - should route to canary but Host header should be pristine. +- transactions: + + - client-request: + method: GET + url: /app/test + version: '1.1' + headers: + fields: + - [ Host, example.com ] + - [ Cookie, "SessionID=333" ] + - [ uuid, 1 ] + + # Verify the proxy request has the pristine Host header. + proxy-request: + headers: + fields: + - [ Host, { value: example.com, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "0" ] + - [ Connection, close ] + + proxy-response: + status: 200 + +# Transaction 2: No SessionID cookie - should route to stable with pristine Host header. +- transactions: + + - client-request: + method: GET + url: /app/test + version: '1.1' + headers: + fields: + - [ Host, example.com ] + - [ uuid, 2 ] + + # Verify the proxy request has the pristine (non-stable.com) Host header. + proxy-request: + headers: + fields: + - [ Host, { value: example.com, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "0" ] + - [ Connection, close ] + + proxy-response: + status: 200 + diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml new file mode 100644 index 00000000000..c9377d23808 --- /dev/null +++ b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml @@ -0,0 +1,82 @@ +# 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" + +sessions: + +# Transaction 1: Cookie in bucket - should route to canary server with Host header updated. +- transactions: + + - client-request: + method: GET + url: /app/test + version: '1.1' + headers: + fields: + - [ Host, example.com ] + - [ Cookie, "SessionID=333" ] + - [ uuid, 1 ] + + # Verify the proxy request has the non-pristine Host header. + proxy-request: + headers: + fields: + - [ Host, { value: "canary.com", as: contains } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "0" ] + - [ Connection, close ] + + proxy-response: + status: 200 + +# Transaction 2: No SessionID cookie - should route to stable server (else path) +# without Host header updated. Pristine is never disabled for the else path. +- transactions: + + - client-request: + method: GET + url: /app/test + version: '1.1' + headers: + fields: + - [ Host, example.com ] + - [ uuid, 2 ] + + # Verify the proxy request has NOT updated the Host header (should still be + # pristine). + proxy-request: + headers: + fields: + - [ Host, { value: "example.com", as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "0" ] + - [ Connection, close ] + + proxy-response: + status: 200 +