Add param to forward headers from the auth server to the origin (#9271)

diff --git a/doc/admin-guide/plugins/authproxy.en.rst b/doc/admin-guide/plugins/authproxy.en.rst
index b17984a..c788c63 100644
--- a/doc/admin-guide/plugins/authproxy.en.rst
+++ b/doc/admin-guide/plugins/authproxy.en.rst
@@ -80,6 +80,11 @@
   The TCP port of the authorization host. This is only used by the
   ``redirect`` transform.
 
+--forward-header-prefix=PREFIX
+  If the option is enabled, authentication response header fields, which
+  contain the specified prefix, will be sent to the original server in
+  the original request.
+
 --force-cacheability
   If this options is set, the plugin will allow Traffic Server to
   cache the result of authorized requests. In the normal case, requests
@@ -106,10 +111,11 @@
 
 
 In this example, the request is directed to a local authentication server
-that authorizes the request based on internal policy rules::
+that authorizes the request based on internal policy rules. Authentication response
+headers with the prefix will be proxied to the original server::
 
   map http://cache.example.com http://origin.internal.com/ \
-    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000
+    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000 @pparam=--forward-header-prefix=x-requested
 
   map http://origin.internal.com/ http://origin.internal.com/ \
-    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000
+    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000 @pparam=--forward-header-prefix=x-requested
diff --git a/plugins/authproxy/Makefile.inc b/plugins/authproxy/Makefile.inc
index a46a242..9e2e569 100644
--- a/plugins/authproxy/Makefile.inc
+++ b/plugins/authproxy/Makefile.inc
@@ -19,3 +19,7 @@
 	authproxy/authproxy.cc \
 	authproxy/utils.cc \
 	authproxy/utils.h
+
+check_PROGRAMS +=  authproxy/authproxy_test
+authproxy_authproxy_test_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include
+authproxy_authproxy_test_SOURCES = authproxy/tests/authproxy_test.cc
diff --git a/plugins/authproxy/authproxy.cc b/plugins/authproxy/authproxy.cc
index 74f20ce..a8da078 100644
--- a/plugins/authproxy/authproxy.cc
+++ b/plugins/authproxy/authproxy.cc
@@ -39,6 +39,7 @@
 #include <ts/remap.h>
 
 using std::strlen;
+using std::string_view;
 
 struct AuthRequestContext;
 
@@ -55,10 +56,12 @@
 
 struct AuthOptions {
   std::string hostname;
+  std::string forward_header_prefix;
   int hostport                   = -1;
   AuthRequestTransform transform = nullptr;
   bool force                     = false;
   bool cache_internal_requests   = false;
+  string_view forwardHeaderPrefix;
 
   AuthOptions()  = default;
   ~AuthOptions() = default;
@@ -622,6 +625,38 @@
     TSHttpTxnConfigIntSet(auth->txn, TS_CONFIG_HTTP_CACHE_IGNORE_AUTHENTICATION, 1);
   }
 
+  if (!options->forward_header_prefix.empty()) {
+    // Copy headers with configured prefix in the authentication response to the original request
+    TSMLoc field_loc;
+    TSMLoc next_field_loc;
+    TSMBuffer request_bufp;
+    TSMLoc request_hdr;
+
+    TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &request_bufp, &request_hdr) == TS_SUCCESS);
+    field_loc = TSMimeHdrFieldGet(auth->rheader.buffer, auth->rheader.header, 0);
+    TSReleaseAssert(field_loc != TS_NULL_MLOC);
+
+    while (field_loc) {
+      int key_len = 0;
+      int val_len = 0;
+
+      char *key = const_cast<char *>(TSMimeHdrFieldNameGet(auth->rheader.buffer, auth->rheader.header, field_loc, &key_len));
+      char *val =
+        const_cast<char *>(TSMimeHdrFieldValueStringGet(auth->rheader.buffer, auth->rheader.header, field_loc, -1, &val_len));
+
+      if (key && val && ContainsPrefix(string_view(key, key_len), options->forward_header_prefix)) {
+        // Append the matched header key/val to the original request
+        HttpSetMimeHeader(request_bufp, request_hdr, string_view(key, key_len), string_view(val, val_len));
+      }
+
+      // Validate the next header field
+      next_field_loc = TSMimeHdrFieldNext(auth->rheader.buffer, auth->rheader.header, field_loc);
+      TSHandleMLocRelease(auth->rheader.buffer, auth->rheader.header, field_loc);
+      field_loc = next_field_loc;
+    }
+  }
+
+  // Proceed with the modified request
   TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_CONTINUE);
   return TS_EVENT_CONTINUE;
 }
@@ -693,6 +728,7 @@
     {const_cast<char *>("auth-transform"), required_argument, nullptr, 't'},
     {const_cast<char *>("force-cacheability"), no_argument, nullptr, 'c'},
     {const_cast<char *>("cache-internal"), no_argument, nullptr, 'i'},
+    {const_cast<char *>("forward-header-prefix"), required_argument, nullptr, 'f'},
     {nullptr, 0, nullptr, 0},
   };
 
@@ -717,6 +753,9 @@
     case 'i':
       options->cache_internal_requests = true;
       break;
+    case 'f':
+      options->forward_header_prefix = optarg;
+      break;
     case 't':
       if (strcasecmp(optarg, "redirect") == 0) {
         options->transform = AuthWriteRedirectedRequest;
diff --git a/plugins/authproxy/tests/authproxy_test.cc b/plugins/authproxy/tests/authproxy_test.cc
new file mode 100644
index 0000000..c9026cb
--- /dev/null
+++ b/plugins/authproxy/tests/authproxy_test.cc
@@ -0,0 +1,39 @@
+/*
+ * 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 <string_view>
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp>      /* catch unit-test framework */
+#include "../utils.h"
+
+using std::string_view;
+TEST_CASE("Util methods", "[authproxy][utility]")
+{
+  SECTION("ContainsPrefix()")
+  {
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abc") == true);
+    CHECK(ContainsPrefix(string_view{"abc"}, "abcdef") == false);
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abd") == false);
+    CHECK(ContainsPrefix(string_view{"abc"}, "abc") == true);
+    CHECK(ContainsPrefix(string_view{""}, "") == true);
+    CHECK(ContainsPrefix(string_view{"abc"}, "") == true);
+    CHECK(ContainsPrefix(string_view{""}, "abc") == false);
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abc\0") == true);
+    CHECK(ContainsPrefix(string_view{"abcdef\0"}, "abc\0") == true);
+  }
+}
diff --git a/plugins/authproxy/utils.cc b/plugins/authproxy/utils.cc
index bbc1ac1..e9a9a75 100644
--- a/plugins/authproxy/utils.cc
+++ b/plugins/authproxy/utils.cc
@@ -26,6 +26,7 @@
 #include <getopt.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
+#include <string_view>
 
 void
 HttpDebugHeader(TSMBuffer mbuf, TSMLoc mhdr)
@@ -81,6 +82,23 @@
   TSHandleMLocRelease(mbuf, mhdr, mloc);
 }
 
+void
+HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, const std::string_view value)
+{
+  TSMLoc mloc;
+  mloc = TSMimeHdrFieldFind(mbuf, mhdr, name.data(), name.size());
+  if (mloc == TS_NULL_MLOC) {
+    TSReleaseAssert(TSMimeHdrFieldCreateNamed(mbuf, mhdr, name.data(), name.size(), &mloc) == TS_SUCCESS);
+  } else {
+    TSReleaseAssert(TSMimeHdrFieldValuesClear(mbuf, mhdr, mloc) == TS_SUCCESS);
+  }
+
+  TSReleaseAssert(TSMimeHdrFieldValueStringInsert(mbuf, mhdr, mloc, 0 /* index */, value.data(), value.size()) == TS_SUCCESS);
+  TSReleaseAssert(TSMimeHdrFieldAppend(mbuf, mhdr, mloc) == TS_SUCCESS);
+
+  TSHandleMLocRelease(mbuf, mhdr, mloc);
+}
+
 unsigned
 HttpGetContentLength(TSMBuffer mbuf, TSMLoc mhdr)
 {
diff --git a/plugins/authproxy/utils.h b/plugins/authproxy/utils.h
index 51cf01c..bd672ee 100644
--- a/plugins/authproxy/utils.h
+++ b/plugins/authproxy/utils.h
@@ -18,6 +18,8 @@
 
 #pragma once
 
+#include <string>
+#include <string_view>
 #include <ts/ts.h>
 #include <netinet/in.h>
 #include <memory>
@@ -102,8 +104,15 @@
 // Set the value of an arbitrary HTTP header.
 void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, const char *value);
 void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, unsigned value);
+void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, const std::string_view value);
 
 // Dump the given HTTP header to the debug log.
 void HttpDebugHeader(TSMBuffer mbuf, TSMLoc mhdr);
 
+// Check if the string contains the prefix
+inline bool
+ContainsPrefix(const std::string_view str, const std::string &prefix)
+{
+  return str.size() < prefix.size() ? false : (strncmp(str.data(), prefix.data(), prefix.size()) == 0);
+}
 // vim: set ts=4 sw=4 et :