This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new d94e6c3  For combo_handler plugin, add an optional whitelist of 
allowed values for Content-Type.
d94e6c3 is described below

commit d94e6c369d308769671b4e922e1698b7701fed6b
Author: Walter Karas <[email protected]>
AuthorDate: Thu Nov 21 13:30:11 2019 -0600

    For combo_handler plugin, add an optional whitelist of allowed values for 
Content-Type.
    
    The whitelist is in a text file, whose name is given on the plugin line in 
plugin.config.
    
    (cherry picked from commit d474cf757b7401c06c6b4441aa049ed0bd7588b8)
---
 doc/admin-guide/plugins/combo_handler.en.rst       |  10 ++
 plugins/esi/README.combo                           |   2 +
 plugins/esi/combo_handler.cc                       | 142 ++++++++++++++++++---
 .../pluginTest/combo_handler/combo_handler.test.py | 133 +++++++++++++++++++
 .../combo_handler/combo_handler_files/tr1.gold     |   9 ++
 .../combo_handler/combo_handler_files/tr2.gold     |  18 +++
 6 files changed, 297 insertions(+), 17 deletions(-)

diff --git a/doc/admin-guide/plugins/combo_handler.en.rst 
b/doc/admin-guide/plugins/combo_handler.en.rst
index 528704d..926c518 100644
--- a/doc/admin-guide/plugins/combo_handler.en.rst
+++ b/doc/admin-guide/plugins/combo_handler.en.rst
@@ -47,6 +47,16 @@ The arguments in the :file:`plugin.config` line in order 
represent
 2. The name of the key used for signature verification (disabled by
    default)
 
+3. A colon separated list of headers which, if present on at least one 
response, will be
+   added to the combo response.
+
+4. The path of a config file with allowed content types of objects to be 
combined, one per
+   line, without parameters. (Blank lines and comments starting with "#" are 
ignored.)
+   Parameters in the Content-Type field value will be ignored when
+   checking if they appear in the allowed types.  If the path does not start 
with "/", the
+   config file must be located in the ATS config directory.  By default, all 
content types
+   are allowed, but if this file is specified, it must contain at least one 
content type.
+
 A "-" can be supplied as a value for any of these arguments to request
 default value be applied.
 
diff --git a/plugins/esi/README.combo b/plugins/esi/README.combo
index e2042e1..211e8e0 100644
--- a/plugins/esi/README.combo
+++ b/plugins/esi/README.combo
@@ -1,6 +1,8 @@
 Combohandler
 --------------------
 
+NOTE:  THIS FILE IS OBSOLETE, SEE THE TRAFFICSERVER ADMIN GUIDE.
+
 This plugin provides that functionality (and more) with the same
 interface but with these differences in configuration:
 
diff --git a/plugins/esi/combo_handler.cc b/plugins/esi/combo_handler.cc
index 72d27fa..7cc317f 100644
--- a/plugins/esi/combo_handler.cc
+++ b/plugins/esi/combo_handler.cc
@@ -23,10 +23,12 @@
 
 #include <list>
 #include <string>
+#include <string_view>
 #include <sstream>
 #include <vector>
 #include <algorithm>
 #include <ctime>
+#include <fstream>
 #include <pthread.h>
 #include <arpa/inet.h>
 #include <limits>
@@ -36,6 +38,7 @@
 #include "ts/experimental.h"
 #include "ts/remap.h"
 #include "tscore/ink_defs.h"
+#include "tscpp/util/TextView.h"
 
 #include "HttpDataFetcherImpl.h"
 #include "gzip.h"
@@ -168,6 +171,32 @@ struct CacheControlHeader {
   bool _immutable       = true;
 };
 
+class ContentTypeHandler
+{
+public:
+  ContentTypeHandler(std::string &resp_header_fields) : 
_resp_header_fields(resp_header_fields) {}
+
+  // Returns false if _content_type_whitelist is not empty, and content-type 
field is either not present or not in the
+  // whitelist.  Adds first Content-type field it encounters in the headers 
passed to this function.
+  //
+  bool nextObjectHeader(TSMBuffer bufp, TSMLoc hdr_loc);
+
+  // Load whitelist from config file.
+  //
+  static void loadWhiteList(std::string const &file_spec);
+
+private:
+  // Add Content-Type field to these.
+  //
+  std::string &_resp_header_fields;
+
+  bool _added_content_type{false};
+
+  static vector<std::string> _content_type_whitelist;
+};
+
+vector<std::string> ContentTypeHandler::_content_type_whitelist;
+
 bool
 InterceptData::init(TSVConn vconn)
 {
@@ -309,7 +338,6 @@ static bool writeResponse(InterceptData &int_data);
 static bool writeErrorResponse(InterceptData &int_data, int &n_bytes_written);
 static bool writeStandardHeaderFields(InterceptData &int_data, int 
&n_bytes_written);
 static void prepareResponse(InterceptData &int_data, ByteBlockList 
&body_blocks, string &resp_header_fields);
-static bool getContentType(TSMBuffer bufp, TSMLoc hdr_loc, string 
&resp_header_fields);
 static bool getDefaultBucket(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc hdr_obj, 
ClientRequest &creq);
 
 // libesi TLS key.
@@ -392,6 +420,20 @@ TSPluginInit(int argc, const char *argv[])
     LOG_DEBUG("WhiteList: %s", HEADER_WHITELIST[i].c_str());
   }
 
+  std::string content_type_whitelist_filespec = (argc > optind && 
(argv[optind][0] != '-' || argv[optind][1])) ? argv[optind] : "";
+  if (content_type_whitelist_filespec.empty()) {
+    LOG_DEBUG("No Content-Type whitelist file specified (all content types 
allowed)");
+  } else {
+    // If we have a path and it's not an absolute path, make it relative to the
+    // configuration directory.
+    if (content_type_whitelist_filespec[0] != '/') {
+      content_type_whitelist_filespec = std::string(TSConfigDirGet()) + '/' + 
content_type_whitelist_filespec;
+    }
+    LOG_DEBUG("Content-Type whitelist file: %s", 
content_type_whitelist_filespec.c_str());
+    ContentTypeHandler::loadWhiteList(content_type_whitelist_filespec);
+  }
+  ++optind;
+
   TSReleaseAssert(pthread_key_create(&threadKey, nullptr) == 0);
 
   TSCont rrh_contp = TSContCreate(handleReadRequestHeader, nullptr);
@@ -926,8 +968,6 @@ writeResponse(InterceptData &int_data)
 static void
 prepareResponse(InterceptData &int_data, ByteBlockList &body_blocks, string 
&resp_header_fields)
 {
-  bool got_content_type = false;
-
   if (int_data.creq.status == TS_HTTP_STATUS_OK) {
     HttpDataFetcherImpl::ResponseData resp_data;
     TSMLoc field_loc;
@@ -941,12 +981,16 @@ prepareResponse(InterceptData &int_data, ByteBlockList 
&body_blocks, string &res
       flags_list[i] = 0;
     }
 
+    ContentTypeHandler cth(resp_header_fields);
+
     for (StringList::iterator iter = int_data.creq.file_urls.begin(); iter != 
int_data.creq.file_urls.end(); ++iter) {
       if (int_data.fetcher->getData(*iter, resp_data) && resp_data.status == 
TS_HTTP_STATUS_OK) {
         body_blocks.push_back(ByteBlock(resp_data.content, 
resp_data.content_len));
         if (find(HEADER_WHITELIST.begin(), HEADER_WHITELIST.end(), 
TS_MIME_FIELD_CONTENT_TYPE) == HEADER_WHITELIST.end()) {
-          if (!got_content_type) {
-            got_content_type = getContentType(resp_data.bufp, 
resp_data.hdr_loc, resp_header_fields);
+          if (!cth.nextObjectHeader(resp_data.bufp, resp_data.hdr_loc)) {
+            LOG_ERROR("Content type missing or forbidden for requested URL 
[%s]", iter->c_str());
+            int_data.creq.status = TS_HTTP_STATUS_FORBIDDEN;
+            break;
           }
         }
 
@@ -1040,10 +1084,9 @@ prepareResponse(InterceptData &int_data, ByteBlockList 
&body_blocks, string &res
   }
 }
 
-static bool
-getContentType(TSMBuffer bufp, TSMLoc hdr_loc, string &resp_header_fields)
+bool
+ContentTypeHandler::nextObjectHeader(TSMBuffer bufp, TSMLoc hdr_loc)
 {
-  bool retval      = false;
   TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, 
TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE);
   if (field_loc != TS_NULL_MLOC) {
     bool values_added = false;
@@ -1052,21 +1095,86 @@ getContentType(TSMBuffer bufp, TSMLoc hdr_loc, string 
&resp_header_fields)
     int n_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc);
     for (int i = 0; i < n_values; ++i) {
       value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, i, 
&value_len);
-      if (!values_added) {
-        resp_header_fields.append("Content-Type: ");
-        values_added = true;
-      } else {
-        resp_header_fields.append(", ");
+      ts::TextView tv{value, value_len};
+      tv = tv.prefix(';').rtrim(std::string_view(" \t"));
+      if (_content_type_whitelist.empty()) {
+        ;
+      } else if (std::find_if(_content_type_whitelist.begin(), 
_content_type_whitelist.end(), [tv](ts::TextView tv2) -> bool {
+                   return strcasecmp(tv, tv2) == 0;
+                 }) == _content_type_whitelist.end()) {
+        return false;
+      } else if (tv.empty()) {
+        // Whitelist is bad, contains an empty string.
+        return false;
+      }
+      if (!_added_content_type) {
+        if (!values_added) {
+          _resp_header_fields.append("Content-Type: ");
+          values_added = true;
+        } else {
+          _resp_header_fields.append(", ");
+        }
+        _resp_header_fields.append(value, value_len);
       }
-      resp_header_fields.append(value, value_len);
     }
     TSHandleMLocRelease(bufp, hdr_loc, field_loc);
     if (values_added) {
-      resp_header_fields.append("\r\n");
-      retval = true;
+      _resp_header_fields.append("\r\n");
+
+      // Assume that the Content-type field from the first header covers all 
the responses being combined.
+      _added_content_type = true;
     }
+    return true;
   }
-  return retval;
+  // No content type header field so doesn't pass whitelist if there is one.
+  return _content_type_whitelist.empty();
+}
+
+void
+ContentTypeHandler::loadWhiteList(std::string const &file_spec)
+{
+  std::fstream fs;
+  char line_buffer[256];
+  bool extra_junk_on_line{false};
+  int line_num = 0;
+
+  fs.open(file_spec);
+  if (fs.good()) {
+    for (;;) {
+      ++line_num;
+      fs.getline(line_buffer, sizeof(line_buffer));
+      if (!fs.good()) {
+        break;
+      }
+      constexpr std::string_view bs{" \t"sv};
+      ts::TextView line{line_buffer, std::size_t(fs.gcount() - 1)};
+      line.ltrim(bs);
+      if (line.empty() || line[0] == '#') {
+        // Empty/comment line.
+        continue;
+      }
+      ts::TextView content_type{line.take_prefix_at(bs)};
+      line.trim(bs);
+      if (line.size() && (line[0] != '#')) {
+        extra_junk_on_line = true;
+        break;
+      }
+      _content_type_whitelist.emplace_back(content_type);
+    }
+  }
+  if (fs.fail() && !(fs.eof() && (fs.gcount() == 0))) {
+    LOG_ERROR("Error reading Content-Type whitelist config file %s, line %d", 
file_spec.c_str(), line_num);
+  } else if (extra_junk_on_line) {
+    LOG_ERROR("More than one type on line %d in Content-Type whitelist config 
file %s", line_num, file_spec.c_str());
+  } else if (_content_type_whitelist.empty()) {
+    LOG_ERROR("Content-type whitelist config file %s must have at least one 
entry", file_spec.c_str());
+  } else {
+    // End of file.
+    return;
+  }
+  _content_type_whitelist.clear();
+  // An empty string marks object as bad.
+  _content_type_whitelist.emplace_back("");
 }
 
 static const char INVARIANT_FIELD_LINES[]    = {"Vary: Accept-Encoding\r\n"};
diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py 
b/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py
new file mode 100644
index 0000000..6c0820d
--- /dev/null
+++ b/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py
@@ -0,0 +1,133 @@
+'''
+'''
+#  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 = '''
+Test combo_handler plugin
+'''
+
+# Skip if plugin not present.
+#
+Test.SkipUnless(
+    Condition.PluginExists('combo_handler.so'),
+)
+
+# Function to generate a unique data file path (in the top level of the test's 
run directory),  put data (in string 'data') into
+# the file, and return the file name.
+#
+_data_file__file_count = 0
+def data_file(data):
+    global _data_file__file_count
+    file_path = Test.RunDirectory + 
"/tcp_client_in_{}".format(_data_file__file_count)
+    _data_file__file_count += 1
+    with open(file_path, "x") as f:
+        f.write(data)
+    return file_path
+
+# Function to return command (string) to run tcp_client.py tool.  'host' 
'port', and 'file_path' are the parameters to tcp_client.
+#
+def tcp_client_cmd(host, port, file_path):
+    return "python {}/tcp_client.py {} {} 
{}".format(Test.Variables.AtsTestToolsDir, host, port, file_path)
+
+# Function to return command (string) to run tcp_client.py tool.  'host' and 
'port' are the first two parameters to tcp_client.
+# 'data' is the data to put in the data file input to tcp_client.
+#
+def tcp_client(host, port, data):
+    return tcp_client_cmd(host, port, data_file(data))
+
+server = Test.MakeOriginServer("server")
+
+def add_server_obj(content_type, path):
+    request_header = {
+        "headers": "GET " + path + " HTTP/1.1\r\n" +
+            "Host: just.any.thing\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": ""
+    }
+    response_header = {
+        "headers": "HTTP/1.1 200 OK\r\n" +
+            "Connection: close\r\n" +
+            'Etag: "359670651"\r\n' +
+            "Cache-Control: public, max-age=31536000\r\n" +
+            "Accept-Ranges: bytes\r\n" +
+            "Content-Type: " + content_type + "\r\n" +
+            "\r\n",
+        "timestamp": "1469733493.993",
+        "body": "Content for " + path + "\n"
+    }
+    server.addResponse("sessionfile.log", request_header, response_header)
+
+add_server_obj("text/css ; charset=utf-8", "/obj1")
+add_server_obj("text/javascript", "/sub/obj2")
+add_server_obj("text/argh", "/obj3")
+add_server_obj("application/javascript", "/obj4")
+
+ts = Test.MakeATSProcess("ts")
+
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http|combo_handler',
+})
+
+ts.Disk.plugin_config.AddLine("combo_handler.so - - - ctwl.txt")
+
+ts.Disk.remap_config.AddLine(
+    'map http://xyz/ http://127.0.0.1/ @plugin=combo_handler.so'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://localhost/127.0.0.1/ 
http://127.0.0.1:{}/'.format(server.Variables.Port)
+)
+ts.Disk.remap_config.AddLine(
+    'map http://localhost/sub/ 
http://127.0.0.1:{}/sub/'.format(server.Variables.Port)
+)
+
+ts.Disk.File(ts.Variables.CONFIGDIR + "/ctwl.txt", id="ctwl_cfg", 
typename="ats:config")
+ts.Disk.ctwl_cfg.AddLine("# test  ")
+ts.Disk.ctwl_cfg.AddLine("")
+ts.Disk.ctwl_cfg.AddLine("             text/javascript  # test side comment")
+ts.Disk.ctwl_cfg.AddLine("  application/javascript")
+ts.Disk.ctwl_cfg.AddLine("text/css")
+
+tr = Test.AddTestRun()
+tr.Processes.Default.StartBefore(ts, ready=When.PortOpen(ts.Variables.port))
+tr.Processes.Default.StartBefore(server, 
ready=When.PortOpen(server.Variables.Port))
+tr.Processes.Default.Command = "echo start stuff"
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = tcp_client("127.0.0.1", ts.Variables.port,
+    "GET /admin/v1/combo?obj1&sub:obj2&obj3 HTTP/1.1\n" +
+    "Host: xyz\n" +
+    "Connection: close\n" +
+    "\n"
+)
+tr.Processes.Default.ReturnCode = 0
+f = tr.Disk.File("_output/1-tr-Default/stream.all.txt")
+f.Content = "combo_handler_files/tr1.gold"
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = tcp_client("127.0.0.1", ts.Variables.port,
+    "GET /admin/v1/combo?obj1&sub:obj2&obj4 HTTP/1.1\n" +
+    "Host: xyz\n" +
+    "Connection: close\n" +
+    "\n"
+)
+tr.Processes.Default.ReturnCode = 0
+f = tr.Disk.File("_output/2-tr-Default/stream.all.txt")
+f.Content = "combo_handler_files/tr2.gold"
+
+ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests 
are failure tests")
diff --git 
a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold 
b/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold
new file mode 100644
index 0000000..133179b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold
@@ -0,0 +1,9 @@
+HTTP/1.1 403 Forbidden
+Date: ``
+Age: ``
+Transfer-Encoding: chunked
+Connection: close
+Server: ATS/``
+
+0
+
diff --git 
a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold 
b/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold
new file mode 100644
index 0000000..4d775f6
--- /dev/null
+++ b/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold
@@ -0,0 +1,18 @@
+HTTP/1.1 200 OK
+Vary: Accept-Encoding
+Last-Modified: ``
+Content-Type: text/css ; charset=utf-8
+Cache-Control: max-age=31536000, Public
+Date: ``
+Age: ``
+Transfer-Encoding: chunked
+Connection: close
+Server: ATS/``
+
+3a
+Content for /obj1
+Content for /sub/obj2
+Content for /obj4
+
+0
+

Reply via email to