You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ma...@apache.org on 2026/09/16 22:18:18 UTC
(trafficserver) branch master updated: Fix strncasecmp checks that accepted a prefix as equal (#13697)
This is an automated email from the ASF dual-hosted git repository.
masaori335 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 13ef6a7618 Fix strncasecmp checks that accepted a prefix as equal (#13697)
13ef6a7618 is described below
commit 13ef6a7618bda4c36835aab291de95d02e86f2ed
Author: Masaori Koshiba <ma...@apache.org>
AuthorDate: Thu Sep 17 07:18:11 2026 +0900
Fix strncasecmp checks that accepted a prefix as equal (#13697)
* Fix strncasecmp checks that accepted a prefix as equal
strncasecmp(a, b, n) is an equality test only when the lengths are
compared first. Without that, any input that is a prefix of the
expected literal matches: `Vary: Accept` suppressed the
`Vary: Accept-Encoding` compress adds, `escape: jsonX` loaded as JSON,
and isTrue("1abc") was true. Replace these with ts::iequals, which
short-circuits on a length mismatch.
Prefix and suffix matches keep using strncasecmp. StringCompare.h joins
the installed tsutil headers because plugins now include it.
* Address Copilot's comment
* Add an autest for compress appending to an existing Vary header
The existing compress tests only ever see an origin `Vary: Accept-Encoding`,
so they still pass if the prefix-match fix in vary_header() is reverted.
Verified: this test fails with the old strncasecmp, reporting
`vary` as "Accept" instead of "Accept, Accept-Encoding".
---
plugins/cache_promote/configs.cc | 5 +-
plugins/cachekey/configs.cc | 3 +-
plugins/compress/compress.cc | 3 +-
plugins/escalate/escalate.cc | 7 +-
plugins/experimental/access_control/config.cc | 3 +-
plugins/experimental/cache_fill/configs.h | 3 +-
.../custom_redirect/custom_redirect.cc | 5 +-
plugins/lua/ts_lua.cc | 3 +-
plugins/prefetch/configs.cc | 23 ++--
plugins/regex_revalidate/regex_revalidate.cc | 9 +-
plugins/stats_over_http/stats_over_http.cc | 19 ++--
src/iocore/cache/HttpTransactCache.cc | 5 +-
src/proxy/ControlBase.cc | 6 +-
src/proxy/logging/YamlLogConfig.cc | 5 +-
src/proxy/logging/YamlLogConfigDecoders.cc | 6 +-
src/tsutil/CMakeLists.txt | 2 +
src/tsutil/unit_tests/test_StringCompare.cc | 71 +++++++++++++
.../compress/compress-vary-accept.config | 4 +
.../compress/compress-vary-accept.test.py | 27 +++++
.../replay/compress-vary-accept.replay.yaml | 117 +++++++++++++++++++++
20 files changed, 280 insertions(+), 46 deletions(-)
diff --git a/plugins/cache_promote/configs.cc b/plugins/cache_promote/configs.cc
index a9d8b96092..68bd8e41bb 100644
--- a/plugins/cache_promote/configs.cc
+++ b/plugins/cache_promote/configs.cc
@@ -20,6 +20,7 @@
#include "configs.h"
#include "lru_policy.h"
#include "chance_policy.h"
+#include "tsutil/StringCompare.h"
//////////////////////////////////////////////////////////////////////////////////////////////
// ToDo: It's ugly that this is a "global" options list, clearly each policy should be able
@@ -58,9 +59,9 @@ PromotionConfig::factory(int argc, char *argv[])
if (opt == -1) {
break;
} else if (opt == 'p') {
- if (0 == strncasecmp(optarg, "chance", 6)) {
+ if (ts::iequals(optarg, "chance")) {
_policy = new ChancePolicy();
- } else if (0 == strncasecmp(optarg, "lru", 3)) {
+ } else if (ts::iequals(optarg, "lru")) {
_policy = new LRUPolicy();
} else {
TSError("[%s] Unknown policy --policy=%s", PLUGIN_NAME, optarg);
diff --git a/plugins/cachekey/configs.cc b/plugins/cachekey/configs.cc
index d08d7a2094..55db9b699c 100644
--- a/plugins/cachekey/configs.cc
+++ b/plugins/cachekey/configs.cc
@@ -28,6 +28,7 @@
#include <cstring> /* strlen() */
#include "configs.h"
+#include "tsutil/StringCompare.h"
template <typename ContainerType>
static void
@@ -44,7 +45,7 @@ commaSeparateString(ContainerType &c, const String &input)
static bool
isTrue(const char *arg)
{
- return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
+ return (nullptr == arg || ts::iequals("true", arg) || ts::iequals("1", arg) || ts::iequals("yes", arg));
}
void
diff --git a/plugins/compress/compress.cc b/plugins/compress/compress.cc
index 3994be9b72..1cae68a992 100644
--- a/plugins/compress/compress.cc
+++ b/plugins/compress/compress.cc
@@ -29,6 +29,7 @@
#include <tsutil/PostScript.h>
#include <tsutil/Metrics.h>
+#include <tsutil/StringCompare.h>
#include "ts/ts.h"
#include "tscore/ink_defs.h"
@@ -250,7 +251,7 @@ vary_header(TSMBuffer bufp, TSMLoc hdr_loc)
count = TSMimeHdrFieldValuesCount(bufp, hdr_loc, ce_loc);
for (idx = 0; idx < count; idx++) {
const char *value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, ce_loc, idx, &len);
- if (len && strncasecmp("Accept-Encoding", value, len) == 0) {
+ if (len > 0 && ts::iequals("Accept-Encoding", std::string_view{value, static_cast<std::string_view::size_type>(len)})) {
// Bail, Vary: Accept-Encoding already sent from origin
TSHandleMLocRelease(bufp, hdr_loc, ce_loc);
return TS_SUCCESS;
diff --git a/plugins/escalate/escalate.cc b/plugins/escalate/escalate.cc
index 67b6d77518..56c6ad8f80 100644
--- a/plugins/escalate/escalate.cc
+++ b/plugins/escalate/escalate.cc
@@ -35,6 +35,7 @@
#include "swoc/IPEndpoint.h"
#include "swoc/TextView.h"
+#include "tsutil/StringCompare.h"
// Constants and some declarations
@@ -258,11 +259,11 @@ TSRemapNewInstance(int argc, char *argv[], void **instance, char *errbuf, int er
char *sep, *token, *save;
// Ugly, but we set the precedence before with non-command line parsing of args
- if (0 == strncasecmp(argv[i], "--pristine", 10)) {
+ if (ts::iequals(argv[i], "--pristine")) {
es->use_pristine = true;
- } else if (0 == strncasecmp(argv[i], "--no-redirect-header", 20)) {
+ } else if (ts::iequals(argv[i], "--no-redirect-header")) {
es->add_redirect_header = false;
- } else if (0 == strncasecmp(argv[i], "--escalate-non-get-methods", 26)) {
+ } else if (ts::iequals(argv[i], "--escalate-non-get-methods")) {
es->escalate_non_get_methods = true;
} else {
// Each token should be a status code then a URL, separated by ':'.
diff --git a/plugins/experimental/access_control/config.cc b/plugins/experimental/access_control/config.cc
index 30b65077b3..d2f2e186ad 100644
--- a/plugins/experimental/access_control/config.cc
+++ b/plugins/experimental/access_control/config.cc
@@ -28,11 +28,12 @@
#include "common.h"
#include "config.h"
+#include "tsutil/StringCompare.h"
static bool
isTrue(const char *arg)
{
- return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
+ return (nullptr == arg || ts::iequals("true", arg) || ts::iequals("1", arg) || ts::iequals("yes", arg));
}
/**
diff --git a/plugins/experimental/cache_fill/configs.h b/plugins/experimental/cache_fill/configs.h
index 81e31b05ab..489dc90c88 100644
--- a/plugins/experimental/cache_fill/configs.h
+++ b/plugins/experimental/cache_fill/configs.h
@@ -32,6 +32,7 @@
#include <sys/socket.h>
#include "rules.h"
+#include "tsutil/StringCompare.h"
///////////////////////////////////////////////////////////////////////////
// This holds one complete background fetch rule
@@ -73,7 +74,7 @@ public:
static bool
isTrue(const char *arg)
{
- return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
+ return (nullptr == arg || ts::iequals("true", arg) || ts::iequals("1", arg) || ts::iequals("yes", arg));
}
// This parses and populates the BgFetchRule linked list (_rules).
diff --git a/plugins/experimental/custom_redirect/custom_redirect.cc b/plugins/experimental/custom_redirect/custom_redirect.cc
index 84ee69e123..91949eab53 100644
--- a/plugins/experimental/custom_redirect/custom_redirect.cc
+++ b/plugins/experimental/custom_redirect/custom_redirect.cc
@@ -31,6 +31,7 @@
#include <arpa/inet.h>
#include <netdb.h>
#include <ts/ts.h>
+#include <tsutil/StringCompare.h>
#include <cstring>
#include <cstdlib>
@@ -63,8 +64,8 @@ handle_response(TSHttpTxn txnp, TSCont /* contp ATS_UNUSED */)
} else {
int method_len;
const char *method = TSHttpHdrMethodGet(req_bufp, req_loc, &method_len);
- if ((return_code == TS_HTTP_STATUS_NONE || return_code == status) &&
- ((strncasecmp(method, TS_HTTP_METHOD_GET, TS_HTTP_LEN_GET) == 0))) {
+ if ((return_code == TS_HTTP_STATUS_NONE || return_code == status) && method != nullptr &&
+ ts::iequals(std::string_view{method, static_cast<std::string_view::size_type>(method_len)}, TS_HTTP_METHOD_GET)) {
redirect_url_loc = TSMimeHdrFieldFind(resp_bufp, resp_loc, redirect_url_header, redirect_url_header_len);
if (redirect_url_loc) {
diff --git a/plugins/lua/ts_lua.cc b/plugins/lua/ts_lua.cc
index f4af729070..943a3e12d8 100644
--- a/plugins/lua/ts_lua.cc
+++ b/plugins/lua/ts_lua.cc
@@ -24,6 +24,7 @@
#include <pthread.h>
#include "ts_lua_util.h"
+#include "tsutil/StringCompare.h"
extern "C" {
#include "luajit.h"
@@ -242,7 +243,7 @@ lifecycleHandler(TSCont contp, TSEvent /* event ATS_UNUSED */, void *edata)
{
// ensure the message is for ts_lua
TSPluginMsg *const msgp = (TSPluginMsg *)edata;
- if (0 != strncasecmp(msgp->tag, TS_LUA_DEBUG_TAG, strlen(msgp->tag))) {
+ if (!ts::iequals(msgp->tag, TS_LUA_DEBUG_TAG)) {
return TS_EVENT_NONE;
}
diff --git a/plugins/prefetch/configs.cc b/plugins/prefetch/configs.cc
index 9e86a9a2bc..9263f787a3 100644
--- a/plugins/prefetch/configs.cc
+++ b/plugins/prefetch/configs.cc
@@ -21,14 +21,14 @@
* @brief Plugin configuration.
*/
-#include <charconv> /* std::from_chars() */
-#include <cstring> /* strlen() */
-#include <fstream> /* std::ifstream */
-#include <getopt.h> /* getopt_long() */
-#include <sstream> /* std::istringstream */
-#include <strings.h> /* strncasecmp() */
+#include <charconv> /* std::from_chars() */
+#include <cstring> /* strlen() */
+#include <fstream> /* std::ifstream */
+#include <getopt.h> /* getopt_long() */
+#include <sstream> /* std::istringstream */
#include "configs.h"
+#include "tsutil/StringCompare.h"
DbgCtl Bg_dbg_ctl{PLUGIN_NAME};
@@ -47,7 +47,7 @@ commaSeparateString(ContainerType &c, const String &input)
static bool
isTrue(const char *arg)
{
- return (0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
+ return (ts::iequals("true", arg) || ts::iequals("1", arg) || ts::iequals("yes", arg));
}
static String
@@ -66,13 +66,6 @@ fetchOverflowString(const EvalPolicy policy)
}
}
-static bool
-iequals(const StringView lhs, const StringView rhs)
-{
- return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(),
- [](const char a, const char b) { return tolower(a) == tolower(b); });
-}
-
bool
PrefetchConfig::setFetchOverflow(const char *optarg)
{
@@ -83,7 +76,7 @@ PrefetchConfig::setFetchOverflow(const char *optarg)
_fetchOverflow = EvalPolicy::Overflow32;
} else if (StringView("64") == optarg) {
_fetchOverflow = EvalPolicy::Overflow64;
- } else if (iequals("bignum", optarg)) {
+ } else if (ts::iequals("bignum", optarg)) {
_fetchOverflow = EvalPolicy::Bignum;
} else {
return false;
diff --git a/plugins/regex_revalidate/regex_revalidate.cc b/plugins/regex_revalidate/regex_revalidate.cc
index 11fc3720e1..ef45897188 100644
--- a/plugins/regex_revalidate/regex_revalidate.cc
+++ b/plugins/regex_revalidate/regex_revalidate.cc
@@ -34,6 +34,7 @@
#include <unistd.h>
#include "tsutil/Regex.h"
+#include "tsutil/StringCompare.h"
#define CONFIG_TMOUT 60000
#define FREE_TMOUT 300000
@@ -323,9 +324,9 @@ load_state(plugin_state_t *pstate, invalidate_t **ilist)
}
auto const type = matches[4];
- if (0 == strncasecmp(type.data(), RESULT_STALE, type.length())) {
+ if (ts::iequals(type, RESULT_STALE)) {
Dbg(dbg_ctl, "state: regex line set to result type %s: '%s'", RESULT_STALE, inv->regex_text);
- } else if (0 == strncasecmp(type.data(), RESULT_MISS, type.length())) {
+ } else if (ts::iequals(type, RESULT_MISS)) {
Dbg(dbg_ctl, "state: regex line set to result type %s: '%s'", RESULT_MISS, inv->regex_text);
inv->new_result = TS_CACHE_LOOKUP_MISS;
} else {
@@ -439,10 +440,10 @@ load_config(plugin_state_t *pstate, invalidate_t **ilist)
if (5 == rc) {
auto const type = matches[4];
- if (0 == strncasecmp(type.data(), RESULT_MISS, type.length())) {
+ if (ts::iequals(type, RESULT_MISS)) {
Dbg(dbg_ctl, "Regex line set to result type %s: '%s'", RESULT_MISS, i->regex_text);
i->new_result = TS_CACHE_LOOKUP_MISS;
- } else if (0 != strncasecmp(type.data(), RESULT_STALE, type.length())) {
+ } else if (!ts::iequals(type, RESULT_STALE)) {
Dbg(dbg_ctl, "Unknown regex line result type '%.*s', using default '%s' '%s'", (int)type.length(), type.data(),
RESULT_STALE, i->regex_text);
}
diff --git a/plugins/stats_over_http/stats_over_http.cc b/plugins/stats_over_http/stats_over_http.cc
index 954e894ffa..98d39f2eee 100644
--- a/plugins/stats_over_http/stats_over_http.cc
+++ b/plugins/stats_over_http/stats_over_http.cc
@@ -49,6 +49,7 @@
#include "swoc/TextView.h"
#include "tscore/ink_config.h"
#include <tsutil/ts_ip.h>
+#include <tsutil/StringCompare.h>
#if HAVE_BROTLI_ENCODE_H
#include <brotli/encode.h>
@@ -1151,21 +1152,27 @@ stats_origin(TSCont contp, TSEvent /* event ATS_UNUSED */, void *edata)
my_state->output_format = output_format_t::JSON_OUTPUT; // default to json output
// accept header exists, use it to determine response type
if (accept_field != TS_NULL_MLOC) {
- int len = -1;
- const char *str = TSMimeHdrFieldValueStringGet(reqp, hdr_loc, accept_field, -1, &len);
+ int len = -1;
+ const char *str = TSMimeHdrFieldValueStringGet(reqp, hdr_loc, accept_field, -1, &len);
+ std::string_view accept{};
+
+ if (str != nullptr && len > 0) {
+ accept = std::string_view{str, static_cast<std::string_view::size_type>(len)};
+ }
// Parse the Accept header, default to JSON output unless its another supported format
- if (!strncasecmp(str, "text/csv", len)) {
+ if (ts::iequals(accept, "text/csv")) {
Dbg(dbg_ctl, "Saw text/csv in accept header, sending CSV output.");
my_state->output_format = output_format_t::CSV_OUTPUT;
- } else if (!strncasecmp(str, "text/plain; version=0.0.4", len)) {
+ } else if (ts::iequals(accept, "text/plain; version=0.0.4")) {
Dbg(dbg_ctl, "Saw text/plain; version=0.0.4 in accept header, sending Prometheus output.");
my_state->output_format = output_format_t::PROMETHEUS_OUTPUT;
- } else if (!strncasecmp(str, "text/plain; version=2.0.0", len)) {
+ } else if (ts::iequals(accept, "text/plain; version=2.0.0")) {
Dbg(dbg_ctl, "Saw text/plain; version=2.0.0 in accept header, sending Prometheus v2 output.");
my_state->output_format = output_format_t::PROMETHEUS_V2_OUTPUT;
} else {
- Dbg(dbg_ctl, "Saw %.*s in accept header, defaulting to JSON output.", len, str);
+ Dbg(dbg_ctl, "Saw %.*s in accept header, defaulting to JSON output.", static_cast<int>(accept.size()),
+ accept.empty() ? "" : accept.data());
my_state->output_format = output_format_t::JSON_OUTPUT;
}
}
diff --git a/src/iocore/cache/HttpTransactCache.cc b/src/iocore/cache/HttpTransactCache.cc
index be8f0864d1..43f62e483d 100644
--- a/src/iocore/cache/HttpTransactCache.cc
+++ b/src/iocore/cache/HttpTransactCache.cc
@@ -30,6 +30,7 @@
#include "proxy/hdrs/HTTP.h"
#include "proxy/hdrs/HttpCompat.h"
#include "tscore/ink_time.h"
+#include "tsutil/StringCompare.h"
#include <ctime>
@@ -941,9 +942,7 @@ HttpTransactCache::calculate_quality_of_accept_encoding_match(MIMEField *accept_
} else {
// does this document have the identity encoding? //
for (c_value = c_values_list.head; c_value; c_value = c_value->next) {
- auto c_encoding = c_value->str;
- auto c_encoding_len = c_value->len;
- if ((c_encoding_len >= 8) && (strncasecmp(c_encoding, "identity", 8) == 0)) {
+ if (ts::iequals(std::string_view{c_value->str, c_value->len}, "identity")) {
is_identity_encoding = true;
break;
}
diff --git a/src/proxy/ControlBase.cc b/src/proxy/ControlBase.cc
index 564889183d..0cfbb4ac3f 100644
--- a/src/proxy/ControlBase.cc
+++ b/src/proxy/ControlBase.cc
@@ -34,6 +34,8 @@
#include "tscore/ink_defs.h"
#include "tscore/ink_time.h"
+#include "tsutil/StringCompare.h"
+
#include "proxy/hdrs/URL.h"
#include "tscore/Tokenizer.h"
#include "proxy/ControlBase.h"
@@ -661,9 +663,9 @@ InternalMod::make(char *value, const char **error)
{
InternalMod tmp;
- if (0 == strncasecmp("false", value, 5)) {
+ if (ts::iequals("false", value)) {
tmp.flag = false;
- } else if (0 == strncasecmp("true", value, 4)) {
+ } else if (ts::iequals("true", value)) {
tmp.flag = true;
} else {
*error = "Value must be true or false";
diff --git a/src/proxy/logging/YamlLogConfig.cc b/src/proxy/logging/YamlLogConfig.cc
index 99b376507e..b179988ceb 100644
--- a/src/proxy/logging/YamlLogConfig.cc
+++ b/src/proxy/logging/YamlLogConfig.cc
@@ -27,6 +27,7 @@
#include "proxy/logging/LogObject.h"
#include "tscore/EnumDescriptor.h"
+#include "tsutil/StringCompare.h"
#include <yaml-cpp/yaml.h>
#include <algorithm>
@@ -176,9 +177,9 @@ YamlLogConfig::decodeLogObject(const YAML::Node &node)
LogFileFormat file_type = LOG_FILE_ASCII; // default value
if (node["mode"]) {
std::string mode = node["mode"].as<std::string>();
- file_type = (0 == strncasecmp(mode.c_str(), "bin", 3) || (1 == mode.size() && mode[0] == 'b') ?
+ file_type = (ts::iequals(mode, "binary") || ts::iequals(mode, "bin") || ts::iequals(mode, "b") ?
LOG_FILE_BINARY :
- (0 == strcasecmp(mode.c_str(), "ascii_pipe") ? LOG_FILE_PIPE : LOG_FILE_ASCII));
+ (ts::iequals(mode, "ascii_pipe") ? LOG_FILE_PIPE : LOG_FILE_ASCII));
}
int obj_rolling_enabled = cfg->rolling_enabled;
diff --git a/src/proxy/logging/YamlLogConfigDecoders.cc b/src/proxy/logging/YamlLogConfigDecoders.cc
index 0c3b438c35..cbabf591d4 100644
--- a/src/proxy/logging/YamlLogConfigDecoders.cc
+++ b/src/proxy/logging/YamlLogConfigDecoders.cc
@@ -24,6 +24,8 @@
#include "proxy/logging/LogConfig.h"
#include "proxy/logging/LogObject.h"
+#include "tsutil/StringCompare.h"
+
#include <yaml-cpp/yaml.h>
#include <algorithm>
#include <memory>
@@ -73,9 +75,9 @@ convert<std::unique_ptr<LogFormat>>::decode(const Node &node, std::unique_ptr<Lo
LogEscapeType escape_type = LOG_ESCAPE_NONE; // default value
if (node["escape"]) {
std::string escape = node["escape"].as<std::string>();
- if (!strncasecmp(escape.c_str(), "json", 4)) {
+ if (ts::iequals(escape, "json")) {
escape_type = LOG_ESCAPE_JSON;
- } else if (!strncasecmp(escape.c_str(), "none", 4)) {
+ } else if (ts::iequals(escape, "none")) {
escape_type = LOG_ESCAPE_NONE;
} else {
throw YAML::ParserException(node.Mark(), "invalid 'escape' argument '" + escape + "' for format name '" + name + "'");
diff --git a/src/tsutil/CMakeLists.txt b/src/tsutil/CMakeLists.txt
index 56dd2ee8ed..b6f6224f9b 100644
--- a/src/tsutil/CMakeLists.txt
+++ b/src/tsutil/CMakeLists.txt
@@ -30,6 +30,7 @@ set(TSUTIL_PUBLIC_HEADERS
${PROJECT_SOURCE_DIR}/include/tsutil/LocalBuffer.h
${PROJECT_SOURCE_DIR}/include/tsutil/PostScript.h
${PROJECT_SOURCE_DIR}/include/tsutil/Strerror.h
+ ${PROJECT_SOURCE_DIR}/include/tsutil/StringCompare.h
${PROJECT_SOURCE_DIR}/include/tsutil/StringConvert.h
${PROJECT_SOURCE_DIR}/include/tsutil/Regex.h
${PROJECT_SOURCE_DIR}/include/tsutil/TsMutex.h
@@ -81,6 +82,7 @@ if(BUILD_TESTING)
unit_tests/test_Metrics.cc
unit_tests/test_PostScript.cc
unit_tests/test_Strerror.cc
+ unit_tests/test_StringCompare.cc
unit_tests/test_StringConvert.cc
unit_tests/test_Regex.cc
unit_tests/test_ts_meta.cc
diff --git a/src/tsutil/unit_tests/test_StringCompare.cc b/src/tsutil/unit_tests/test_StringCompare.cc
new file mode 100644
index 0000000000..53ff2696fd
--- /dev/null
+++ b/src/tsutil/unit_tests/test_StringCompare.cc
@@ -0,0 +1,71 @@
+/** @file
+
+ Unit tests for StringCompare.h.
+
+ @section license License
+
+ 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.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <string_view>
+#include <tsutil/StringCompare.h>
+
+TEST_CASE("iequals", "[STE]")
+{
+ SECTION("equal")
+ {
+ REQUIRE(ts::iequals("Accept-Encoding", "Accept-Encoding"));
+ REQUIRE(ts::iequals("", ""));
+ }
+
+ SECTION("differing case")
+ {
+ REQUIRE(ts::iequals("ACCEPT-ENCODING", "accept-encoding"));
+ REQUIRE(ts::iequals("Accept-Encoding", "aCCePt-eNCoDiNG"));
+ }
+
+ SECTION("prefix is not equal")
+ {
+ REQUIRE_FALSE(ts::iequals("Accept", "Accept-Encoding"));
+ REQUIRE_FALSE(ts::iequals("Accept-Encoding", "Accept"));
+ REQUIRE_FALSE(ts::iequals("", "Accept-Encoding"));
+ REQUIRE_FALSE(ts::iequals("Accept-Encoding", ""));
+ }
+
+ SECTION("suffix is not equal")
+ {
+ REQUIRE_FALSE(ts::iequals("Encoding", "Accept-Encoding"));
+ REQUIRE_FALSE(ts::iequals("Accept-Encoding", "Encoding"));
+ }
+
+ SECTION("same length, differing content")
+ {
+ REQUIRE_FALSE(ts::iequals("gzip", "zstd"));
+ REQUIRE_FALSE(ts::iequals("Accept-Encoding", "Accept-Language"));
+ }
+
+ SECTION("length is honored over NUL termination")
+ {
+ // Callers build views from a pointer and a length; the terminator must not decide the result.
+ static constexpr char raw[] = "Accept-Encoding-and-more";
+
+ REQUIRE(ts::iequals(std::string_view{raw, 15}, "Accept-Encoding"));
+ REQUIRE_FALSE(ts::iequals(std::string_view{raw, 16}, "Accept-Encoding"));
+ }
+}
diff --git a/tests/gold_tests/pluginTest/compress/compress-vary-accept.config b/tests/gold_tests/pluginTest/compress/compress-vary-accept.config
new file mode 100644
index 0000000000..34c9f1be12
--- /dev/null
+++ b/tests/gold_tests/pluginTest/compress/compress-vary-accept.config
@@ -0,0 +1,4 @@
+cache false
+compressible-content-type text/*
+supported-algorithms gzip
+minimum-content-length 0
diff --git a/tests/gold_tests/pluginTest/compress/compress-vary-accept.test.py b/tests/gold_tests/pluginTest/compress/compress-vary-accept.test.py
new file mode 100644
index 0000000000..02c95f5bdb
--- /dev/null
+++ b/tests/gold_tests/pluginTest/compress/compress-vary-accept.test.py
@@ -0,0 +1,27 @@
+'''
+Verify compress appends Accept-Encoding to an existing origin Vary header.
+'''
+# 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 = '''
+Verify compress appends Accept-Encoding to an origin Vary header whose value is
+a prefix of Accept-Encoding.
+'''
+
+Test.SkipUnless(Condition.PluginExists('compress.so'))
+
+Test.ATSReplayTest(replay_file="replay/compress-vary-accept.replay.yaml")
diff --git a/tests/gold_tests/pluginTest/compress/replay/compress-vary-accept.replay.yaml b/tests/gold_tests/pluginTest/compress/replay/compress-vary-accept.replay.yaml
new file mode 100644
index 0000000000..a40d17a95a
--- /dev/null
+++ b/tests/gold_tests/pluginTest/compress/replay/compress-vary-accept.replay.yaml
@@ -0,0 +1,117 @@
+# 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"
+
+ blocks:
+ - compressible-body: &compressible-body
+ content:
+ encoding: plain
+ size: 1024
+
+autest:
+ description: 'Verify compress appends Accept-Encoding to an existing origin Vary header'
+
+ server:
+ name: 'server'
+
+ client:
+ name: 'client'
+
+ ats:
+ name: 'ts'
+
+ copy_to_config_dir:
+ - 'compress-vary-accept.config'
+
+ records_config:
+ proxy.config.diags.debug.enabled: 1
+ proxy.config.diags.debug.tags: 'compress'
+
+ # The pparam resolves relative to the ATS config dir, where
+ # copy_to_config_dir places it.
+ remap_config:
+ - from: "http://compress.example/"
+ to: "http://127.0.0.1:{SERVER_HTTP_PORT}/"
+ plugins:
+ - name: 'compress.so'
+ args: ['compress-vary-accept.config']
+
+sessions:
+- transactions:
+
+ #############################################################################
+ # An origin Vary value that is a prefix of "Accept-Encoding" must not be
+ # mistaken for it: compress has to append Accept-Encoding alongside Accept.
+ #############################################################################
+ - client-request:
+ method: "GET"
+ version: "1.1"
+ url: /vary-accept
+ headers:
+ fields:
+ - [ Host, compress.example ]
+ - [ Accept-Encoding, gzip ]
+ - [ uuid, vary-accept ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, text/javascript ]
+ - [ Vary, Accept ]
+ - [ Content-Length, 1024 ]
+ <<: *compressible-body
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Encoding, { value: gzip, as: equal } ]
+ - [ Vary, { value: "Accept, Accept-Encoding", as: equal } ]
+
+ #############################################################################
+ # Control: an origin that already varies on Accept-Encoding is left alone,
+ # so the value is not duplicated.
+ #############################################################################
+ - client-request:
+ method: "GET"
+ version: "1.1"
+ url: /vary-accept-encoding
+ headers:
+ fields:
+ - [ Host, compress.example ]
+ - [ Accept-Encoding, gzip ]
+ - [ uuid, vary-accept-encoding ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, text/javascript ]
+ - [ Vary, Accept-Encoding ]
+ - [ Content-Length, 1024 ]
+ <<: *compressible-body
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Encoding, { value: gzip, as: equal } ]
+ - [ Vary, { value: "Accept-Encoding", as: equal } ]