Skip to content

Commit e6692b2

Browse files
committed
cookie_remap: disable_pristine_host_hdr
This adds an optional disable_pristine_host_hdr configuration parameter to cookie_remap that, when enabled, disables the proxy.config.url_remap.pristine_host_hdr setting for matched transactions. This allows the Host header to be updated to match the hostname in the sendto URL, which is useful when downstream routing (such as parent proxies or origin server selection) depends on the Host header value. The feature is off by default to preserve backward compatibility and only affects the sendto path, while the else path continues to use the configured pristine host header setting.
1 parent 577cbed commit e6692b2

File tree

7 files changed

+390
-4
lines changed

7 files changed

+390
-4
lines changed

doc/admin-guide/plugins/cookie_remap.en.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Cookie Based Routing Inside TrafficServer Using cookie_remap
4141
* :ref:`status: HTTP status-code <status-http-status-code>`
4242
* :ref:`else: url [optional] <else-url-optional>`
4343
* :ref:`connector: and <connector-and>`
44+
* :ref:`disable_pristine_host_hdr: true|false [optional] <disable-pristine-host-hdr>`
4445

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

@@ -216,6 +217,22 @@ connector: and
216217

217218
'and' is the only supported connector
218219

220+
.. _disable-pristine-host-hdr:
221+
222+
disable_pristine_host_hdr: true|false [optional]
223+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
224+
225+
When set to ``true``, disables the ``proxy.config.url_remap.pristine_host_hdr``
226+
configuration for the matched transaction, allowing the Host header to be
227+
updated to match the hostname in the ``sendto`` URL. This is useful when
228+
downstream routing (such as a parent proxy or origin server selection) depends
229+
on the Host header value matching the remapped destination. The default value
230+
is ``false``, which preserves the pristine host header behavior.
231+
232+
This option only affects the successful match (``sendto``) path. The ``else``
233+
path will continue to use the configured pristine host header setting (typically
234+
enabled in production environments).
235+
219236
.. _reserved-path-expressions:
220237

221238
Reserved path expressions
@@ -389,6 +406,33 @@ An example configuration file
389406
sendto: http://cnn.com/$1
390407
else: http://yahoo.com
391408
409+
Example with Pristine Host Header Control
410+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
411+
412+
This example demonstrates using ``disable_pristine_host_hdr`` to route a
413+
percentage of users to a canary server in an environment where
414+
``proxy.config.url_remap.pristine_host_hdr`` is normally enabled. When the
415+
cookie bucket matches, pristine host header is disabled for that transaction,
416+
allowing the Host header to be updated to match the remapped destination::
417+
418+
op:
419+
cookie: SessionID
420+
operation: bucket
421+
bucket: 1/10
422+
sendto: https://canary.example.com/app/$unmatched_path
423+
disable_pristine_host_hdr: true
424+
else: https://stable.example.com/app/$unmatched_path
425+
426+
In this configuration:
427+
428+
* 10% of users (based on the SessionID cookie) are sent to the canary server.
429+
* For canary traffic, pristine host header is disabled, so the Host header is
430+
updated to ``canary.example.com``.
431+
* For regular traffic (else path), pristine host header remains enabled, so the
432+
Host header stays as the original client value.
433+
* This allows downstream routing (parent proxies or origin servers) to direct
434+
canary traffic correctly based on the updated Host header.
435+
392436
.. _debugging-things:
393437

394438
Debugging things

plugins/experimental/cookie_remap/cookie_remap.cc

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,18 @@ class op
502502
else_status = static_cast<TSHttpStatus>(atoi(s.c_str()));
503503
}
504504

505+
void
506+
setDisablePristineHostHdr(bool val)
507+
{
508+
disable_pristine_host_hdr = val;
509+
}
510+
511+
bool
512+
getDisablePristineHostHdr() const
513+
{
514+
return disable_pristine_host_hdr;
515+
}
516+
505517
void
506518
printOp() const
507519
{
@@ -515,10 +527,14 @@ class op
515527
if (else_sendto.size() > 0) {
516528
Dbg(dbg_ctl, "else: %s", else_sendto.c_str());
517529
}
530+
if (disable_pristine_host_hdr) {
531+
Dbg(dbg_ctl, "disable_pristine_host_hdr: true");
532+
}
518533
}
519534

520535
bool
521-
process(CookieJar &jar, std::string &dest, TSHttpStatus &retstat, TSRemapRequestInfo *rri, UrlComponents &req_url) const
536+
process(CookieJar &jar, std::string &dest, TSHttpStatus &retstat, TSRemapRequestInfo *rri, UrlComponents &req_url,
537+
bool &used_sendto) const
522538
{
523539
if (sendto == "") {
524540
return false; // guessing every operation must have a
@@ -774,12 +790,14 @@ class op
774790
if (status > 0) {
775791
retstat = status;
776792
}
793+
used_sendto = true; // We took the sendto path
777794
return true;
778795
} else if (else_sendto.size() > 0 && retval == 0) {
779796
dest = else_sendto;
780797
if (else_status > 0) {
781798
retstat = else_status;
782799
}
800+
used_sendto = false; // We took the else path
783801
return true;
784802
} else {
785803
dest = "";
@@ -791,8 +809,9 @@ class op
791809
SubOpQueue subops{};
792810
std::string sendto{""};
793811
std::string else_sendto{""};
794-
TSHttpStatus status = TS_HTTP_STATUS_NONE;
795-
TSHttpStatus else_status = TS_HTTP_STATUS_NONE;
812+
TSHttpStatus status = TS_HTTP_STATUS_NONE;
813+
TSHttpStatus else_status = TS_HTTP_STATUS_NONE;
814+
bool disable_pristine_host_hdr = false;
796815
};
797816

798817
using StringPair = std::pair<std::string, std::string>;
@@ -831,6 +850,10 @@ build_op(op &o, OpMap const &q)
831850
o.setStatus(val);
832851
}
833852

853+
if (key == "disable_pristine_host_hdr") {
854+
o.setDisablePristineHostHdr(val == "true" || val == "1" || val == "yes");
855+
}
856+
834857
if (key == "operation") {
835858
sub->setOperation(val);
836859
}
@@ -1183,7 +1206,8 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
11831206

11841207
for (auto &op : *ops) {
11851208
Dbg(dbg_ctl, ">>> processing new operation");
1186-
if (op->process(jar, rewrite_to, status, rri, req_url)) {
1209+
bool used_sendto = false;
1210+
if (op->process(jar, rewrite_to, status, rri, req_url, used_sendto)) {
11871211
cr_substitutions(rewrite_to, req_url);
11881212

11891213
size_t pos = 7; // 7 because we want to ignore the // in
@@ -1244,6 +1268,16 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
12441268
TSError("can't parse substituted URL string");
12451269
goto error;
12461270
} else {
1271+
// Disable pristine host header if configured to do so and we took the
1272+
// sendto path. This allows the Host header to be updated to match the
1273+
// remapped destination. The else path (i.e., the non-sendto one)
1274+
// always preserves the pristine host header configuration, whether
1275+
// enabled or disabled.
1276+
if (op->getDisablePristineHostHdr() && used_sendto) {
1277+
Dbg(dbg_ctl, "Disabling pristine_host_hdr for this transaction (sendto path)");
1278+
TSHttpTxnConfigIntSet(txnp, TS_CONFIG_URL_REMAP_PRISTINE_HOST_HDR, 0);
1279+
}
1280+
12471281
if (field != nullptr) {
12481282
TSHandleMLocRelease(rri->requestBufp, rri->requestHdrp, field);
12491283
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Test configuration for update_host_header functionality
2+
# Routes cookie bucket traffic to canary server WITHOUT updating Host header
3+
4+
op:
5+
cookie: SessionID
6+
operation: bucket
7+
bucket: 30/100
8+
sendto: http://canary.com:$CANARY_PORT/app/test
9+
else: http://stable.com:$STABLE_PORT/app/test
10+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Test configuration for disable_pristine_host_hdr functionality
2+
# Routes cookie bucket traffic to canary server with pristine host header disabled
3+
4+
op:
5+
cookie: SessionID
6+
operation: bucket
7+
bucket: 30/100
8+
sendto: http://canary.com:$CANARY_PORT/app/test
9+
disable_pristine_host_hdr: true
10+
else: http://stable.com:$STABLE_PORT/app/test
11+
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'''
2+
Verify cookie_remap plugin's disable_pristine_host_hdr functionality.
3+
'''
4+
# Licensed to the Apache Software Foundation (ASF) under one
5+
# or more contributor license agreements. See the NOTICE file
6+
# distributed with this work for additional information
7+
# regarding copyright ownership. The ASF licenses this file
8+
# to you under the Apache License, Version 2.0 (the
9+
# "License"); you may not use this file except in compliance
10+
# with the License. You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
import os
21+
22+
Test.Summary = '''
23+
Test cookie_remap plugin's disable_pristine_host_hdr functionality. Verifies that
24+
when disable_pristine_host_hdr is set to true, the pristine host header setting
25+
is disabled for the matched transaction, allowing the Host header to be updated
26+
to match the remapped destination.
27+
'''
28+
Test.SkipUnless(Condition.PluginExists('cookie_remap.so'))
29+
Test.ContinueOnFail = True
30+
31+
32+
class TestUpdateHostHeader:
33+
"""
34+
Test the disable_pristine_host_hdr feature of cookie_remap plugin.
35+
36+
This test verifies that:
37+
1. Cookie bucket routing works correctly
38+
2. When disable_pristine_host_hdr is enabled, the Host header is updated
39+
to match the sendto URL destination
40+
3. When disable_pristine_host_hdr is not set, the Host header remains as
41+
the original client value
42+
"""
43+
44+
# Counter for unique process names across multiple test instances
45+
test_counter: int = 0
46+
47+
def __init__(self, disable_pristine_host_hdr=True):
48+
"""Initialize the test by setting up servers and ATS configuration.
49+
:param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr
50+
in the cookie_remap configuration.
51+
"""
52+
self.test_id = TestUpdateHostHeader.test_counter
53+
TestUpdateHostHeader.test_counter += 1
54+
self.disable_pristine_host_hdr = disable_pristine_host_hdr
55+
self.replay_file = f'disable_pristine_host_hdr_{"true" if disable_pristine_host_hdr else "false"}.replay.yaml'
56+
self._setupDns()
57+
self._setupServers()
58+
self._setupTS(disable_pristine_host_hdr)
59+
self._setupClient()
60+
61+
def _setupDns(self):
62+
"""Configure the DNS server."""
63+
self._dns = Test.MakeDNServer(f"dns_{self.test_id}", default='127.0.0.1')
64+
65+
def _setupServers(self):
66+
"""
67+
Configure the origin servers using proxy-verifier.
68+
69+
Creates two servers to simulate canary and stable environments.
70+
"""
71+
self._server_canary = Test.MakeVerifierServerProcess(f"server_canary_{self.test_id}", self.replay_file)
72+
expected_host = 'canary.com' if self.disable_pristine_host_hdr else 'example.com'
73+
self._server_canary.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}')
74+
75+
self._server_stable = Test.MakeVerifierServerProcess(f"server_stable_{self.test_id}", self.replay_file)
76+
# The else path always preserves pristine host header (example.com)
77+
expected_host = 'example.com'
78+
self._server_stable.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}')
79+
80+
def _setupTS(self, disable_pristine_host_hdr):
81+
"""Configure Traffic Server with cookie_remap plugin.
82+
:param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr in cookie_remap.
83+
"""
84+
ts = Test.MakeATSProcess(f"ts_{self.test_id}")
85+
self._ts = ts
86+
87+
# Enable debug logging for cookie_remap and enable pristine_host_hdr
88+
# (simulating production environment)
89+
ts.Disk.records_config.update(
90+
{
91+
'proxy.config.diags.debug.enabled': 1,
92+
'proxy.config.diags.debug.tags': 'cookie_remap|http',
93+
'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}",
94+
'proxy.config.dns.resolv_conf': 'NULL',
95+
'proxy.config.url_remap.pristine_host_hdr': 1,
96+
})
97+
98+
# Read and configure the cookie_remap configuration file
99+
config_filname = f'disable_pristine_host_hdr_config_{"true" if disable_pristine_host_hdr else "false"}.txt'
100+
config_path = os.path.join(Test.TestDirectory, f"configs/{config_filname}")
101+
with open(config_path, 'r') as config_file:
102+
config_content = config_file.read()
103+
config_content = config_content.replace("$CANARY_PORT", str(self._server_canary.Variables.http_port))
104+
config_content = config_content.replace("$STABLE_PORT", str(self._server_stable.Variables.http_port))
105+
ts.Disk.File(ts.Variables.CONFIGDIR + f"/{config_filname}", id="cookie_config")
106+
ts.Disk.cookie_config.WriteOn(config_content)
107+
108+
# Configure remap rule with cookie_remap plugin
109+
ts.Disk.remap_config.AddLine(
110+
'map http://example.com http://shouldnothit.com '
111+
f'@plugin=cookie_remap.so @pparam=config/{config_filname}')
112+
113+
def _setupClient(self):
114+
"""Setup the client for the test."""
115+
enabled_str = "enabled" if self.disable_pristine_host_hdr else "disabled"
116+
tr = Test.AddTestRun(f'Test cookie bucket routing with disable_pristine_host_hdr {enabled_str}')
117+
118+
p = tr.AddVerifierClientProcess(f'client_{self.test_id}', self.replay_file, http_ports=[self._ts.Variables.port])
119+
p.StartBefore(self._dns)
120+
p.StartBefore(self._ts)
121+
p.StartBefore(self._server_canary)
122+
p.StartBefore(self._server_stable)
123+
124+
125+
# Execute the test
126+
TestUpdateHostHeader(disable_pristine_host_hdr=True)
127+
TestUpdateHostHeader(disable_pristine_host_hdr=False)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
meta:
18+
version: "1.0"
19+
20+
sessions:
21+
22+
# Transaction 1: Cookie in bucket - should route to canary but Host header should NOT be updated
23+
- transactions:
24+
25+
- client-request:
26+
method: GET
27+
url: /app/test
28+
version: '1.1'
29+
headers:
30+
fields:
31+
- [ Host, example.com ]
32+
- [ Cookie, "SessionID=333" ]
33+
- [ uuid, 1 ]
34+
35+
# Verify the proxy request has NOT updated the Host header (should still be example.com)
36+
proxy-request:
37+
headers:
38+
fields:
39+
- [ Host, { value: example.com, as: equal } ]
40+
41+
server-response:
42+
status: 200
43+
reason: OK
44+
headers:
45+
fields:
46+
- [ Content-Length, "0" ]
47+
- [ Connection, close ]
48+
49+
proxy-response:
50+
status: 200
51+
52+
# Transaction 2: No SessionID cookie - should route to stable but Host header should NOT be updated
53+
- transactions:
54+
55+
- client-request:
56+
method: GET
57+
url: /app/test
58+
version: '1.1'
59+
headers:
60+
fields:
61+
- [ Host, example.com ]
62+
- [ uuid, 2 ]
63+
64+
# Verify the proxy request has NOT updated the Host header (should still be example.com)
65+
proxy-request:
66+
headers:
67+
fields:
68+
- [ Host, { value: example.com, as: equal } ]
69+
70+
server-response:
71+
status: 200
72+
reason: OK
73+
headers:
74+
fields:
75+
- [ Content-Length, "0" ]
76+
- [ Connection, close ]
77+
78+
proxy-response:
79+
status: 200
80+

0 commit comments

Comments
 (0)