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
44 changes: 44 additions & 0 deletions doc/admin-guide/plugins/cookie_remap.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Cookie Based Routing Inside TrafficServer Using cookie_remap
* :ref:`status: HTTP status-code <status-http-status-code>`
* :ref:`else: url [optional] <else-url-optional>`
* :ref:`connector: and <connector-and>`
* :ref:`disable_pristine_host_hdr: true|false [optional] <disable-pristine-host-hdr>`

* :ref:`Reserved path expressions <reserved-path-expressions>`

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 38 additions & 4 deletions plugins/experimental/cookie_remap/cookie_remap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,18 @@ class op
else_status = static_cast<TSHttpStatus>(atoi(s.c_str()));
}

void
setDisablePristineHostHdr(bool val)
{
disable_pristine_host_hdr = val;
}

bool
getDisablePristineHostHdr() const
{
return disable_pristine_host_hdr;
}

void
printOp() const
{
Expand All @@ -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
Expand Down Expand Up @@ -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 = "";
Expand All @@ -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<std::string, std::string>;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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

Loading