This is an automated email from the ASF dual-hosted git repository. eze pushed a commit to branch 9.2.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/9.2.x by this push: new 2db8b8dc96 Add max inclusion support to esi plugin for 9.2.x (#12296) 2db8b8dc96 is described below commit 2db8b8dc96e57fc292850f77b9783630cc9590b9 Author: Kit Chan <kic...@apache.org> AuthorDate: Mon Jun 16 14:30:43 2025 -0700 Add max inclusion support to esi plugin for 9.2.x (#12296) * Add max inclusion support to esi plugin for 9.2.x --------- Co-authored-by: Shu Kit Chan <kic...@c40b7d47.home-kichan.bf2.ows.oath.cloud> --- doc/admin-guide/plugins/esi.en.rst | 4 +- plugins/esi/esi.cc | 121 +++++++++++++----- .../pluginTest/esi/esi_nested_include.test.py | 137 +++++++++++++++++++++ .../pluginTest/esi/gold/nested_include_body.gold | 12 ++ 4 files changed, 241 insertions(+), 33 deletions(-) diff --git a/doc/admin-guide/plugins/esi.en.rst b/doc/admin-guide/plugins/esi.en.rst index 1759131292..0b1ead09be 100644 --- a/doc/admin-guide/plugins/esi.en.rst +++ b/doc/admin-guide/plugins/esi.en.rst @@ -76,7 +76,7 @@ Enabling ESI esi.so -2. There are four optional arguments that can be passed to the above ``esi.so`` entry: +2. There are optional arguments that can be passed to the above ``esi.so`` entry: - ``--private-response`` will add private cache control and expires headers to the processed ESI document. - ``--packed-node-support`` will enable the support for using the packed node feature, which will improve the @@ -86,6 +86,8 @@ Enabling ESI - ``--first-byte-flush`` will enable the first byte flush feature, which will flush content to users as soon as the entire ESI document is received and parsed without all ESI includes fetched. The flushing will stop at the ESI include markup till that include is fetched. +- ``--max-inclusion-depth <max-depth>`` controls the maximum depth of recursive ESI inclusion allowed (between 0 and 9). + Default is 3. 3. ``HTTP_COOKIE`` variable support is turned off by default. It can be turned on with ``-f <handler_config>`` or ``-handler <handler_config>``. For example: diff --git a/plugins/esi/esi.cc b/plugins/esi/esi.cc index 3eb18d2e4d..06d37dfbf5 100644 --- a/plugins/esi/esi.cc +++ b/plugins/esi/esi.cc @@ -56,6 +56,7 @@ struct OptionInfo { bool private_response; bool disable_gzip_output; bool first_byte_flush; + unsigned max_inclusion_depth{3}; }; static HandlerManager *gHandlerManager = nullptr; @@ -74,6 +75,9 @@ static Utils::HeaderValueList gAllowlistCookies; #define MIME_FIELD_XESI "X-Esi" #define MIME_FIELD_XESI_LEN 5 +#define MIME_FIELD_XESIDEPTH "X-Esi-Depth" +#define MIME_FIELD_XESIDEPTH_LEN 11 + #define HTTP_VALUE_PRIVATE_EXPIRES "-1" #define HTTP_VALUE_PRIVATE_CC "max-age=0, private" @@ -315,7 +319,9 @@ ContData::getClientState() } TSHandleMLocRelease(bufp, req_hdr_loc, url_loc); } + TSMLoc field_loc = TSMimeHdrFieldGet(req_bufp, req_hdr_loc, 0); + bool depth_field = false; while (field_loc) { TSMLoc next_field_loc; const char *name; @@ -323,38 +329,55 @@ ContData::getClientState() name = TSMimeHdrFieldNameGet(req_bufp, req_hdr_loc, field_loc, &name_len); if (name) { - int n_values; - n_values = TSMimeHdrFieldValuesCount(req_bufp, req_hdr_loc, field_loc); - if (n_values && (n_values != TS_ERROR)) { - const char *value = nullptr; - int value_len = 0; - if (n_values == 1) { - value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, 0, &value_len); - - if (nullptr != value && value_len) { - if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && - Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { - gzip_output = true; - } - } + if (Utils::areEqual(name, name_len, MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN)) { + unsigned d = TSMimeHdrFieldValueUintGet(req_bufp, req_hdr_loc, field_loc, -1); + d = (d + 1) % 10; + char dstr[2]; + int const len = snprintf(dstr, sizeof(dstr), "%u", d); + + HttpHeader header; + if (len != 1) { + header = HttpHeader(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, "1", 1); } else { - for (int i = 0; i < n_values; ++i) { - value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, i, &value_len); + header = HttpHeader(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, dstr, 1); + } + data_fetcher->useHeader(header); + esi_vars->populate(header); + depth_field = true; + + } else { + int n_values; + n_values = TSMimeHdrFieldValuesCount(req_bufp, req_hdr_loc, field_loc); + if (n_values && (n_values != TS_ERROR)) { + const char *value = nullptr; + int value_len = 0; + if (n_values == 1) { + value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, 0, &value_len); + if (nullptr != value && value_len) { if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { gzip_output = true; } } + } else { + for (int i = 0; i < n_values; ++i) { + value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, i, &value_len); + if (nullptr != value && value_len) { + if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && + Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { + gzip_output = true; + } + } + } + value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, -1, &value_len); } - value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, -1, &value_len); - } - - if (value != nullptr) { - HttpHeader header(name, name_len, value, value_len); - data_fetcher->useHeader(header); - esi_vars->populate(header); + if (value != nullptr) { + HttpHeader header(name, name_len, value, value_len); + data_fetcher->useHeader(header); + esi_vars->populate(header); + } } } } @@ -363,6 +386,12 @@ ContData::getClientState() TSHandleMLocRelease(req_bufp, req_hdr_loc, field_loc); field_loc = next_field_loc; } + + if (depth_field == false) { + HttpHeader header(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, "1", 1); + data_fetcher->useHeader(header); + esi_vars->populate(header); + } } if (gzip_output) { @@ -1252,7 +1281,7 @@ maskOsCacheHeaders(TSHttpTxn txnp) } static bool -isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, bool *intercept_header, bool *head_only) +isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, const OptionInfo *pOptionInfo, bool *intercept_header, bool *head_only) { // We are only interested in transforming "200 OK" responses with a // Content-Type: text/ header and with X-Esi header @@ -1267,6 +1296,21 @@ isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, bool *intercept_header, bo return false; } + TSMLoc loc; + unsigned d; + + d = 0; + loc = TSMimeHdrFieldFind(bufp, hdr_loc, MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN); + if (loc != TS_NULL_MLOC) { + d = TSMimeHdrFieldValueUintGet(bufp, hdr_loc, loc, -1); + } + TSHandleMLocRelease(bufp, hdr_loc, loc); + if (d >= pOptionInfo->max_inclusion_depth) { + TSError("[esi][%s] The current esi inclusion depth (%u) is larger than or equal to the max (%u)", __FUNCTION__, d, + pOptionInfo->max_inclusion_depth); + return false; + } + int method_len; const char *method; method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len); @@ -1341,7 +1385,7 @@ isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, bool *intercept_header, bo } static bool -isCacheObjTransformable(TSHttpTxn txnp, bool *intercept_header, bool *head_only) +isCacheObjTransformable(TSHttpTxn txnp, const OptionInfo *pOptionInfo, bool *intercept_header, bool *head_only) { int obj_status; if (TSHttpTxnCacheLookupStatusGet(txnp, &obj_status) == TS_ERROR) { @@ -1350,7 +1394,7 @@ isCacheObjTransformable(TSHttpTxn txnp, bool *intercept_header, bool *head_only) } if (obj_status == TS_CACHE_LOOKUP_HIT_FRESH) { TSDebug(DEBUG_TAG, "[%s] doc found in cache, will add transformation", __FUNCTION__); - return isTxnTransformable(txnp, true, intercept_header, head_only); + return isTxnTransformable(txnp, true, pOptionInfo, intercept_header, head_only); } TSDebug(DEBUG_TAG, "[%s] cache object's status is %d; not transformable", __FUNCTION__, obj_status); return false; @@ -1523,7 +1567,7 @@ globalHookHandler(TSCont contp, TSEvent event, void *edata) if (event == TS_EVENT_HTTP_READ_RESPONSE_HDR) { bool mask_cache_headers = false; TSDebug(DEBUG_TAG, "[%s] handling read response header event", __FUNCTION__); - if (isTxnTransformable(txnp, false, &intercept_header, &head_only)) { + if (isTxnTransformable(txnp, false, pOptionInfo, &intercept_header, &head_only)) { addTransform(txnp, true, intercept_header, head_only, pOptionInfo); Stats::increment(Stats::N_OS_DOCS); mask_cache_headers = true; @@ -1536,7 +1580,7 @@ globalHookHandler(TSCont contp, TSEvent event, void *edata) } } else { TSDebug(DEBUG_TAG, "[%s] handling cache lookup complete event", __FUNCTION__); - if (isCacheObjTransformable(txnp, &intercept_header, &head_only)) { + if (isCacheObjTransformable(txnp, pOptionInfo, &intercept_header, &head_only)) { // we make the assumption above that a transformable cache // object would already have a transformation. We should revisit // that assumption in case we change the statement below @@ -1589,7 +1633,7 @@ esiPluginInit(int argc, const char *argv[], struct OptionInfo *pOptionInfo) gHandlerManager = new HandlerManager(HANDLER_MGR_DEBUG_TAG, &TSDebug, &TSError); } - memset(pOptionInfo, 0, sizeof(struct OptionInfo)); + new (pOptionInfo) OptionInfo; if (argc > 1) { int c; @@ -1599,11 +1643,12 @@ esiPluginInit(int argc, const char *argv[], struct OptionInfo *pOptionInfo) {const_cast<char *>("disable-gzip-output"), no_argument, nullptr, 'z'}, {const_cast<char *>("first-byte-flush"), no_argument, nullptr, 'b'}, {const_cast<char *>("handler-filename"), required_argument, nullptr, 'f'}, + {const_cast<char *>("max-inclusion-depth"), required_argument, nullptr, 'i'}, {nullptr, 0, nullptr, 0}, }; int longindex = 0; - while ((c = getopt_long(argc, const_cast<char *const *>(argv), "npzbf:", longopts, &longindex)) != -1) { + while ((c = getopt_long(argc, const_cast<char *const *>(argv), "npzbf:i:", longopts, &longindex)) != -1) { switch (c) { case 'n': pOptionInfo->packed_node_support = true; @@ -1623,6 +1668,18 @@ esiPluginInit(int argc, const char *argv[], struct OptionInfo *pOptionInfo) gHandlerManager->loadObjects(handler_conf); break; } + case 'i': { + unsigned max; + auto num = std::sscanf(optarg, "%u", &max); + if (num != 1) { + TSEmergency("[esi][%s] value for maximum inclusion depth (%s) is not unsigned integer", __FUNCTION__, optarg); + } + if (max > 9) { + TSEmergency("[esi][%s] maximum inclusion depth (%s) large than 9", __FUNCTION__, optarg); + } + pOptionInfo->max_inclusion_depth = max; + break; + } default: break; } @@ -1632,9 +1689,9 @@ esiPluginInit(int argc, const char *argv[], struct OptionInfo *pOptionInfo) TSDebug(DEBUG_TAG, "[%s] Plugin started, " "packed-node-support: %d, private-response: %d, " - "disable-gzip-output: %d, first-byte-flush: %d ", + "disable-gzip-output: %d, first-byte-flush: %d, max-inclusion-depth %u ", __FUNCTION__, pOptionInfo->packed_node_support, pOptionInfo->private_response, pOptionInfo->disable_gzip_output, - pOptionInfo->first_byte_flush); + pOptionInfo->first_byte_flush, pOptionInfo->max_inclusion_depth); return 0; } diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py b/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py new file mode 100644 index 0000000000..7c5be0201b --- /dev/null +++ b/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py @@ -0,0 +1,137 @@ +''' +Test nested include for the ESI plugin. +''' +# 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 nested include for the ESI plugin. +''' + +Test.SkipUnless(Condition.PluginExists('esi.so'),) + + +class EsiTest(): + """ + A class that encapsulates the configuration and execution of a set of ESI + test cases. + """ + """ static: The same server Process is used across all tests. """ + _server = None + """ static: A counter to keep the ATS process names unique across tests. """ + _ts_counter = 0 + """ static: A counter to keep any output file names unique across tests. """ + _output_counter = 0 + """ The ATS process for this set of test cases. """ + _ts = None + + def __init__(self, plugin_config): + """ + Args: + plugin_config (str): The config line to place in plugin.config for + the ATS process. + """ + if EsiTest._server is None: + EsiTest._server = EsiTest._create_server() + + self._ts = EsiTest._create_ats(self, plugin_config) + + @staticmethod + def _create_server(): + """ + Create and start a server process. + """ + # Configure our server. + server = Test.MakeOriginServer("server", lookup_key="{%uuid}") + + # Generate the set of ESI responses. + request_header = { + "headers": "GET /esi-nested-include.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + esi_body = r'''<p> +<esi:include src="http://www.example.com/esi-nested-include.html"/> +</p> +''' + response_header = { + "headers": + "HTTP/1.1 200 OK\r\n" + "X-Esi: 1\r\n" + "Cache-Control: private\r\n" + "Content-Type: text/html\r\n" + + "Connection: close\r\n" + "Content-Length: {}\r\n".format(len(esi_body)) + "\r\n", + "timestamp": "1469733493.993", + "body": esi_body + } + server.addResponse("sessionfile.log", request_header, response_header) + + # Create a run to start the server. + tr = Test.AddTestRun("Start the server.") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = "echo starting the server" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + + return server + + @staticmethod + def _create_ats(self, plugin_config): + """ + Create and start an ATS process. + """ + EsiTest._ts_counter += 1 + + # Configure ATS with a vanilla ESI plugin configuration. + ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{EsiTest._server.Variables.Port}') + ts.Disk.plugin_config.AddLine(plugin_config) + + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r'The current esi inclusion depth \(3\) is larger than or equal to the max \(3\)', + 'Verify the ESI error concerning the max inclusion depth') + + # Create a run to start the ATS process. + tr = Test.AddTestRun("Start the ATS process.") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = "echo starting ATS" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + return ts + + def run_test(self): + # Test 1: Verify basic ESI functionality without processing internal txn. + tr = Test.AddTestRun("First request") + tr.Processes.Default.Command = \ + ('curl http://127.0.0.1:{0}/main.php -H"Host: www.example.com" ' + '-H"Accept: */*" --verbose'.format( + self._ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/nested_include_body.gold" + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + +# +# Configure and run the test cases. +# + +# Run the tests with ESI configured with private response. +first_test = EsiTest(plugin_config='esi.so') +first_test.run_test() diff --git a/tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold b/tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold new file mode 100644 index 0000000000..84e0e4c699 --- /dev/null +++ b/tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold @@ -0,0 +1,12 @@ +<p> +<p> +<p> +<p> +<esi:include src="http://www.example.com/esi-nested-include.html"/> +</p> + +</p> + +</p> + +</p>