diff --git a/tests/gold_tests/pluginTest/client_packet_mark/client_packet_mark.test.py b/tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py similarity index 100% rename from tests/gold_tests/pluginTest/client_packet_mark/client_packet_mark.test.py rename to tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py diff --git a/tests/tools/plugins/CMakeLists.txt b/tests/tools/plugins/CMakeLists.txt index a658ba9aa24..701022dd27b 100644 --- a/tests/tools/plugins/CMakeLists.txt +++ b/tests/tools/plugins/CMakeLists.txt @@ -15,7 +15,7 @@ # ####################### -add_autest_plugin(client_packet_mark client_packet_mark.cc) +add_autest_plugin(client_packet_mark client_packet_mark.cc packet_mark_common.cc) add_autest_plugin(conf_remap_stripped conf_remap_stripped.cc) add_autest_plugin(continuations_verify continuations_verify.cc) add_autest_plugin(cont_schedule cont_schedule.cc) diff --git a/tests/tools/plugins/client_packet_mark.cc b/tests/tools/plugins/client_packet_mark.cc index 72acab439f9..6abd6aba88e 100644 --- a/tests/tools/plugins/client_packet_mark.cc +++ b/tests/tools/plugins/client_packet_mark.cc @@ -27,121 +27,31 @@ limitations under the License. */ -#include +#include "packet_mark_common.h" -extern "C" { -#include -} +#include -#include -#include -#include -#include -#include #include namespace { -constexpr std::string_view PLUGIN_NAME = "client_packet_mark"; -constexpr std::string_view MARK_HEADER = "X-Set-Mark"; -constexpr std::string_view ECHO_HEADER = "X-Client-Packet-Mark"; - -DbgCtl dbg_ctl{PLUGIN_NAME.data()}; - -/** Read a header field and interpret its value as a 32-bit unsigned quantity. - - Values are parsed with strtoul (base 0), so "0x0000000A" and "10" are both - accepted. Returns std::nullopt if the header is absent. */ -std::optional -get_uint_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header) -{ - TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), static_cast(header.length())); - if (field_loc == TS_NULL_MLOC) { - return std::nullopt; - } +constexpr char PLUGIN_NAME[] = "client_packet_mark"; +constexpr char MARK_HEADER[] = "X-Set-Mark"; +constexpr char ECHO_HEADER[] = "X-Client-Packet-Mark"; - int value_len = 0; - const char *value_str = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, -1, &value_len); - uint32_t result = 0; - if (value_str != nullptr && value_len > 0) { - std::string value(value_str, value_len); - result = static_cast(strtoul(value.c_str(), nullptr, 0)); - } - TSHandleMLocRelease(bufp, hdr_loc, field_loc); - return result; -} - -/** Create the echo header on the response with the value formatted as 0x%08x. */ -void -set_echo_header(TSMBuffer bufp, TSMLoc hdr_loc, uint32_t value) -{ - // 0x + 8 hex digits for a uint32_t + NUL = 11 bytes; 16 is comfortably enough. - char formatted[16]; - std::snprintf(formatted, sizeof(formatted), "0x%08x", value); - - TSMLoc field_loc = TS_NULL_MLOC; - if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, ECHO_HEADER.data(), static_cast(ECHO_HEADER.length()), &field_loc) == - TS_SUCCESS) { - // -1 length lets the API strlen the null-terminated buffer, so we do not - // rely on snprintf's return value (which is the would-be length, not the - // truncated length) as a byte count. - TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, formatted, -1); - TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc); - TSHandleMLocRelease(bufp, hdr_loc, field_loc); - } -} +DbgCtl dbg_ctl{PLUGIN_NAME}; int handle_send_response(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata) { TSHttpTxn txnp = static_cast(edata); - if (event != TS_EVENT_HTTP_SEND_RESPONSE_HDR) { - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); - return 0; - } - - TSMBuffer req_bufp = nullptr; - TSMLoc req_loc = TS_NULL_MLOC; - if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_loc) != TS_SUCCESS) { - TSError("[%s] Failed to get client request headers", PLUGIN_NAME.data()); - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); - return 0; - } - - std::optional mark = get_uint_header(req_bufp, req_loc, MARK_HEADER); - TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_loc); - - if (mark.has_value()) { - Dbg(dbg_ctl, "Setting client packet mark to 0x%08x", *mark); - TSHttpTxnClientPacketMarkSet(txnp, static_cast(*mark)); - } - - uint32_t observed = 0; -#if defined(SO_MARK) - int client_fd = -1; - if (TSHttpTxnClientFdGet(txnp, &client_fd) == TS_SUCCESS && client_fd >= 0) { - socklen_t optlen = sizeof(observed); - if (getsockopt(client_fd, SOL_SOCKET, SO_MARK, &observed, &optlen) != 0) { - TSError("[%s] getsockopt(SO_MARK) failed on fd %d", PLUGIN_NAME.data(), client_fd); - } - } else { - TSError("[%s] Failed to obtain client fd", PLUGIN_NAME.data()); - } -#else - // SO_MARK is Linux-only. On other platforms the accompanying AuTest is skipped - // via Test.SkipUnless, so this readback path is never exercised; keep it - // compilable so the plugin still builds everywhere. - TSError("[%s] SO_MARK is not supported on this platform", PLUGIN_NAME.data()); -#endif - - TSMBuffer resp_bufp = nullptr; - TSMLoc resp_loc = TS_NULL_MLOC; - if (TSHttpTxnClientRespGet(txnp, &resp_bufp, &resp_loc) == TS_SUCCESS) { - set_echo_header(resp_bufp, resp_loc, observed); - TSHandleMLocRelease(resp_bufp, TS_NULL_MLOC, resp_loc); - } else { - TSError("[%s] Failed to get client response headers", PLUGIN_NAME.data()); + if (event == TS_EVENT_HTTP_SEND_RESPONSE_HDR) { + // The client connection is live here; this applies the mark to it and reads + // it back off the client socket. + packet_mark::LogContext log{PLUGIN_NAME, dbg_ctl}; + packet_mark::apply_client_mark(log, txnp, MARK_HEADER); + packet_mark::echo_client_mark(log, txnp, ECHO_HEADER); } TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); @@ -154,12 +64,12 @@ void TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */) { TSPluginRegistrationInfo info; - info.plugin_name = PLUGIN_NAME.data(); + info.plugin_name = PLUGIN_NAME; info.vendor_name = "Apache Software Foundation"; info.support_email = "dev@trafficserver.apache.org"; if (TSPluginRegister(&info) != TS_SUCCESS) { - TSError("[%s] Plugin registration failed", PLUGIN_NAME.data()); + TSError("[%s] Plugin registration failed", PLUGIN_NAME); return; } diff --git a/tests/tools/plugins/packet_mark_common.cc b/tests/tools/plugins/packet_mark_common.cc new file mode 100644 index 00000000000..ac47799c12f --- /dev/null +++ b/tests/tools/plugins/packet_mark_common.cc @@ -0,0 +1,187 @@ +/** @file + + Shared helpers for the packet-mark test plugins. + + @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 "packet_mark_common.h" + +extern "C" { +#include +} + +#include +#include +#include +#include +#include +#include + +namespace packet_mark +{ +namespace +{ + // The tsapi setter and getters are bound as non-type template parameters below, + // which pins the correct client/server trio at compile time. These must be raw + // function-pointer types, not std::function: a non-type template parameter has + // to be a structural type, and std::function is a runtime type-erasure wrapper, + // so template Setter> is ill-formed. + using MarkSetter = TSReturnCode (*)(TSHttpTxn, int); + using FdGetter = TSReturnCode (*)(TSHttpTxn, int *); + using RespGetter = TSReturnCode (*)(TSHttpTxn, TSMBuffer *, TSMLoc *); + + std::optional + get_uint_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header) + { + // Values are parsed with strtoul (base 0), so "0x0000000A" and "10" are both + // accepted. Returns std::nullopt if the header is absent, empty, or not a valid + // number -- a malformed value is a test-harness error, not a silent 0. + TSMLoc field_loc{TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), static_cast(header.length()))}; + if (field_loc == TS_NULL_MLOC) { + return std::nullopt; + } + + int value_len{0}; + char const *value_str{TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, -1, &value_len)}; + + std::optional result{std::nullopt}; + if (value_str != nullptr && value_len > 0) { + std::string value{value_str, static_cast(value_len)}; + char *end{nullptr}; + errno = 0; + unsigned long const parsed{std::strtoul(value.c_str(), &end, 0)}; + // Reject empty, partially-numeric, or out-of-range values: a malformed + // header is a test-harness error, not a silent 0. + if (errno == 0 && end == value.c_str() + value.size() && parsed <= UINT32_MAX) { + result = static_cast(parsed); + } + } + TSHandleMLocRelease(bufp, hdr_loc, field_loc); + return result; + } + + void + set_echo_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header, uint32_t value) + { + // 0x + 8 hex digits for a uint32_t + NUL = 11 bytes; 16 is comfortably enough. + char formatted[16]; + std::snprintf(formatted, sizeof(formatted), "0x%08x", value); + + TSMLoc field_loc{TS_NULL_MLOC}; + if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, header.data(), static_cast(header.length()), &field_loc) == TS_SUCCESS) { + // -1 length lets the API strlen the null-terminated buffer, so we do not + // rely on snprintf's return value (which is the would-be length, not the + // truncated length) as a byte count. + TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, formatted, -1); + TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc); + TSHandleMLocRelease(bufp, hdr_loc, field_loc); + } + } + + std::optional + get_so_mark([[maybe_unused]] int fd) + { +#if defined(SO_MARK) + if (fd < 0) { + return std::nullopt; + } + + uint32_t observed{0}; + socklen_t optlen{sizeof(observed)}; + if (getsockopt(fd, SOL_SOCKET, SO_MARK, &observed, &optlen) != 0) { + return std::nullopt; + } + return observed; +#else + // SO_MARK is Linux-only. On other platforms the accompanying AuTest is + // skipped via Test.SkipUnless, so this readback path is never exercised; + // keep it compilable so the plugins still build everywhere. + return std::nullopt; +#endif + } + + // Parameterized on the exact tsapi function and kept private to this file, + // driven only by the named entry points below. The public API is split by + // client/server rather than taking the function as an argument so each plugin + // links against exactly the tsapi trio it exercises. + template + void + apply_mark_from_header(const LogContext &log, TSHttpTxn txnp, std::string_view header) + { + TSMBuffer req_bufp{nullptr}; + TSMLoc req_loc{TS_NULL_MLOC}; + if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_loc) != TS_SUCCESS) { + TSError("[%.*s] Failed to get client request headers", static_cast(log.plugin_name.length()), log.plugin_name.data()); + return; + } + + std::optional const mark{get_uint_header(req_bufp, req_loc, header)}; + TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_loc); + + if (mark.has_value()) { + Dbg(log.dbg_ctl, "Setting packet mark to 0x%08x (via %.*s)", *mark, static_cast(header.length()), header.data()); + if (Setter(txnp, static_cast(*mark)) != TS_SUCCESS) { + TSError("[%.*s] Failed to set packet mark 0x%08x", static_cast(log.plugin_name.length()), log.plugin_name.data(), + *mark); + } + } + } + + template + void + echo_observed_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header) + { + int fd{-1}; + if (FdGet(txnp, &fd) != TS_SUCCESS || fd < 0) { + TSError("[%.*s] Failed to obtain socket fd", static_cast(log.plugin_name.length()), log.plugin_name.data()); + return; + } + + std::optional const observed{get_so_mark(fd)}; + if (!observed.has_value()) { + TSError("[%.*s] Failed to read SO_MARK on fd %d", static_cast(log.plugin_name.length()), log.plugin_name.data(), fd); + return; + } + + TSMBuffer resp_bufp{nullptr}; + TSMLoc resp_loc{TS_NULL_MLOC}; + if (RespGet(txnp, &resp_bufp, &resp_loc) != TS_SUCCESS) { + TSError("[%.*s] Failed to get response headers", static_cast(log.plugin_name.length()), log.plugin_name.data()); + return; + } + + set_echo_header(resp_bufp, resp_loc, echo_header, *observed); + TSHandleMLocRelease(resp_bufp, TS_NULL_MLOC, resp_loc); + } +} // anonymous namespace + +void +apply_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view header) +{ + apply_mark_from_header(log, txnp, header); +} + +void +echo_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header) +{ + echo_observed_mark(log, txnp, echo_header); +} + +} // namespace packet_mark diff --git a/tests/tools/plugins/packet_mark_common.h b/tests/tools/plugins/packet_mark_common.h new file mode 100644 index 00000000000..3e43e53d0b8 --- /dev/null +++ b/tests/tools/plugins/packet_mark_common.h @@ -0,0 +1,47 @@ +/** @file + + Shared helpers for the packet-mark test plugins. + + The plugin reads a target mark out of a request header, applies it to a + connection via the tsapi under test, reads the applied mark back off the + relevant socket with getsockopt(SO_MARK), and echoes the observed value into a + response header for the accompanying AuTest to assert on. Everything except + the tsapi call and the fd getter is identical, so it lives here. + + @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. + */ + +#pragma once + +#include + +#include + +namespace packet_mark +{ +struct LogContext { + std::string_view plugin_name; + const DbgCtl &dbg_ctl; +}; + +void apply_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view header); + +void echo_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header); + +} // namespace packet_mark