Copilot commented on code in PR #13131:
URL: https://github.com/apache/trafficserver/pull/13131#discussion_r3169422166


##########
plugins/experimental/url_sig/url_sig_config.cc:
##########
@@ -0,0 +1,155 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <memory>
+#include <sstream>
+
+std::unique_ptr<UrlSigConfig>
+load_config(std::istream &input, std::string &error)
+{
+  auto cfg     = std::make_unique<UrlSigConfig>();
+  int  line_no = 0; // incremented per line
+
+  std::string line;
+  while (std::getline(input, line)) {
+    line_no++;
+
+    // Skip empty lines and comments.
+    if (line.empty() || line[0] == '#') {
+      continue;
+    }
+
+    auto const eq_pos = line.find('=');
+    if (eq_pos == std::string::npos) {
+      // Not a fatal error, just skip like original.
+      continue;
+    }
+
+    std::string_view key_part(line.data(), eq_pos);
+    std::string_view value_part(line.data() + eq_pos + 1, line.size() - eq_pos 
- 1);
+
+    // Trim trailing whitespace from key.
+    while (!key_part.empty() && (key_part.back() == ' ' || key_part.back() == 
'\t')) {
+      key_part.remove_suffix(1);
+    }
+    // Trim leading whitespace from value.
+    while (!value_part.empty() && (value_part.front() == ' ' || 
value_part.front() == '\t')) {
+      value_part.remove_prefix(1);
+    }
+    // Trim trailing whitespace/newline from value.
+    while (!value_part.empty() &&
+           (value_part.back() == ' ' || value_part.back() == '\t' || 
value_part.back() == '\n' || value_part.back() == '\r')) {
+      value_part.remove_suffix(1);
+    }
+
+    if (key_part.starts_with("key")) {
+      std::string_view const index_str = key_part.substr(3);
+      int                    keynum    = -1;
+
+      if (index_str == "0") {
+        keynum = 0;
+      } else {
+        auto [ptr, ec] = std::from_chars(index_str.data(), index_str.data() + 
index_str.size(), keynum);
+        if (ec != std::errc{} || keynum == 0) {
+          keynum = -1;
+        }
+      }
+
+      if (keynum < 0) {
+        error = "Key number is NaN at line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (static_cast<int>(value_part.size()) >= MAX_KEY_LEN) {
+        error = "Maximum key length (" + std::to_string(MAX_KEY_LEN - 1) + ") 
exceeded on line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (keynum >= static_cast<int>(cfg->keys.size())) {
+        cfg->keys.resize(keynum + 1);
+      }
+      cfg->keys[keynum] = std::string(value_part);

Review Comment:
   This resizes `cfg->keys` to `keynum + 1` with no upper bound, so a typo like 
`key999999 = ...` can cause large memory growth. The legacy plugin limited keys 
to `key0`..`key15` and the docs still describe that; consider enforcing a hard 
maximum to preserve behavior and avoid misconfiguration issues.



##########
plugins/experimental/url_sig/url_sig_verify.cc:
##########
@@ -0,0 +1,535 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <ctime>
+#include <vector>
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+namespace
+{
+
+/// Find parameter value in a delimited parameter string.
+/// @param params parameter string (query or semicolon-delimited).
+/// @param key parameter key (e.g. "E").
+/// @param delim delimiter between parameters ('&' or ';').
+/// @return value portion after "key=", empty if not found.
+std::string_view
+find_param(std::string_view const params, std::string_view const key, char 
const delim)
+{
+  std::string const search = std::string(key) + "=";
+  auto              pos    = params.find(search);
+
+  // Ensure it's at start or preceded by delimiter.
+  while (pos != std::string_view::npos) {
+    if (pos == 0 || params[pos - 1] == delim) {
+      auto const val_start = pos + search.size();
+      auto const val_end   = params.find(delim, val_start);
+      if (val_end == std::string_view::npos) {
+        return params.substr(val_start);
+      }
+      return params.substr(val_start, val_end - val_start);
+    }
+    pos = params.find(search, pos + 1);
+  }
+  return {};
+}
+
+/// Compute HMAC signature and return hex string.
+std::string
+compute_hmac(int const algorithm, std::string_view const key, std::string_view 
const data)
+{
+  EVP_MD const *md           = nullptr;
+  unsigned int  expected_len = 0;
+
+  switch (algorithm) {
+  case USIG_HMAC_SHA1:
+    md           = EVP_sha1();
+    expected_len = SHA1_SIG_SIZE;
+    break;
+  case USIG_HMAC_MD5:
+    md           = EVP_md5();
+    expected_len = MD5_SIG_SIZE;
+    break;
+  default:
+    return {};
+  }
+
+  unsigned char sig[MAX_SIG_SIZE + 1];
+  unsigned int  sig_len = 0;
+
+  HMAC(md, key.data(), static_cast<int>(key.size()), reinterpret_cast<unsigned 
char const *>(data.data()), data.size(), sig,
+       &sig_len);
+
+  if (sig_len != expected_len) {
+    return {};
+  }
+
+  std::string hex;
+  hex.reserve(sig_len * 2);
+  for (unsigned int i = 0; i < sig_len; i++) {
+    char buf[3];
+    snprintf(buf, sizeof(buf), "%02x", sig[i]);
+    hex.append(buf, 2);
+  }

Review Comment:
   `compute_hmac()` uses `snprintf()` but this file doesn't include `<cstdio>`, 
which can cause a build failure depending on transitive includes. Add the 
missing header.



##########
plugins/experimental/url_sig/url_sig_verify.cc:
##########
@@ -0,0 +1,535 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <ctime>
+#include <vector>
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+namespace
+{
+
+/// Find parameter value in a delimited parameter string.
+/// @param params parameter string (query or semicolon-delimited).
+/// @param key parameter key (e.g. "E").
+/// @param delim delimiter between parameters ('&' or ';').
+/// @return value portion after "key=", empty if not found.
+std::string_view
+find_param(std::string_view const params, std::string_view const key, char 
const delim)
+{
+  std::string const search = std::string(key) + "=";
+  auto              pos    = params.find(search);
+
+  // Ensure it's at start or preceded by delimiter.
+  while (pos != std::string_view::npos) {
+    if (pos == 0 || params[pos - 1] == delim) {
+      auto const val_start = pos + search.size();
+      auto const val_end   = params.find(delim, val_start);
+      if (val_end == std::string_view::npos) {
+        return params.substr(val_start);
+      }
+      return params.substr(val_start, val_end - val_start);
+    }
+    pos = params.find(search, pos + 1);
+  }
+  return {};
+}
+
+/// Compute HMAC signature and return hex string.
+std::string
+compute_hmac(int const algorithm, std::string_view const key, std::string_view 
const data)
+{
+  EVP_MD const *md           = nullptr;
+  unsigned int  expected_len = 0;
+
+  switch (algorithm) {
+  case USIG_HMAC_SHA1:
+    md           = EVP_sha1();
+    expected_len = SHA1_SIG_SIZE;
+    break;
+  case USIG_HMAC_MD5:
+    md           = EVP_md5();
+    expected_len = MD5_SIG_SIZE;
+    break;
+  default:
+    return {};
+  }
+
+  unsigned char sig[MAX_SIG_SIZE + 1];
+  unsigned int  sig_len = 0;
+
+  HMAC(md, key.data(), static_cast<int>(key.size()), reinterpret_cast<unsigned 
char const *>(data.data()), data.size(), sig,
+       &sig_len);
+
+  if (sig_len != expected_len) {
+    return {};
+  }
+
+  std::string hex;
+  hex.reserve(sig_len * 2);
+  for (unsigned int i = 0; i < sig_len; i++) {
+    char buf[3];
+    snprintf(buf, sizeof(buf), "%02x", sig[i]);
+    hex.append(buf, 2);
+  }
+  return hex;
+}
+
+/// Split a string_view by delimiter, returning vector of parts.
+std::vector<std::string_view>
+split(std::string_view const sv_in, char const delim)
+{
+  std::vector<std::string_view> result;
+  std::string_view              sv = sv_in;
+  while (!sv.empty()) {
+    auto pos = sv.find(delim);
+    if (pos == std::string_view::npos) {
+      result.push_back(sv);
+      break;
+    }
+    result.push_back(sv.substr(0, pos));
+    sv.remove_prefix(pos + 1);
+  }
+  return result;
+}
+
+/// Base64 decode (minimal implementation for path params).
+/// Uses OpenSSL EVP_DecodeBlock.
+std::string
+base64_decode(std::string_view const input)
+{
+  if (input.empty()) {
+    return {};
+  }
+
+  // EVP_DecodeBlock needs null-terminated input; output can be up to 3/4 * 
input_len.
+  std::string padded(input);
+  // Pad to multiple of 4.
+  while (padded.size() % 4 != 0) {
+    padded.push_back('=');
+  }
+
+  std::vector<unsigned char> out(padded.size());
+  int const                  decoded_len =
+    EVP_DecodeBlock(out.data(), reinterpret_cast<unsigned char const 
*>(padded.data()), static_cast<int>(padded.size()));
+
+  if (decoded_len < 0) {

Review Comment:
   `EVP_DecodeBlock()` only supports the standard Base64 alphabet; the legacy 
plugin accepted URL-safe Base64 (`-`/`_`) via `TSBase64Decode` / 
`ats_base64_decode`. Since the signing tools/docs use URL-safe Base64 for 
path-param mode, this will break existing signed URLs unless you translate 
`-`→`+` and `_`→`/` (and keep the padding behavior) before decoding.



##########
plugins/experimental/url_sig/url_sig_config.cc:
##########
@@ -0,0 +1,155 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <memory>
+#include <sstream>
+
+std::unique_ptr<UrlSigConfig>
+load_config(std::istream &input, std::string &error)
+{
+  auto cfg     = std::make_unique<UrlSigConfig>();
+  int  line_no = 0; // incremented per line
+
+  std::string line;
+  while (std::getline(input, line)) {
+    line_no++;
+
+    // Skip empty lines and comments.
+    if (line.empty() || line[0] == '#') {
+      continue;
+    }
+
+    auto const eq_pos = line.find('=');
+    if (eq_pos == std::string::npos) {
+      // Not a fatal error, just skip like original.
+      continue;
+    }
+
+    std::string_view key_part(line.data(), eq_pos);
+    std::string_view value_part(line.data() + eq_pos + 1, line.size() - eq_pos 
- 1);
+
+    // Trim trailing whitespace from key.
+    while (!key_part.empty() && (key_part.back() == ' ' || key_part.back() == 
'\t')) {
+      key_part.remove_suffix(1);
+    }
+    // Trim leading whitespace from value.
+    while (!value_part.empty() && (value_part.front() == ' ' || 
value_part.front() == '\t')) {
+      value_part.remove_prefix(1);
+    }
+    // Trim trailing whitespace/newline from value.
+    while (!value_part.empty() &&
+           (value_part.back() == ' ' || value_part.back() == '\t' || 
value_part.back() == '\n' || value_part.back() == '\r')) {
+      value_part.remove_suffix(1);
+    }
+
+    if (key_part.starts_with("key")) {
+      std::string_view const index_str = key_part.substr(3);
+      int                    keynum    = -1;
+
+      if (index_str == "0") {
+        keynum = 0;
+      } else {
+        auto [ptr, ec] = std::from_chars(index_str.data(), index_str.data() + 
index_str.size(), keynum);
+        if (ec != std::errc{} || keynum == 0) {
+          keynum = -1;
+        }
+      }
+
+      if (keynum < 0) {
+        error = "Key number is NaN at line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (static_cast<int>(value_part.size()) >= MAX_KEY_LEN) {
+        error = "Maximum key length (" + std::to_string(MAX_KEY_LEN - 1) + ") 
exceeded on line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (keynum >= static_cast<int>(cfg->keys.size())) {
+        cfg->keys.resize(keynum + 1);
+      }
+      cfg->keys[keynum] = std::string(value_part);
+
+    } else if (key_part == "error_url") {
+      // Format: error_url = <status_code> <url>
+      // e.g. "error_url = 403" or "error_url = 302 http://example.com/error";
+      int status_code      = 0;
+      auto const [ptr, ec] = std::from_chars(value_part.data(), 
value_part.data() + value_part.size(), status_code);
+      if (ec != std::errc{}) {
+        continue;
+      }
+
+      if (status_code == 302) {
+        cfg->err_status = UrlSigErrStatus::MOVED_TEMPORARILY;
+        // Skip past status code and whitespace to get URL.
+        std::string_view remainder(ptr, static_cast<size_t>(value_part.data() 
+ value_part.size() - ptr));
+        while (!remainder.empty() && (remainder.front() == ' ' || 
remainder.front() == '\t')) {
+          remainder.remove_prefix(1);
+        }
+        cfg->err_url = std::string(remainder);
+      } else {
+        cfg->err_status = UrlSigErrStatus::FORBIDDEN;
+        cfg->err_url.clear();

Review Comment:
   For `error_url`, any status other than 302 is currently treated as 403 
(FORBIDDEN). The legacy implementation rejected unsupported status codes at 
instance creation, so configs like `error_url = 404` would now silently change 
meaning. Consider explicitly accepting only 302/403 (or erroring on anything 
else) to keep compatibility.
   ```suggestion
         } else if (status_code == 403) {
           cfg->err_status = UrlSigErrStatus::FORBIDDEN;
           cfg->err_url.clear();
         } else {
           error = "Line " + std::to_string(line_no) + ": unsupported error_url 
status code " + std::to_string(status_code);
           return nullptr;
   ```



##########
doc/admin-guide/plugins/url_sig.en.rst:
##########
@@ -48,243 +84,258 @@ step process. First, you must generate a configuration 
file containing the list
 of valid signing keys. Secondly, you must indicate to |TS| which URLs require
 valid signatures.
 
-Generating Keys
----------------
-
-This plugin comes with two Perl scripts which assist in generating signatures.
-For |TS| to verify URL signatures, it must have the relevant keys. Using the
-provided *genkeys* script, you can generate a suitable configuration file::
-
-    ./genkeys.pl > url_sig.config
-
-The resulting file will look something like the following, with the actual keys
-differing (as they are generated randomly each time the script is run)::
-
-    key0 = YwG7iAxDo6Gaa38KJOceV4nsxiAJZ3DS
-    key1 = nLE3SZKRgaNM9hLz_HnIvrCw_GtTUJT1
-    key2 = YicZbmr6KlxfxPTJ3p9vYhARdPQ9WJYZ
-    key3 = DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
-    key4 = C1r6R6MINoQd5YSH25fU66tuRhhz3fs_
-    key5 = l4dxe6YEpYbJtyiOmX5mafhwKImC5kej
-    key6 = ekKNHXu9_oOC5eqIGJVxV0vI9FYvKVya
-    key7 = BrjibTmpTTuhMHqphkQAuCWA0Zg97WQB
-    key8 = rEtWLb1jcYoq9VG8Z8TKgX4GxBuro20J
-    key9 = mrP_6ibDBG4iYpfDB6W8yn3ZyGmdwc6M
-    key10 = tbzoTTGZXPLcvpswCQCYz1DAIZcAOGyX
-    key11 = lWsn6gUeSEW79Fk2kwKVfzhVG87EXLna
-    key12 = Riox0SmGtBWsrieLUHVWtpj18STM4MP1
-    key13 = kBsn332B7yG3HdcV7Tw51pkvHod7_84l
-    key14 = hYI4GUoIlZRf0AyuXkT3QLvBMEoFxkma
-    key15 = EIgJKwIR0LU9CUeTUdVtjMgGmxeCIbdg
-    error_url = 403
-
-This file should be placed in your |TS| ``etc`` directory, with permissions and
-ownership set such that only the |TS| processes may read it.
+Config File Format
+------------------
+
+The config file is a simple ``key = value`` text file. Lines starting with
+``#`` are comments. The file must contain at least one key and an ``error_url``
+line.
+
+=================  =================================  
=============================================
+Key                Value                              Description
+=================  =================================  
=============================================
+``key0``–``key15`` string (max 255 chars)             Shared HMAC signing keys.
+``error_url``      ``403`` or ``302 <redirect_url>``  Response for failed 
validation.
+``sig_anchor``     string                             Anchor name for 
path-parameter mode.
+``excl_regex``     PCRE regex pattern                 URLs matching skip 
signature validation.
+``url_type``       ``pristine`` or ``remap``          Which URL to validate 
against (default: remap).
+``ignore_expiry``  ``true``                           Disable expiration 
checking (debug only).
+=================  =================================  
=============================================
+
+Example configuration::
+
+   # Shared signing keys (up to 16, index 0–15).
+   key0 = YwG7iAxDo6Gaa38KJOceV4nsxiAJZ3DS
+   key1 = nLE3SZKRgaNM9hLz_HnIvrCw_GtTUJT1
+   key2 = YicZbmr6KlxfxPTJ3p9vYhARdPQ9WJYZ
+   key3 = DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
+   key4 = C1r6R6MINoQd5YSH25fU66tuRhhz3fs_
+   key5 = l4dxe6YEpYbJtyiOmX5mafhwKImC5kej
+   key6 = ekKNHXu9_oOC5eqIGJVxV0vI9FYvKVya
+   key7 = BrjibTmpTTuhMHqphkQAuCWA0Zg97WQB
+   key8 = rEtWLb1jcYoq9VG8Z8TKgX4GxBuro20J
+   key9 = mrP_6ibDBG4iYpfDB6W8yn3ZyGmdwc6M
+   key10 = tbzoTTGZXPLcvpswCQCYz1DAIZcAOGyX
+   key11 = lWsn6gUeSEW79Fk2kwKVfzhVG87EXLna
+   key12 = Riox0SmGtBWsrieLUHVWtpj18STM4MP1
+   key13 = kBsn332B7yG3HdcV7Tw51pkvHod7_84l
+   key14 = hYI4GUoIlZRf0AyuXkT3QLvBMEoFxkma
+   key15 = EIgJKwIR0LU9CUeTUdVtjMgGmxeCIbdg
+   error_url = 403
+
+Additional options example::
+
+   sig_anchor = urlsig
+   excl_regex = (/crossdomain.xml|/clientaccesspolicy.xml|/test.html)
+   url_type = pristine
+   ignore_expiry = true
 
 .. important::
 
    The configuration file contains the full set of secret keys which |TS| will
    be using to verify incoming requests, and as such should be treated with as
    much care as any other file in your infrastructure containing keys, pass
-   phrases, and other sensitive data. Unauthorized access to the contents of
-   this file will allow others to spoof requests from your signing portal, thus
-   defeating the entire purpose of using a signing portal in the first place.
+   phrases, and other sensitive data.
 
-Requiring Signatures on URLs
-----------------------------
+Generating Keys
+---------------
 
-To require a valid signature, verified by a key from the list you generated
-earlier, modify your :file:`remap.config` configuration to include this plugin
-for any rules you wish it to affect.
+The plugin ships with a Go tool to generate random keys. Run it with
+``go run``::
 
-Two parameters for each remap rule are required, and a third one is optional::
+   go run genkeys.go > /etc/trafficserver/url_sig.config
 
-    @plugin=url_sig.so @pparam=<config file> @pparam=pristineurl
+No Go modules or dependencies are needed — the file uses only the Go standard
+library.
 
-The first simply enables this plugin for the rule. The second specifies the
-location of the configuration file containing your signing keys.  The third 
one,
-if present, causes authentication to be performed on the original (pristine) 
URL
-as received from the client. (The value of the parameter is not case 
sensitive.)
+The original Perl script ``genkeys.pl`` is still available for backward
+compatibility but requires the ``Digest::SHA`` and ``MIME::Base64::URLSafe``
+modules.
 

Review Comment:
   The documentation says `genkeys.pl` is still available for backward 
compatibility, but this PR deletes `genkeys.pl`. Please update the docs (or 
keep the script) so the shipped artifacts match the guidance.
   ```suggestion
   
   ```



##########
plugins/experimental/url_sig/url_sig_config.cc:
##########
@@ -0,0 +1,155 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <memory>
+#include <sstream>
+
+std::unique_ptr<UrlSigConfig>
+load_config(std::istream &input, std::string &error)
+{
+  auto cfg     = std::make_unique<UrlSigConfig>();
+  int  line_no = 0; // incremented per line
+
+  std::string line;
+  while (std::getline(input, line)) {
+    line_no++;
+
+    // Skip empty lines and comments.
+    if (line.empty() || line[0] == '#') {
+      continue;
+    }
+
+    auto const eq_pos = line.find('=');
+    if (eq_pos == std::string::npos) {
+      // Not a fatal error, just skip like original.
+      continue;
+    }
+
+    std::string_view key_part(line.data(), eq_pos);
+    std::string_view value_part(line.data() + eq_pos + 1, line.size() - eq_pos 
- 1);
+
+    // Trim trailing whitespace from key.
+    while (!key_part.empty() && (key_part.back() == ' ' || key_part.back() == 
'\t')) {
+      key_part.remove_suffix(1);
+    }
+    // Trim leading whitespace from value.
+    while (!value_part.empty() && (value_part.front() == ' ' || 
value_part.front() == '\t')) {
+      value_part.remove_prefix(1);
+    }
+    // Trim trailing whitespace/newline from value.
+    while (!value_part.empty() &&
+           (value_part.back() == ' ' || value_part.back() == '\t' || 
value_part.back() == '\n' || value_part.back() == '\r')) {
+      value_part.remove_suffix(1);
+    }
+
+    if (key_part.starts_with("key")) {
+      std::string_view const index_str = key_part.substr(3);
+      int                    keynum    = -1;
+
+      if (index_str == "0") {
+        keynum = 0;
+      } else {
+        auto [ptr, ec] = std::from_chars(index_str.data(), index_str.data() + 
index_str.size(), keynum);
+        if (ec != std::errc{} || keynum == 0) {
+          keynum = -1;
+        }
+      }
+
+      if (keynum < 0) {
+        error = "Key number is NaN at line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (static_cast<int>(value_part.size()) >= MAX_KEY_LEN) {
+        error = "Maximum key length (" + std::to_string(MAX_KEY_LEN - 1) + ") 
exceeded on line " + std::to_string(line_no);
+        return nullptr;
+      }
+
+      if (keynum >= static_cast<int>(cfg->keys.size())) {
+        cfg->keys.resize(keynum + 1);
+      }
+      cfg->keys[keynum] = std::string(value_part);
+
+    } else if (key_part == "error_url") {
+      // Format: error_url = <status_code> <url>
+      // e.g. "error_url = 403" or "error_url = 302 http://example.com/error";
+      int status_code      = 0;
+      auto const [ptr, ec] = std::from_chars(value_part.data(), 
value_part.data() + value_part.size(), status_code);
+      if (ec != std::errc{}) {
+        continue;
+      }
+
+      if (status_code == 302) {
+        cfg->err_status = UrlSigErrStatus::MOVED_TEMPORARILY;
+        // Skip past status code and whitespace to get URL.
+        std::string_view remainder(ptr, static_cast<size_t>(value_part.data() 
+ value_part.size() - ptr));
+        while (!remainder.empty() && (remainder.front() == ' ' || 
remainder.front() == '\t')) {
+          remainder.remove_prefix(1);
+        }
+        cfg->err_url = std::string(remainder);
+      } else {
+        cfg->err_status = UrlSigErrStatus::FORBIDDEN;
+        cfg->err_url.clear();
+      }
+
+    } else if (key_part == "sig_anchor") {
+      cfg->sig_anchor = std::string(value_part);
+
+    } else if (key_part == "excl_regex") {
+      // excl_regex is handled by the caller setting excl_regex_match after 
load.
+      // Store raw pattern in sig_anchor-like fashion? No — we just note it 
here.
+      // The ATS adapter will compile the regex and set the callback.
+      // Store the pattern string temporarily in a way the adapter can 
retrieve.
+      // Actually, let's just skip it here — the adapter reads the file itself 
for regex.
+      // But wait, we want core to be self-contained for config...
+      // Compromise: we note the pattern but don't compile it (no regex dep in 
core).
+      // The caller can re-read or we store it.
+      // For now: store nothing, adapter handles regex separately by reading 
config.
+      // This matches original behavior where excl_regex was compiled with ATS 
Regex.
+
+    } else if (key_part == "ignore_expiry") {
+      cfg->ignore_expiry = (value_part == "true");
+
+    } else if (key_part == "url_type") {
+      cfg->pristine_url_flag = (value_part == "pristine");
+    }
+    // Unknown keys silently ignored (matches original for forward compat).
+  }
+
+  // Validate config.
+  switch (cfg->err_status) {
+  case UrlSigErrStatus::MOVED_TEMPORARILY:
+    if (cfg->err_url.empty()) {
+      error = "Invalid config, err_status == 302, but err_url is empty";

Review Comment:
   Because `err_status` defaults to FORBIDDEN and this validation only checks 
consistency, `load_config()` will succeed even if the config file omits the 
required `error_url` line (and even if no keys are provided). The legacy plugin 
required `error_url` to be set during parsing. Consider tracking whether 
`error_url` (and at least one key) was seen and failing if not.



##########
plugins/experimental/url_sig/url_sig.cc:
##########
@@ -77,766 +74,174 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int 
errbuf_size)
   return TS_SUCCESS;
 }
 
-// To force a config file reload touch remap.config and do a "traffic_ctl 
config reload"
 TSReturnCode
 TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int 
errbuf_size)
 {
-  char config_filepath_buf[PATH_MAX], *config_file;
-
-  if ((argc < 3) || (argc > 4)) {
+  if (argc < 3 || 4 < argc) {
     snprintf(errbuf, errbuf_size,
-             "[TSRemapNewInstance] - Argument count wrong (%d)... config file 
path is required first pparam, \"pristineurl\" is"
+             "[TSRemapNewInstance] - Argument count wrong (%d)... config file 
path is required first pparam, \"pristineurl\" is "
              "optional second pparam.",
              argc);
     return TS_ERROR;
   }
+
   Dbg(dbg_ctl, "Initializing remap function of %s -> %s with config from %s", 
argv[0], argv[1], argv[2]);
 
-  if (argv[2][0] == '/') {
-    config_file = argv[2];
-  } else {
+  // Resolve config file path.
+  char        config_filepath_buf[PATH_MAX];
+  char const *config_file = argv[2];
+  if (argv[2][0] != '/') {
     snprintf(config_filepath_buf, sizeof(config_filepath_buf), "%s/%s", 
TSConfigDirGet(), argv[2]);
     config_file = config_filepath_buf;
   }
+
   Dbg(dbg_ctl, "config file name: %s", config_file);
-  FILE *file = fopen(config_file, "r");
-  if (file == nullptr) {
+
+  std::ifstream file(config_file);
+  if (!file.is_open()) {
     snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Error opening file 
%s", config_file);
     return TS_ERROR;
   }
 
-  char line[300];
-  int  line_no = 0;
-  int  keynum;
-  bool eat_comment = false;
-
-  auto cfg = std::make_unique<config>();
-
-  while (fgets(line, sizeof(line), file) != nullptr) {
-    Dbg(dbg_ctl, "LINE: %s (%d)", line, (int)strlen(line));
-    line_no++;
+  std::string error;
+  auto        cfg = load_config(file, error);
+  if (!cfg) {
+    snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - %s", error.c_str());
+    return TS_ERROR;
+  }
 
-    if (eat_comment) {
-      // Check if final char is EOL, if so we are done eating
-      if (line[strlen(line) - 1] == '\n') {
-        eat_comment = false;
-      }
+  // Handle excl_regex: re-read file to find the pattern.
+  file.clear();
+  file.seekg(0);
+  std::string line;
+  while (std::getline(file, line)) {
+    if (line.empty() || line[0] == '#') {
       continue;
     }
-    if (line[0] == '#' || strlen(line) <= 1) {
-      // Check if we have a comment longer than the full buffer if no EOL
-      if (line[strlen(line) - 1] != '\n') {
-        eat_comment = true;
-      }
+    auto eq = line.find('=');
+    if (eq == std::string::npos) {
       continue;
     }
-    char *pos = strchr(line, '=');
-    if (pos == nullptr) {
-      TSError("[url_sig] Error parsing line %d of file %s (%s)", line_no, 
config_file, line);
-      continue;
-    }
-    *pos        = '\0';
-    char *value = pos + 1;
-    while (isspace(*value)) { // remove whitespace
-      value++;
-    }
-    pos = strchr(value, '\n'); // remove the new line, terminate the string
-    if (pos != nullptr) {
-      *pos = '\0';
-    }
-    if (pos == nullptr || strlen(value) >= MAX_KEY_LEN) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Maximum key length 
(%d) exceeded on line %d", MAX_KEY_LEN - 1, line_no);
-      fclose(file);
-      return TS_ERROR;
+    std::string_view key(line.data(), eq);
+    // Trim trailing whitespace from key.
+    while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
+      key.remove_suffix(1);
     }
-    if (strncmp(line, "key", 3) == 0) {
-      if (strncmp(line + 3, "0", 1) == 0) {
-        keynum = 0;
-      } else {
-        Dbg(dbg_ctl, ">>> %s <<<", line + 3);
-        keynum = atoi(line + 3);
-        if (keynum == 0) {
-          keynum = -1; // Not a Number
-        }
-      }
-      Dbg(dbg_ctl, "key number %d == %s", keynum, value);
-      if (keynum >= MAX_KEY_NUM || keynum < 0) {
-        snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Key number (%d) 
>= MAX_KEY_NUM (%d) or NaN", keynum, MAX_KEY_NUM);
-        fclose(file);
-        return TS_ERROR;
+    if (key == "excl_regex") {
+      std::string_view value(line.data() + eq + 1, line.size() - eq - 1);
+      // Trim whitespace.
+      while (!value.empty() && (value.front() == ' ' || value.front() == 
'\t')) {
+        value.remove_prefix(1);
       }
-      snprintf(&cfg->keys[keynum][0], MAX_KEY_LEN, "%s", value);
-    } else if (strncmp(line, "error_url", 9) == 0) {
-      if (atoi(value)) {
-        cfg->err_status = static_cast<TSHttpStatus>(atoi(value));
+      while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || 
value.back() == '\n')) {
+        value.remove_suffix(1);
       }
-      value += 3;
-      while (isspace(*value)) {
-        value++;
-      }
-      if (cfg->err_status == TS_HTTP_STATUS_MOVED_TEMPORARILY) {
-        cfg->err_url = value;
-      } else {
-        cfg->err_url.clear();
-      }
-    } else if (strncmp(line, "sig_anchor", 10) == 0) {
-      cfg->sig_anchor = value;
-    } else if (strncmp(line, "excl_regex", 10) == 0) {
-      // Compile regex.
-      std::string error;
-      int         erroffset = 0;
 
-      if (cfg->excl_regex) {
-        Dbg(dbg_ctl, "Skipping duplicate excl_regex");
-        continue;
-      }
-
-      cfg->excl_regex = std::make_unique<Regex>();
-      if (!cfg->excl_regex->compile(value, error, erroffset, 0)) {
-        Dbg(dbg_ctl, "Regex compilation failed with error (%s) at character 
%d", error.c_str(), erroffset);
-        cfg->excl_regex.reset();
-      }
-    } else if (strncmp(line, "ignore_expiry", 13) == 0) {
-      if (strncmp(value, "true", 4) == 0) {
-        cfg->ignore_expiry = true;
-        TSError("[url_sig] Plugin IGNORES sig expiration");
-      }
-    } else if (strncmp(line, "url_type", 8) == 0) {
-      if (strncmp(value, "pristine", 8) == 0) {
-        cfg->pristine_url_flag = true;
-        Dbg(dbg_ctl, "Pristine URLs (from config) will be used");
+      auto const  regex = std::make_shared<Regex>();
+      std::string re_error;
+      int         erroffset = 0;
+      if (regex->compile(std::string(value).c_str(), re_error, erroffset, 0)) {
+        cfg->excl_regex_match = [regex](std::string_view url) -> bool { return 
regex->exec(url); };
+      } else {
+        Dbg(dbg_ctl, "Regex compilation failed with error (%s) at character 
%d", re_error.c_str(), erroffset);
       }
-    } else {
-      TSError("[url_sig] Error parsing line %d of file %s (%s)", line_no, 
config_file, line);
+      break; // Only first excl_regex used.
     }
   }
+  file.close();
 
-  fclose(file);
-
-  if (argc > 3) {
+  // Handle pristineurl pparam override.
+  if (4 <= argc) {
     if (strcasecmp(argv[3], "pristineurl") == 0) {
       cfg->pristine_url_flag = true;
       Dbg(dbg_ctl, "Pristine URLs (from args) will be used");
-
     } else {
       snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - second pparam (if 
present) must be pristineurl");
       return TS_ERROR;
     }
   }
 
-  switch (cfg->err_status) {
-  case TS_HTTP_STATUS_MOVED_TEMPORARILY:
-    if (cfg->err_url.empty()) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Invalid config, 
err_status == 302, but err_url is empty");
-      return TS_ERROR;
-    }
-    break;
-  case TS_HTTP_STATUS_FORBIDDEN:
-    if (!cfg->err_url.empty()) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Invalid config, 
err_status == 403, but err_url is not empty");
-      return TS_ERROR;
-    }
-    break;
-  default:
-    snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Return code %d not 
supported", cfg->err_status);
-    return TS_ERROR;
+  if (cfg->ignore_expiry) {
+    TSError("[url_sig] Plugin IGNORES sig expiration");
   }
 
-  // Transfer ownership to ih which will later be deleted in 
TSRemapDeleteInstance.
-  *ih = (void *)cfg.release();
+  *ih = static_cast<void *>(cfg.release());
   return TS_SUCCESS;
 }
 
 void
 TSRemapDeleteInstance(void *ih)
 {
-  auto *cfg = static_cast<config *>(ih);
-  delete cfg;
-}
-
-static void
-err_log(const char *url, int url_len, const char *msg)
-{
-  if (msg && url) {
-    Dbg(dbg_ctl, "Test");
-
-    Dbg(dbg_ctl, "[URL=%.*s]: %s", url_len, url, msg);
-    TSError("[url_sig] [URL=%.*s]: %s", url_len, url, msg); // This goes to 
error.log
-  } else {
-    TSError("[url_sig] Invalid err_log request");
-  }
-}
-
-// See the README.  All Signing parameters must be concatenated to the end
-// of the url and any application query parameters.
-static char *
-getAppQueryString(const char *query_string, int query_length)
-{
-  int   done = 0;
-  char *p;
-  char  buf[MAX_QUERY_LEN + 1];
-
-  if (query_length > MAX_QUERY_LEN) {
-    Dbg(dbg_ctl, "Cannot process the query string as the length exceeds %d 
bytes", MAX_QUERY_LEN);
-    return nullptr;
-  }
-  memset(buf, 0, sizeof(buf));
-  memcpy(buf, query_string, query_length);
-  p = buf;
-
-  Dbg(dbg_ctl, "query_string: %s, query_length: %d", query_string, 
query_length);
-
-  do {
-    switch (*p) {
-    case 'A':
-    case 'C':
-    case 'E':
-    case 'K':
-    case 'P':
-    case 'S':
-      done = 1;
-      if ((p > buf) && (*(p - 1) == '&')) {
-        *(p - 1) = '\0';
-      } else {
-        (*p = '\0');
-      }
-      break;
-    default:
-      p = strchr(p, '&');
-      if (p == nullptr) {
-        done = 1;
-      } else {
-        p++;
-      }
-      break;
-    }
-  } while (!done);
-
-  if (strlen(buf) > 0) {
-    p = TSstrdup(buf);
-    return p;
-  } else {
-    return nullptr;
-  }
-}
-
-/** fixedBufferWrite safely writes no more than *dest_len bytes to *dest_end
- * from src. If copying src_len bytes to *dest_len would overflow, it returns
- * zero. *dest_end is advanced and *dest_len is decremented to account for the
- * written data. No null-terminators are written automatically (though they
- * could be copied with data).
- */
-static int
-fixedBufferWrite(char **dest_end, int *dest_len, const char *src, int src_len)
-{
-  if (src_len > *dest_len) {
-    return 0;
-  }
-  memcpy(*dest_end, src, src_len);
-  *dest_end += src_len;
-  *dest_len -= src_len;
-  return 1;
-}
-
-static char *
-urlParse(char const *const url_in, char const *anchor, char *new_path_seg, int 
new_path_seg_len, char *signed_seg,
-         unsigned int signed_seg_len)
-{
-  char         *segment[MAX_SEGMENTS];
-  char          url[8192]            = {'\0'};
-  unsigned char decoded_string[2048] = {'\0'};
-  char          new_url[8192]; /* new_url is not null_terminated */
-  char         *p = nullptr, *sig_anchor = nullptr, *saveptr = nullptr;
-  int           i = 0, numtoks = 0, sig_anchor_seg = 0;
-  size_t        decoded_len = 0;
-
-  strncat(url, url_in, sizeof(url) - strlen(url) - 1);
-
-  char *new_url_end      = new_url;
-  int   new_url_len_left = sizeof(new_url);
-
-  char *new_path_seg_end      = new_path_seg;
-  int   new_path_seg_len_left = new_path_seg_len;
-
-  char *skip = strchr(url, ':');
-  if (!skip || skip[1] != '/' || skip[2] != '/') {
-    return nullptr;
-  }
-  skip += 3;
-  // preserve the scheme in the new_url.
-  if (!fixedBufferWrite(&new_url_end, &new_url_len_left, url, skip - url)) {
-    TSError("insufficient space to copy schema into new_path_seg buffer.");
-    return nullptr;
-  }
-  Dbg(dbg_ctl, "%s:%d - new_url: %.*s\n", __FILE__, __LINE__, 
(int)(new_url_end - new_url), new_url);
-
-  // parse the url.
-  if ((p = strtok_r(skip, "/", &saveptr)) != nullptr) {
-    segment[numtoks++] = p;
-    do {
-      p = strtok_r(nullptr, "/", &saveptr);
-      if (p != nullptr) {
-        segment[numtoks] = p;
-        if (anchor != nullptr && sig_anchor_seg == 0) {
-          // look for the signed anchor string.
-          if ((sig_anchor = strcasestr(segment[numtoks], anchor)) != nullptr) {
-            // null terminate this segment just before he signing anchor, this 
should be a ';'.
-            *(sig_anchor - 1) = '\0';
-            if ((sig_anchor = strstr(sig_anchor, "=")) != nullptr) {
-              *sig_anchor = '\0';
-              sig_anchor++;
-              sig_anchor_seg = numtoks;
-            }
-          }
-        }
-        numtoks++;
-      }
-    } while (p != nullptr && numtoks < MAX_SEGMENTS);
-  } else {
-    return nullptr;
-  }
-  if ((numtoks >= MAX_SEGMENTS) || (numtoks < 3)) {
-    return nullptr;
-  }
-
-  // create a new path string for later use when dealing with query parameters.
-  // this string will not contain the signing parameters.  skips the fqdn by
-  // starting with segment 1.
-  for (i = 1; i < numtoks; i++) {
-    // if no signing anchor is found, skip the signed parameters segment.
-    if (sig_anchor == nullptr && i == numtoks - 2) {
-      // the signing parameters when no signature anchor is found, should be 
in the
-      // last path segment so skip them.
-      continue;
-    }
-    if (!fixedBufferWrite(&new_path_seg_end, &new_path_seg_len_left, 
segment[i], strlen(segment[i]))) {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-      return nullptr;
-    }
-    if (i != numtoks - 1) {
-      if (!fixedBufferWrite(&new_path_seg_end, &new_path_seg_len_left, "/", 
1)) {
-        TSError("insufficient space to copy into new_path_seg buffer.");
-        return nullptr;
-      }
-    }
-  }
-  *new_path_seg_end = '\0';
-  Dbg(dbg_ctl, "new_path_seg: %s", new_path_seg);
-
-  // save the encoded signing parameter data
-  if (sig_anchor != nullptr) { // a signature anchor string was found.
-    if (strlen(sig_anchor) < signed_seg_len) {
-      memcpy(signed_seg, sig_anchor, strlen(sig_anchor));
-    } else {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-    }
-  } else { // no signature anchor string was found, assume it is in the last 
path segment.
-    if (strlen(segment[numtoks - 2]) < signed_seg_len) {
-      memcpy(signed_seg, segment[numtoks - 2], strlen(segment[numtoks - 2]));
-    } else {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-      return nullptr;
-    }
-  }
-  Dbg(dbg_ctl, "signed_seg: %s", signed_seg);
-
-  // no signature anchor was found so decode and save the signing parameters 
assumed
-  // to be in the last path segment.
-  if (sig_anchor == nullptr) {
-    if (TSBase64Decode(segment[numtoks - 2], strlen(segment[numtoks - 2]), 
decoded_string, sizeof(decoded_string), &decoded_len) !=
-        TS_SUCCESS) {
-      Dbg(dbg_ctl, "Unable to decode the  path parameter string.");
-    }
-  } else {
-    if (TSBase64Decode(sig_anchor, strlen(sig_anchor), decoded_string, 
sizeof(decoded_string), &decoded_len) != TS_SUCCESS) {
-      Dbg(dbg_ctl, "Unable to decode the  path parameter string.");
-    }
-  }
-  Dbg(dbg_ctl, "decoded_string: %s", decoded_string);
-
-  {
-    int oob = 0; /* Out Of Buffer */
-
-    for (i = 0; i < numtoks; i++) {
-      // cp the base64 decoded string.
-      if (i == sig_anchor_seg && sig_anchor != nullptr) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, segment[i], 
strlen(segment[i]))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, 
reinterpret_cast<char *>(decoded_string),
-                              strlen(reinterpret_cast<char 
*>(decoded_string)))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-
-        continue;
-      } else if (i == numtoks - 2 && sig_anchor == nullptr) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, 
reinterpret_cast<char *>(decoded_string),
-                              strlen(reinterpret_cast<char 
*>(decoded_string)))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-        continue;
-      }
-      if (!fixedBufferWrite(&new_url_end, &new_url_len_left, segment[i], 
strlen(segment[i]))) {
-        oob = 1;
-        break;
-      }
-      if (i < numtoks - 1) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-      }
-    }
-    if (oob) {
-      TSError("insufficient space to copy into new_url.");
-    }
-  }
-  return TSstrndup(new_url, new_url_end - new_url);
+  delete static_cast<UrlSigConfig *>(ih);
 }
 
 TSRemapStatus
 TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
 {
-  const struct config *cfg = static_cast<const struct config *>(ih);
-
-  int          url_len         = 0;
-  int          current_url_len = 0;
-  uint64_t     expiration      = 0;
-  int          algorithm       = -1;
-  int          keyindex        = -1;
-  int          cmp_res;
-  int          rval;
-  unsigned int i               = 0;
-  int          j               = 0;
-  unsigned int sig_len         = 0;
-  bool         has_path_params = false;
-
-  /* all strings are locally allocated except url... about 25k per instance */
-  char *const   current_url       = TSUrlStringGet(rri->requestBufp, 
rri->requestUrl, &current_url_len);
-  char         *url               = current_url;
-  char          path_params[8192] = {'\0'}, new_path[8192] = {'\0'};
-  char          signed_part[8192]           = {'\0'}; // this initializes the 
whole array and is needed
-  char          urltokstr[8192]             = {'\0'};
-  char          client_ip[INET6_ADDRSTRLEN] = {'\0'}; // chose the larger ipv6 
size
-  char          ipstr[INET6_ADDRSTRLEN]     = {'\0'}; // chose the larger ipv6 
size
-  unsigned char sig[MAX_SIG_SIZE + 1];
-  char          sig_string[2 * MAX_SIG_SIZE + 1];
-
-  if (current_url_len >= MAX_REQ_LEN - 1) {
-    err_log(current_url, current_url_len, "Request Url string too long");
-    goto deny;
-  }
+  auto const *const cfg = static_cast<UrlSigConfig *>(ih);
+
+  // Get URL.
+  int         url_len         = 0;
+  char *const current_url_raw = TSUrlStringGet(rri->requestBufp, 
rri->requestUrl, &url_len);
+  std::string url_to_check(current_url_raw, url_len);
+  TSfree(current_url_raw);
 
   if (cfg->pristine_url_flag) {
     TSMBuffer    mbuf;
     TSMLoc       ul;
     TSReturnCode rc = TSHttpTxnPristineUrlGet(txnp, &mbuf, &ul);
     if (rc != TS_SUCCESS) {
-      TSError("[url_sig] Failed call to TSHttpTxnPristineUrlGet()");
+      Dbg(dbg_ctl, "[url_sig] Failed call to TSHttpTxnPristineUrlGet()");
       goto deny;
     }
-    url = TSUrlStringGet(mbuf, ul, &url_len);
-    if (url_len >= MAX_REQ_LEN - 1) {
-      err_log(url, url_len, "Pristine URL string too long.");
+    int         pristine_len = 0;
+    char *const pristine_raw = TSUrlStringGet(mbuf, ul, &pristine_len);
+    url_to_check             = std::string(pristine_raw, pristine_len);
+    TSfree(pristine_raw);

Review Comment:
   `TSHttpTxnPristineUrlGet()` returns a `TSMLoc` (`ul`) that should be 
released with `TSHandleMLocRelease(mbuf, TS_NULL_MLOC, ul)` once you're done 
with it (see e.g. `plugins/cachekey/cachekey.cc`). This code frees the string 
but never releases `ul`, which can leak per-transaction resources in pristine 
URL mode.
   ```suggestion
       TSfree(pristine_raw);
       TSHandleMLocRelease(mbuf, TS_NULL_MLOC, ul);
   ```



##########
doc/admin-guide/plugins/url_sig.en.rst:
##########
@@ -48,243 +84,258 @@ step process. First, you must generate a configuration 
file containing the list
 of valid signing keys. Secondly, you must indicate to |TS| which URLs require
 valid signatures.
 
-Generating Keys
----------------
-
-This plugin comes with two Perl scripts which assist in generating signatures.
-For |TS| to verify URL signatures, it must have the relevant keys. Using the
-provided *genkeys* script, you can generate a suitable configuration file::
-
-    ./genkeys.pl > url_sig.config
-
-The resulting file will look something like the following, with the actual keys
-differing (as they are generated randomly each time the script is run)::
-
-    key0 = YwG7iAxDo6Gaa38KJOceV4nsxiAJZ3DS
-    key1 = nLE3SZKRgaNM9hLz_HnIvrCw_GtTUJT1
-    key2 = YicZbmr6KlxfxPTJ3p9vYhARdPQ9WJYZ
-    key3 = DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
-    key4 = C1r6R6MINoQd5YSH25fU66tuRhhz3fs_
-    key5 = l4dxe6YEpYbJtyiOmX5mafhwKImC5kej
-    key6 = ekKNHXu9_oOC5eqIGJVxV0vI9FYvKVya
-    key7 = BrjibTmpTTuhMHqphkQAuCWA0Zg97WQB
-    key8 = rEtWLb1jcYoq9VG8Z8TKgX4GxBuro20J
-    key9 = mrP_6ibDBG4iYpfDB6W8yn3ZyGmdwc6M
-    key10 = tbzoTTGZXPLcvpswCQCYz1DAIZcAOGyX
-    key11 = lWsn6gUeSEW79Fk2kwKVfzhVG87EXLna
-    key12 = Riox0SmGtBWsrieLUHVWtpj18STM4MP1
-    key13 = kBsn332B7yG3HdcV7Tw51pkvHod7_84l
-    key14 = hYI4GUoIlZRf0AyuXkT3QLvBMEoFxkma
-    key15 = EIgJKwIR0LU9CUeTUdVtjMgGmxeCIbdg
-    error_url = 403
-
-This file should be placed in your |TS| ``etc`` directory, with permissions and
-ownership set such that only the |TS| processes may read it.
+Config File Format
+------------------
+
+The config file is a simple ``key = value`` text file. Lines starting with
+``#`` are comments. The file must contain at least one key and an ``error_url``
+line.
+
+=================  =================================  
=============================================
+Key                Value                              Description
+=================  =================================  
=============================================
+``key0``–``key15`` string (max 255 chars)             Shared HMAC signing keys.
+``error_url``      ``403`` or ``302 <redirect_url>``  Response for failed 
validation.
+``sig_anchor``     string                             Anchor name for 
path-parameter mode.
+``excl_regex``     PCRE regex pattern                 URLs matching skip 
signature validation.
+``url_type``       ``pristine`` or ``remap``          Which URL to validate 
against (default: remap).
+``ignore_expiry``  ``true``                           Disable expiration 
checking (debug only).
+=================  =================================  
=============================================
+
+Example configuration::
+
+   # Shared signing keys (up to 16, index 0–15).
+   key0 = YwG7iAxDo6Gaa38KJOceV4nsxiAJZ3DS
+   key1 = nLE3SZKRgaNM9hLz_HnIvrCw_GtTUJT1
+   key2 = YicZbmr6KlxfxPTJ3p9vYhARdPQ9WJYZ
+   key3 = DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
+   key4 = C1r6R6MINoQd5YSH25fU66tuRhhz3fs_
+   key5 = l4dxe6YEpYbJtyiOmX5mafhwKImC5kej
+   key6 = ekKNHXu9_oOC5eqIGJVxV0vI9FYvKVya
+   key7 = BrjibTmpTTuhMHqphkQAuCWA0Zg97WQB
+   key8 = rEtWLb1jcYoq9VG8Z8TKgX4GxBuro20J
+   key9 = mrP_6ibDBG4iYpfDB6W8yn3ZyGmdwc6M
+   key10 = tbzoTTGZXPLcvpswCQCYz1DAIZcAOGyX
+   key11 = lWsn6gUeSEW79Fk2kwKVfzhVG87EXLna
+   key12 = Riox0SmGtBWsrieLUHVWtpj18STM4MP1
+   key13 = kBsn332B7yG3HdcV7Tw51pkvHod7_84l
+   key14 = hYI4GUoIlZRf0AyuXkT3QLvBMEoFxkma
+   key15 = EIgJKwIR0LU9CUeTUdVtjMgGmxeCIbdg
+   error_url = 403
+
+Additional options example::
+
+   sig_anchor = urlsig
+   excl_regex = (/crossdomain.xml|/clientaccesspolicy.xml|/test.html)
+   url_type = pristine
+   ignore_expiry = true
 
 .. important::
 
    The configuration file contains the full set of secret keys which |TS| will
    be using to verify incoming requests, and as such should be treated with as
    much care as any other file in your infrastructure containing keys, pass
-   phrases, and other sensitive data. Unauthorized access to the contents of
-   this file will allow others to spoof requests from your signing portal, thus
-   defeating the entire purpose of using a signing portal in the first place.
+   phrases, and other sensitive data.
 
-Requiring Signatures on URLs
-----------------------------
+Generating Keys
+---------------
 
-To require a valid signature, verified by a key from the list you generated
-earlier, modify your :file:`remap.config` configuration to include this plugin
-for any rules you wish it to affect.
+The plugin ships with a Go tool to generate random keys. Run it with
+``go run``::
 
-Two parameters for each remap rule are required, and a third one is optional::
+   go run genkeys.go > /etc/trafficserver/url_sig.config
 
-    @plugin=url_sig.so @pparam=<config file> @pparam=pristineurl
+No Go modules or dependencies are needed — the file uses only the Go standard
+library.
 
-The first simply enables this plugin for the rule. The second specifies the
-location of the configuration file containing your signing keys.  The third 
one,
-if present, causes authentication to be performed on the original (pristine) 
URL
-as received from the client. (The value of the parameter is not case 
sensitive.)
+The original Perl script ``genkeys.pl`` is still available for backward
+compatibility but requires the ``Digest::SHA`` and ``MIME::Base64::URLSafe``
+modules.
 
-For example, if we wanted to restrict all paths under a ``/download`` directory
-on our website ``foo.com`` we might have a remap line like this::
+Requiring Signatures on URLs
+-----------------------------
 
-    map http://foo.com/download/ http://origin.server.tld/download/ \
-        @plugin=url_sig.so @pparam=url_sig.config
+Modify your :file:`remap.config` to include this plugin for any rules you
+wish to protect.
 
-.. note::
+Two parameters for each remap rule are required, and a third one is optional::
 
-   To be consistent, the config file option `pristine = true` should
-   be preferred over using a plugin argument.
+   @plugin=url_sig.so @pparam=<config file> @pparam=pristineurl
 
+The first enables the plugin for the rule. The second specifies the location of
+the configuration file (relative to ``etc/trafficserver/`` unless an absolute
+path). The optional third parameter causes authentication to be performed on
+the original (pristine) URL as received from the client.
 
-Signing a URL
-=============
+Example::
+
+   map http://cdn.example.com http://origin.example.com \
+       @plugin=url_sig.so @pparam=url_sig.config
 
-Signing a URL is solely the responsibility of your signing portal service. This
-requires that whatever application runs that service must also have a list of
-your signing keys (generated earlier in Configuration_ and stored in the
-``url_sig.config`` file in your |TS| configuration directory). How your signing
-portal application is informed about, or stores, these keys is up to you, but
-it is critical that the ``keyN`` index numbers are matched to the same keys.
+With pristine URL mode::
 
-Signing is performed by adding several query parameters to a URL, before
-redirecting the client. The parameters' values are all generated by your
-signing portal application, and then a hash is calculated by your portal, using
-the entire URL just constructed, and attached as the final query parameter.
+   map http://cdn.example.com http://origin.example.com \
+       @plugin=url_sig.so @pparam=url_sig.config @pparam=pristineurl
 
 .. note::
 
-   Ordering is important when adding the query parameters and generating the
-   signature. The signature itself is a hash, using your chosen algorithm, of
-   the entire URL to which you are about to redirect the client, up to and
-   including the ``S=`` substring indicating the signature parameter.
+   To be consistent, the config file option ``url_type = pristine`` should
+   be preferred over using a plugin argument.
 
-The following list details all the query parameters you must add to the URL you
-will hand back to the client for redirection.
+Signing a URL
+=============
 
-Client IP
-    The IP address of the client being redirected. This must be their IP as it
-    will appear to your |TS| cache.  Both IP v4 and v6 addresses are 
supported::
+Signing is performed by adding several query parameters to a URL before
+redirecting the client. The parameters are all generated by your signing portal
+application. A hash is then calculated over the constructed URL and attached as
+the final query parameter.
 
-        C=<ip address>
+.. note::
 
-Expiration
-    The time at which this signature will no longer be valid, expressed in
-    seconds since epoch (and thus in UTC)::
+   Ordering matters. The signature is a hash of the entire URL up to and
+   including the ``S=`` substring. The ``S=`` value itself is not included in
+   the hash input.
 
-        E=<seconds since epoch expiration>
+Signing Parameters
+------------------
 
-Algorithm
-    The hash algorithm which your signing portal application has elected to use
-    for this signature::
+=====  ==========  ===========  
=================================================================
+Param  Name        Required     Description
+=====  ==========  ===========  
=================================================================
+``C``  Client IP   optional     Locks signature to a specific client IP (IPv4 
or IPv6).
+``E``  Expiration  required     Seconds since Unix epoch when the signature 
expires.
+``A``  Algorithm   required     ``1`` = HMAC-SHA1, ``2`` = HMAC-MD5.
+``K``  Key Index   required     Index (0–15) of the key in the config file.
+``P``  Parts       required     Bitmask of URL parts to include in the 
signature (see below).
+``S``  Signature   required     Hex-encoded HMAC digest. **Must be last 
parameter.**
+=====  ==========  ===========  
=================================================================
 
-        A=<algorithm number>
+Parts Mask
+~~~~~~~~~~
 
-    The only supported values at this time are:
+The URL (minus scheme) is split by ``/``. Each character in the parts string
+controls whether that segment is included in the signed string:
 
-    ===== =========
-    Value Algorithm
-    ===== =========
-    ``1`` HMAC_SHA1
-    ``2`` HMAC_MD5
-    ===== =========
+==========  ================================================================
+Value       Effect
+==========  ================================================================
+``1``       Use the FQDN and all directory parts for signature verification.
+``01``      Ignore the FQDN, but verify using all directory parts.
+``0110``    Ignore the FQDN, use only the first two directory parts.
+``110``     Use the FQDN and first directory, ignore the remainder.
+==========  ================================================================
 
-Key Index
-    The key number, from your plugin configuration, which was used for this
-    signature. See Configuration_ for generating these keys and determining the
-    index number of each::
+If the parts string is shorter than the number of URL segments, the last
+character repeats for remaining segments.
 
-        K=<key number>
+Query String Mode (Default)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Parts
-    Configures which components of the URL to use for signature verification.
-    The value of this parameter is a string of ones and zeros, each enabling
-    or disabling the use of a URL part for signatures. The URL scheme (e.g.
-    ``http://``) is never part of the signature. The first number of this
-    parameter's value indicates whether to include the FQDN, and all remaining
-    numbers determine whether to use the directory parts of the URL. If there
-    are more directories in the URL than there are numbers in this parameter,
-    the last number is repeated as many times as necessary::
+Parameters are appended as a standard query string::
 
-        P=<parts specification>
+   http://cdn.example.com/path/file.ts?E=1700000000&A=1&K=3&P=1&S=9e2828d5...
 
-    Examples include:
+Path Parameter Mode
+~~~~~~~~~~~~~~~~~~~
 
-    ========== ================================================================
-    Value      Effect
-    ========== ================================================================
-    ``1``      Use the FQDN and all directory parts for signature verification.
-    ``01``     Ignore the FQDN, but verify using all directory parts.
-    ``0110``   Ignore the FQDN, and use only the first two directory parts,
-               skipping the remainder, for signatures.
-    ``110``    Use the FQDN and first directory for signature verification, but
-               ignore the remainder of the path.
-    ========== ================================================================
+Parameters may instead be base64-encoded and embedded in the URL path before
+the filename. This is useful when origin query parameters must be preserved
+independently of the signing parameters.
 
-Signature
-    The actual signature hash::
+Configure ``sig_anchor`` in the config file and use ``--pathparams
+--siganchor`` when signing::
 
-        S=<signature hash>
+   http://cdn.example.com/vod/t;urlsig=O0U9MTQ2.../prog_index.m3u8?appid=2&t=1
 
-    The hash should be calculated in accordance with the parts specification
-    you have declared in the ``P=`` query parameter, which if you have chosen
-    any value other than ``1`` may require additional URL parsing be performed
-    by your signing portal.
+Application query parameters follow the filename and are never part of the
+signed string.
 
-    Additionally, all query parameters up to and including the ``S=`` substring
-    for this parameter must be included, and must be in the same order as they
-    are returned to the client for redirection. For obvious reasons, the value
-    of this parameter is not included in the source string being hashed.
+Using the Go Signing Tool
+-------------------------
 
-    As a simple example, if we are about to redirect a client to the URL
-    ``https://foo.com/downloads/expensive-app.exe`` with signature verification
-    enabled, then we will compute a signature on the following string::
+The Go signing tool ``sign.go`` produces a curl command for testing. It
+requires only the Go standard library.
 
-        
foo.com/downloads/expensive-app.exe?C=1.2.3.4&E=1453846938&A=1&K=2&P=1&S=
+**Basic query string signing:**
 
-    And, assuming that *key2* from our secrets file matches our example in
-    Configuration_, then our signature becomes::
+.. code-block:: bash
 
-        8c5cfa440458233452ee9b5b570063a0e71827f2
+   go run sign.go \
+       --url http://cdn.example.com/video/segment.ts \
+       --useparts 1 \
+       --algorithm 1 \
+       --duration 3600 \
+       --keyindex 3 \
+       --key DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
 
-    Which is then appended to the URL for redirection as the value of the ``S``
-    parameter.
+**With client IP restriction:**
 
-    For an example implementation of signing which may be adapted for your own
-    portal, refer to the file ``sign.pl`` included with the source code of this
-    plugin.
+.. code-block:: bash
 
-Signature query parameters embedded in the URL path.
+   go run sign.go \
+       --url http://cdn.example.com/video/segment.ts \
+       --useparts 1 --algorithm 1 --duration 3600 \
+       --keyindex 3 --key DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7 \
+       --client 10.10.10.10
 
-    Optionally signature query parameters may be embedded in an opaque base64 
encoded container
-    embedded in the URL path.  The format is  a semicolon, siganchor string, 
base64 encoded
-    string.  ``url_sig`` automatically detects the use of embedded path 
parameters. The
-    following example shows how to generate an embedded path parameters with 
``sign.pl``::
+**Path parameter mode with sig anchor:**
 
-      ./sign.pl --url 
"http://test-remap.domain.com/vod/t/prog_index.m3u8?appid=2&t=1"; --useparts 1 \
-      --algorithm 1 --duration 86400 --key kSCE1_uBREdGI3TPnr_dXKc9f_J4ZV2f 
--pathparams --siganchor urlsig
+.. code-block:: bash
 
-      curl -s -o /dev/null -v --max-redirs 0 
'http://test-remap.domain.com/vod/t;urlsig=O0U9MTQ2MzkyOTM4NTtBPTE7Sz0zO1A9MTtTPTIxYzk2YWRiZWZk'
+   go run sign.go \
+       --url "http://cdn.example.com/vod/t/prog_index.m3u8?appid=2&t=1"; \
+       --useparts 1 --algorithm 1 --duration 86400 \
+       --keyindex 3 --key kSCE1_uBREdGI3TPnr_dXKc9f_J4ZV2f \
+       --pathparams --siganchor urlsig
 
-Other Config File Options
-=========================
+**Through a proxy:**
 
-In addition to the keys and error_url, the following options are supported
-in the configuration file::
+.. code-block:: bash
 
-    sig_anchor
-        signed anchor string token in url
-        default: no anchor
+   go run sign.go \
+       --url http://cdn.example.com/ \
+       --useparts 1 --algorithm 1 --duration 60 \
+       --keyindex 0 --key mykey \
+       --proxy http://localhost:8080
 
-    excl_regex
-        pcre regex for urls that aren't signed.
-        default: no regex
+**Verbose mode** (shows signed string and digest on stderr):
 
-    url_type
-        which url to match against
-        pristine or remap
-        default: remap
+.. code-block:: bash
 
-     ignore_expiry
-        option which assists in testing where the timestamp check is skipped
-        DO NOT run with this set in production!
-        default: false
+   go run sign.go --verbose \
+       --url http://cdn.example.com/ \
+       --useparts 1 --algorithm 1 --duration 60 \
+       --keyindex 0 --key mykey
 
-Example::
+sign.go Flags
+~~~~~~~~~~~~~
 
-    sig_anchor = urlsig
-    excl_regex = (/crossdomain.xml|/clientaccesspolicy.xml|/test.html)
-    url_type = pristine
-    ignore_expiry = true
+================  ========  =========  
=========================================
+Flag              Required  Default    Description
+================  ========  =========  
=========================================
+``--url``         yes                  Full URL to sign.
+``--useparts``    yes                  Parts bitmask string.
+``--duration``    yes                  Signature lifetime in seconds.
+``--key``         yes                  Signing key string.
+``--keyindex``    yes       ``0``      Key index (0–15).
+``--algorithm``   no        ``1``      ``1`` = HMAC-SHA1, ``2`` = HMAC-MD5.
+``--client``      no                   Lock signature to client IP.
+``--pathparams``  no        ``false``  Use path parameter mode.
+``--siganchor``   no                   Anchor name for path params.
+``--proxy``       no                   Proxy URL:port for curl output.
+``--verbose``     no        ``false``  Print signing details to stderr.
+================  ========  =========  
=========================================
 
+The original Perl script ``sign.pl`` is still available with equivalent
+functionality. It requires ``Digest::SHA``, ``Digest::HMAC_MD5``, and
+``MIME::Base64::URLSafe``.
 

Review Comment:
   The documentation says `sign.pl` is still available with equivalent 
functionality, but this PR deletes `sign.pl`. Please reconcile the docs with 
the repository contents.
   ```suggestion
   
   ```



##########
plugins/experimental/url_sig/README.md:
##########
@@ -0,0 +1,328 @@
+# url_sig — Signed URL Plugin
+
+## Overview
+
+The url_sig plugin validates a cryptographic signature embedded in request
+URLs. Requests with an invalid or missing signature are rejected with HTTP 403
+(Forbidden) or redirected with HTTP 302 (Moved Temporarily), depending on
+configuration. When the signature is valid the signing query string is stripped
+so the origin sees a clean URL and the cache can serve hits normally.
+
+The signature is an HMAC (SHA-1 or MD5) computed over selected parts of the
+URL using a shared secret key. A signing portal generates signed URLs; the
+edge cache validates them. Signed URLs do not replace DRM.
+
+## Architecture
+
+The plugin is split into cache-agnostic core logic and a thin ATS adapter:
+
+| File | Purpose |
+|------|---------|
+| `url_sig.h` | Public header — config structs, constants, function 
declarations. No ATS dependencies. |
+| `url_sig_config.cc` | Config file parser. Reads from `std::istream`. |
+| `url_sig_verify.cc` | URL validation — parameter extraction, HMAC 
computation, signature comparison. |
+| `url_sig.cc` | ATS remap plugin glue — implements `TSRemap*` hooks, 
delegates to core. |
+| `test_url_sig.cc` | Catch2 unit tests for core logic. |
+| `genkeys.go` | Go tool to generate a config file with random keys. |
+| `sign.go` | Go tool to sign URLs (produces a curl command). |
+
+## Building
+
+### Plugin (with ATS)
+
+Enable the plugin at CMake configure time:
+
+```bash
+cmake --preset dev -DENABLE_URL_SIG=ON -DBUILD_TESTING=ON
+cmake --build build-dev --target url_sig
+```
+
+### Unit Tests
+
+```bash
+cmake --build build-dev --target test_url_sig
+./build-dev/plugins/experimental/url_sig/test_url_sig
+```
+
+### Go Tools
+
+The signing tools are standalone Go files. Run them directly with `go run`:
+
+```bash
+go run genkeys.go > /etc/trafficserver/url_sig.config
+go run sign.go --url http://example.com/path --useparts 1 --algorithm 1 \
+    --duration 3600 --keyindex 3 --key YOUR_SECRET_KEY
+```
+
+No `go.mod` needed — each file has `//go:build ignore`.
+
+## Configuration
+
+### Config File Format
+
+The config file is a simple `key = value` text file. Lines starting with `#`
+are comments. The file must contain at least one key and an `error_url` line.
+
+```
+# Shared signing keys (up to 16, index 0-15).
+key0 = YwG7iAxDo6Gaa38KJOceV4nsxiAJZ3DS
+key1 = nLE3SZKRgaNM9hLz_HnIvrCw_GtTUJT1
+key2 = YicZbmr6KlxfxPTJ3p9vYhARdPQ9WJYZ
+key3 = DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
+...
+
+# Error behavior: "403" to deny, or "302 <url>" to redirect.
+error_url = 403
+```
+
+#### Supported Keys
+
+| Key | Value | Description |
+|-----|-------|-------------|
+| `key0` .. `key15` | string (max 255 chars) | Shared HMAC signing keys. |
+| `error_url` | `403` or `302 <redirect_url>` | Response for failed 
validation. |
+| `sig_anchor` | string | Anchor name for path-parameter mode (e.g. `urlsig`). 
|
+| `excl_regex` | regex pattern | URLs matching this pattern skip signature 
validation. |
+| `ignore_expiry` | `true` | Disable expiration checking (for debugging only). 
|
+| `url_type` | `pristine` | Use the pristine (pre-remap) URL for validation. |
+
+### Generate a Config File
+
+```bash
+go run genkeys.go > /etc/trafficserver/url_sig.config
+```
+
+Or create one manually. Keys should be random strings shared only between the
+signing portal and the edge caches.
+
+## Plugin Setup
+
+### remap.config
+
+Add the plugin to the remap rule for the domain you want to protect:
+
+```
+map http://cdn.example.com http://origin.example.com \
+    @plugin=url_sig.so @pparam=url_sig.config
+```
+
+The config file path is relative to `etc/trafficserver/` unless it starts
+with `/`.
+
+#### Optional: Pristine URL Mode
+
+To validate against the pre-remap URL, add `pristineurl` as a second
+parameter or set `url_type = pristine` in the config file:
+
+```
+map http://cdn.example.com http://origin.example.com \
+    @plugin=url_sig.so @pparam=url_sig.config @pparam=pristineurl
+```
+
+### Reload
+
+After editing `remap.config` or the signing config:
+
+```bash
+traffic_ctl config reload
+```
+
+## Signing Parameters
+
+The signing parameters are appended to the URL as a query string (default) or
+embedded in the path (path-parameter mode). Parameters:
+
+| Param | Name | Description |
+|-------|------|-------------|
+| `C` | Client IP | Optional. Locks signature to a specific client IP (IPv4 or 
IPv6). |
+| `E` | Expiration | Required. Seconds since Unix epoch when the signature 
expires. |
+| `A` | Algorithm | Required. `1` = HMAC-SHA1, `2` = HMAC-MD5. |
+| `K` | Key Index | Required. Index (0-15) of the key in the config file. |
+| `P` | Parts | Required. Bitmask of URL parts to include in signing (see 
below). |
+| `S` | Signature | Required. Hex-encoded HMAC. Must be last parameter. |
+
+### Parts Mask
+
+The URL (minus scheme) is split by `/`. Each character in the parts string
+controls whether that segment is included in the signed string:
+
+- `1` — include this part and all path parts
+- `0110` — skip fqdn, include parts 1 and 2, skip the rest
+- `01` — skip fqdn, include everything else
+
+If the parts string is shorter than the number of URL segments, the last
+character repeats for remaining segments.
+
+### Query String Mode (Default)
+
+Parameters are appended as a standard query string:
+
+```
+http://cdn.example.com/path/file.ts?E=1700000000&A=1&K=3&P=1&S=9e2828d5...
+```
+
+### Path Parameter Mode
+
+Parameters are base64-encoded and embedded in the path before the filename.
+Use the `sig_anchor` config option and `--pathparams --siganchor` flags:
+
+```
+http://cdn.example.com/path;urlsig=O0U9MTQ2.../file.ts?appid=2
+```
+
+Application query parameters follow the filename and are never part of the
+signed string.
+
+## Signing a URL
+
+### Using the Go Tool
+
+**Basic query string signing:**
+
+```bash
+go run sign.go \
+    --url http://cdn.example.com/video/segment.ts \
+    --useparts 1 \
+    --algorithm 1 \
+    --duration 3600 \
+    --keyindex 3 \
+    --key DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7
+```
+
+Output:
+
+```
+curl -s -o /dev/null -v --max-redirs 0 
'http://cdn.example.com/video/segment.ts?E=1700003600&A=1&K=3&P=1&S=a1b2c3d4...'
+```
+
+**With client IP restriction:**
+
+```bash
+go run sign.go \
+    --url http://cdn.example.com/video/segment.ts \
+    --useparts 1 \
+    --algorithm 1 \
+    --duration 3600 \
+    --keyindex 3 \
+    --key DTV4Tcn046eM9BzJMeYrYpm3kbqOtBs7 \
+    --client 10.10.10.10
+```
+
+**Path parameter mode with sig anchor:**
+
+```bash
+go run sign.go \
+    --url "http://cdn.example.com/vod/t/prog_index.m3u8?appid=2&t=1"; \
+    --useparts 1 \
+    --algorithm 1 \
+    --duration 86400 \
+    --keyindex 3 \
+    --key kSCE1_uBREdGI3TPnr_dXKc9f_J4ZV2f \
+    --pathparams \
+    --siganchor urlsig
+```
+
+**Through a proxy:**
+
+```bash
+go run sign.go \
+    --url http://cdn.example.com/ \
+    --useparts 1 \
+    --algorithm 1 \
+    --duration 60 \
+    --keyindex 0 \
+    --key mykey \
+    --proxy http://localhost:8080
+```
+
+**Verbose mode (shows signed string and digest on stderr):**
+
+```bash
+go run sign.go --verbose --url http://cdn.example.com/ \
+    --useparts 1 --algorithm 1 --duration 60 --keyindex 0 --key mykey
+```
+
+### sign.go Flags
+
+| Flag | Required | Default | Description |
+|------|----------|---------|-------------|
+| `--url` | yes | | Full URL to sign |
+| `--useparts` | yes | | Parts bitmask string |
+| `--duration` | yes | | Signature lifetime in seconds |
+| `--key` | yes | | Signing key string |
+| `--keyindex` | yes | `0` | Key index (0-15) |
+| `--algorithm` | no | `1` | 1=HMAC-SHA1, 2=HMAC-MD5 |
+| `--client` | no | | Lock to client IP |
+| `--pathparams` | no | `false` | Use path parameter mode |
+| `--siganchor` | no | | Anchor name for path params |
+| `--proxy` | no | | Proxy URL:port for curl output |
+| `--verbose` | no | `false` | Print signing details to stderr |
+
+## Debugging
+
+Enable debug logging in `records.yaml`:
+
+```yaml
+records:
+  diags:
+    debug:
+      enabled: 1
+      tags: url_sig
+```
+
+Then reload:
+
+```bash
+traffic_ctl config reload
+```
+
+- Debug output goes to `traffic.out` / `diags.log`.
+- Failed signature checks are logged to `error.log`.
+
+## Walkthrough Example
+
+1. **Generate keys:**
+
+   ```bash
+   go run genkeys.go > /etc/trafficserver/url_sig.config
+   ```
+
+2. **Configure remap** (`remap.config`):
+
+   ```
+   map http://cdn.example.com http://origin.example.com \
+       @plugin=url_sig.so @pparam=url_sig.config
+   ```
+
+3. **Reload ATS:**
+
+   ```bash
+   traffic_ctl config reload
+   ```
+
+4. **Test unsigned request (should get 403):**
+
+   ```bash
+   curl -vs http://localhost:8080/ -H 'Host: cdn.example.com'
+   ```
+
+5. **Sign a URL and test:**
+
+   ```bash
+   # Pick key3 from url_sig.config
+   go run sign.go \
+       --url http://cdn.example.com/ \
+       --useparts 1 --algorithm 1 --duration 60 \
+       --keyindex 3 --key <key3_value_from_config>
+   ```
+
+   Copy the output curl command, add `-H 'Host: cdn.example.com'` if hitting
+   localhost, and run it. Should get a 200.
+
+## Legacy Perl Scripts
+
+The original `genkeys.pl` and `sign.pl` Perl scripts are still present for
+backward compatibility. They require `Digest::SHA`, `Digest::HMAC_MD5`, and
+`MIME::Base64::URLSafe`. The Go tools (`genkeys.go`, `sign.go`) are
+functionally equivalent and have no dependencies beyond the Go standard
+library.

Review Comment:
   This README claims the legacy Perl scripts (`genkeys.pl` / `sign.pl`) are 
still present for backward compatibility, but this PR deletes them. Please 
update this section to match what's actually shipped (or keep the scripts).
   ```suggestion
   The original `genkeys.pl` and `sign.pl` Perl scripts are no longer shipped.
   Use the Go tools (`genkeys.go`, `sign.go`) instead; they are functionally
   equivalent and have no dependencies beyond the Go standard library.
   ```



##########
plugins/experimental/url_sig/unit_tests/test_url_sig.cc:
##########
@@ -0,0 +1,523 @@
+/** @file
+  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 "url_sig.h"
+
+#include <ctime>
+#include <sstream>
+#include <string>
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+// Helper to generate HMAC-SHA1 hex signature for test URLs.
+static std::string
+hmac_sha1_hex(std::string const &key, std::string const &data)
+{
+  unsigned char sig[20];

Review Comment:
   This test helper uses `snprintf()` but doesn't include `<cstdio>`, which can 
break builds depending on transitive includes. Add the missing include.



##########
plugins/experimental/url_sig/url_sig_verify.cc:
##########
@@ -0,0 +1,535 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <ctime>
+#include <vector>
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+namespace
+{
+
+/// Find parameter value in a delimited parameter string.
+/// @param params parameter string (query or semicolon-delimited).
+/// @param key parameter key (e.g. "E").
+/// @param delim delimiter between parameters ('&' or ';').
+/// @return value portion after "key=", empty if not found.
+std::string_view
+find_param(std::string_view const params, std::string_view const key, char 
const delim)
+{
+  std::string const search = std::string(key) + "=";
+  auto              pos    = params.find(search);
+
+  // Ensure it's at start or preceded by delimiter.
+  while (pos != std::string_view::npos) {
+    if (pos == 0 || params[pos - 1] == delim) {
+      auto const val_start = pos + search.size();
+      auto const val_end   = params.find(delim, val_start);
+      if (val_end == std::string_view::npos) {
+        return params.substr(val_start);
+      }
+      return params.substr(val_start, val_end - val_start);
+    }
+    pos = params.find(search, pos + 1);
+  }
+  return {};
+}
+
+/// Compute HMAC signature and return hex string.
+std::string
+compute_hmac(int const algorithm, std::string_view const key, std::string_view 
const data)
+{
+  EVP_MD const *md           = nullptr;
+  unsigned int  expected_len = 0;
+
+  switch (algorithm) {
+  case USIG_HMAC_SHA1:
+    md           = EVP_sha1();
+    expected_len = SHA1_SIG_SIZE;
+    break;
+  case USIG_HMAC_MD5:
+    md           = EVP_md5();
+    expected_len = MD5_SIG_SIZE;
+    break;
+  default:
+    return {};
+  }
+
+  unsigned char sig[MAX_SIG_SIZE + 1];
+  unsigned int  sig_len = 0;
+
+  HMAC(md, key.data(), static_cast<int>(key.size()), reinterpret_cast<unsigned 
char const *>(data.data()), data.size(), sig,
+       &sig_len);
+
+  if (sig_len != expected_len) {
+    return {};
+  }
+
+  std::string hex;
+  hex.reserve(sig_len * 2);
+  for (unsigned int i = 0; i < sig_len; i++) {
+    char buf[3];
+    snprintf(buf, sizeof(buf), "%02x", sig[i]);
+    hex.append(buf, 2);
+  }
+  return hex;
+}
+
+/// Split a string_view by delimiter, returning vector of parts.
+std::vector<std::string_view>
+split(std::string_view const sv_in, char const delim)
+{
+  std::vector<std::string_view> result;
+  std::string_view              sv = sv_in;
+  while (!sv.empty()) {
+    auto pos = sv.find(delim);
+    if (pos == std::string_view::npos) {
+      result.push_back(sv);
+      break;
+    }
+    result.push_back(sv.substr(0, pos));
+    sv.remove_prefix(pos + 1);
+  }
+  return result;
+}
+
+/// Base64 decode (minimal implementation for path params).
+/// Uses OpenSSL EVP_DecodeBlock.
+std::string
+base64_decode(std::string_view const input)
+{
+  if (input.empty()) {
+    return {};
+  }
+
+  // EVP_DecodeBlock needs null-terminated input; output can be up to 3/4 * 
input_len.
+  std::string padded(input);
+  // Pad to multiple of 4.
+  while (padded.size() % 4 != 0) {
+    padded.push_back('=');
+  }
+
+  std::vector<unsigned char> out(padded.size());
+  int const                  decoded_len =
+    EVP_DecodeBlock(out.data(), reinterpret_cast<unsigned char const 
*>(padded.data()), static_cast<int>(padded.size()));
+
+  if (decoded_len < 0) {
+    return {};
+  }
+
+  // Remove padding bytes from length.
+  // Count '=' at end of padded input.
+  int pad_count = 0;
+  for (auto it = padded.rbegin(); it != padded.rend() && *it == '='; ++it) {
+    pad_count++;
+  }
+  // Original input padding.
+  int orig_pad = 0;
+  for (auto it = input.rbegin(); it != input.rend() && *it == '='; ++it) {
+    orig_pad++;
+  }
+  int const len = decoded_len - (pad_count - orig_pad);
+
+  if (len < 0) {
+    return {};
+  }
+
+  return std::string(reinterpret_cast<char *>(out.data()), len);
+}
+
+} // anonymous namespace
+
+std::string
+get_app_query_string(std::string_view const query)
+{
+  if (query.empty()) {
+    return {};
+  }
+
+  if (static_cast<int>(query.size()) < MAX_QUERY_LEN) {

Review Comment:
   This length check uses `< MAX_QUERY_LEN`, which rejects query strings of 
exactly `MAX_QUERY_LEN`. The legacy code rejected only `> MAX_QUERY_LEN`, so 
this is a boundary behavior change.
   ```suggestion
     if (static_cast<int>(query.size()) <= MAX_QUERY_LEN) {
   ```



##########
plugins/experimental/url_sig/url_sig_verify.cc:
##########
@@ -0,0 +1,535 @@
+/** @file
+  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 "url_sig.h"
+
+#include <charconv>
+#include <cstring>
+#include <ctime>
+#include <vector>
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+namespace
+{
+
+/// Find parameter value in a delimited parameter string.
+/// @param params parameter string (query or semicolon-delimited).
+/// @param key parameter key (e.g. "E").
+/// @param delim delimiter between parameters ('&' or ';').
+/// @return value portion after "key=", empty if not found.
+std::string_view
+find_param(std::string_view const params, std::string_view const key, char 
const delim)
+{
+  std::string const search = std::string(key) + "=";
+  auto              pos    = params.find(search);
+
+  // Ensure it's at start or preceded by delimiter.
+  while (pos != std::string_view::npos) {
+    if (pos == 0 || params[pos - 1] == delim) {
+      auto const val_start = pos + search.size();
+      auto const val_end   = params.find(delim, val_start);
+      if (val_end == std::string_view::npos) {
+        return params.substr(val_start);
+      }
+      return params.substr(val_start, val_end - val_start);
+    }
+    pos = params.find(search, pos + 1);
+  }
+  return {};
+}
+
+/// Compute HMAC signature and return hex string.
+std::string
+compute_hmac(int const algorithm, std::string_view const key, std::string_view 
const data)
+{
+  EVP_MD const *md           = nullptr;
+  unsigned int  expected_len = 0;
+
+  switch (algorithm) {
+  case USIG_HMAC_SHA1:
+    md           = EVP_sha1();
+    expected_len = SHA1_SIG_SIZE;
+    break;
+  case USIG_HMAC_MD5:
+    md           = EVP_md5();
+    expected_len = MD5_SIG_SIZE;
+    break;
+  default:
+    return {};
+  }
+
+  unsigned char sig[MAX_SIG_SIZE + 1];
+  unsigned int  sig_len = 0;
+
+  HMAC(md, key.data(), static_cast<int>(key.size()), reinterpret_cast<unsigned 
char const *>(data.data()), data.size(), sig,
+       &sig_len);
+
+  if (sig_len != expected_len) {
+    return {};
+  }
+
+  std::string hex;
+  hex.reserve(sig_len * 2);
+  for (unsigned int i = 0; i < sig_len; i++) {
+    char buf[3];
+    snprintf(buf, sizeof(buf), "%02x", sig[i]);
+    hex.append(buf, 2);
+  }
+  return hex;
+}
+
+/// Split a string_view by delimiter, returning vector of parts.
+std::vector<std::string_view>
+split(std::string_view const sv_in, char const delim)
+{
+  std::vector<std::string_view> result;
+  std::string_view              sv = sv_in;
+  while (!sv.empty()) {
+    auto pos = sv.find(delim);
+    if (pos == std::string_view::npos) {
+      result.push_back(sv);
+      break;
+    }
+    result.push_back(sv.substr(0, pos));
+    sv.remove_prefix(pos + 1);
+  }
+  return result;
+}
+
+/// Base64 decode (minimal implementation for path params).
+/// Uses OpenSSL EVP_DecodeBlock.
+std::string
+base64_decode(std::string_view const input)
+{
+  if (input.empty()) {
+    return {};
+  }
+
+  // EVP_DecodeBlock needs null-terminated input; output can be up to 3/4 * 
input_len.
+  std::string padded(input);
+  // Pad to multiple of 4.
+  while (padded.size() % 4 != 0) {
+    padded.push_back('=');
+  }
+
+  std::vector<unsigned char> out(padded.size());
+  int const                  decoded_len =
+    EVP_DecodeBlock(out.data(), reinterpret_cast<unsigned char const 
*>(padded.data()), static_cast<int>(padded.size()));
+
+  if (decoded_len < 0) {
+    return {};
+  }
+
+  // Remove padding bytes from length.
+  // Count '=' at end of padded input.
+  int pad_count = 0;
+  for (auto it = padded.rbegin(); it != padded.rend() && *it == '='; ++it) {
+    pad_count++;
+  }
+  // Original input padding.
+  int orig_pad = 0;
+  for (auto it = input.rbegin(); it != input.rend() && *it == '='; ++it) {
+    orig_pad++;
+  }
+  int const len = decoded_len - (pad_count - orig_pad);
+
+  if (len < 0) {
+    return {};
+  }
+
+  return std::string(reinterpret_cast<char *>(out.data()), len);
+}
+
+} // anonymous namespace
+
+std::string
+get_app_query_string(std::string_view const query)
+{
+  if (query.empty()) {
+    return {};
+  }
+
+  if (static_cast<int>(query.size()) < MAX_QUERY_LEN) {
+    // Find first signing parameter.
+    std::string_view remaining = query;
+    std::string      result;
+
+    while (!remaining.empty()) {
+      auto             amp = remaining.find('&');
+      std::string_view param;
+      if (amp == std::string_view::npos) {
+        param     = remaining;
+        remaining = {};
+      } else {
+        param = remaining.substr(0, amp);
+        remaining.remove_prefix(amp + 1);
+      }
+
+      // Check if this is a signing parameter (starts with A, C, E, K, P, or S 
followed by =).
+      if (!param.empty()) {
+        char const first = param[0];
+        if ((first == 'A' || first == 'C' || first == 'E' || first == 'K' || 
first == 'P' || first == 'S') && param.size() >= 2 &&
+            param[1] == '=') {
+          // This is a signing param — stop here, don't include it or anything 
after.
+          break;
+        }
+        if (!result.empty()) {
+          result.push_back('&');
+        }
+        result.append(param);
+      }
+    }
+    return result;
+  }
+  return {};
+}
+
+std::string
+url_parse_path_params(std::string_view const url, std::string_view const 
anchor, std::string &new_path, std::string &signed_seg)
+{
+  new_path.clear();
+  signed_seg.clear();
+
+  // Find scheme.
+  auto const colon = url.find(':');
+  if (colon == std::string_view::npos || url.size() < colon + 3 || url[colon + 
1] != '/' || url[colon + 2] != '/') {
+    return {};
+  }
+
+  std::string_view const scheme = url.substr(0, colon + 3);
+  std::string_view const rest   = url.substr(colon + 3);
+
+  // Split path into segments.
+  auto segments = split(rest, '/'); // not const: anchor search may truncate a 
segment
+  if (segments.size() < 3) {
+    return {};
+  }
+  if (static_cast<int>(segments.size()) >= MAX_SEGMENTS) {
+    return {};
+  }
+
+  int              sig_anchor_seg = -1;
+  std::string_view sig_anchor_value;
+
+  // Look for anchor in segments.
+  if (!anchor.empty()) {
+    for (size_t i = 0; i < segments.size(); i++) {
+      auto const anchor_pos = segments[i].find(anchor);
+      if (anchor_pos != std::string_view::npos) {
+        // Find the '=' after anchor.
+        auto const eq_pos = segments[i].find('=', anchor_pos);
+        if (eq_pos != std::string_view::npos) {
+          sig_anchor_value = segments[i].substr(eq_pos + 1);
+          // Truncate segment to before the ';' preceding anchor.
+          if (0 < anchor_pos && segments[i][anchor_pos - 1] == ';') {
+            segments[i] = segments[i].substr(0, anchor_pos - 1);
+          }
+          sig_anchor_seg = static_cast<int>(i);
+        }
+        break;
+      }
+    }
+  }
+
+  // Build new_path (skip fqdn segment[0], skip signing segment if no anchor).
+  for (size_t i = 1; i < segments.size(); i++) {
+    if (sig_anchor_value.empty() && i == segments.size() - 2) {
+      // No anchor: signing params in second-to-last segment, skip it.
+      continue;
+    }
+    if (!new_path.empty()) {
+      new_path.push_back('/');
+    }
+    new_path.append(segments[i]);
+  }
+
+  // Save signed segment.
+  if (!sig_anchor_value.empty()) {
+    signed_seg = std::string(sig_anchor_value);
+  } else {
+    signed_seg = std::string(segments[segments.size() - 2]);
+  }
+
+  // Decode the signed segment.
+  std::string const decoded = base64_decode(signed_seg);
+
+  // Build new URL with decoded params inserted.
+  std::string new_url;
+  new_url.append(scheme);
+
+  for (size_t i = 0; i < segments.size(); i++) {
+    if (static_cast<int>(i) == sig_anchor_seg && !sig_anchor_value.empty()) {
+      new_url.append(segments[i]);
+      new_url.append(decoded);
+      new_url.push_back('/');
+      continue;
+    } else if (sig_anchor_value.empty() && i == segments.size() - 2) {
+      new_url.append(decoded);
+      new_url.push_back('/');
+      continue;
+    }
+
+    new_url.append(segments[i]);
+    if (i < segments.size() - 1) {
+      new_url.push_back('/');
+    }
+  }
+
+  return new_url;
+}
+
+UrlSigResult
+validate_url(UrlSigConfig const &cfg, std::string_view const url, 
std::string_view const client_ip, time_t const now)
+{
+  UrlSigResult result;
+  result.status = UrlSigStatus::DENY;
+
+  if (static_cast<int>(url.size()) >= MAX_REQ_LEN - 1) {
+    result.reason = "Request URL string too long";
+    return result;
+  }
+
+  // Check exclusion regex.
+  if (cfg.excl_regex_match) {
+    // Only check up to first '?' or '#'.
+    auto const             end_pos  = url.find_first_of("?#");
+    std::string_view const base_url = (end_pos != std::string_view::npos) ? 
url.substr(0, end_pos) : url;
+    if (cfg.excl_regex_match(base_url)) {
+      result.status = UrlSigStatus::ALLOW;
+      return result;
+    }
+  }
+
+  // Determine if query string or path params mode.
+  bool             has_path_params = false;
+  std::string      parsed_url_storage;
+  std::string_view working_url = url;
+  std::string      new_path;
+  std::string      signed_seg;
+
+  auto const       qmark = url.find('?');
+  std::string_view query;
+
+  if (qmark == std::string_view::npos || url.find("E=", qmark) == 
std::string_view::npos) {
+    // No query string with E= found — try path params.
+    parsed_url_storage = url_parse_path_params(url, cfg.sig_anchor, new_path, 
signed_seg);
+    if (parsed_url_storage.empty()) {
+      result.reason = "Unable to parse/decode URL path parameters";
+      return result;
+    }
+
+    has_path_params = true;
+    working_url     = parsed_url_storage;
+
+    // Find semicolon-delimited params.
+    auto const semi = parsed_url_storage.find(';');
+    if (semi == std::string_view::npos) {
+      result.reason = "Has no signing query string or signing path parameters";
+      return result;
+    }
+    // Include leading ';' so signed_part matches reference behavior.
+    query = std::string_view(parsed_url_storage).substr(semi);
+  } else {
+    query = url.substr(qmark + 1);
+  }
+
+  char const delim = has_path_params ? ';' : '&';
+
+  // For path params, skip the leading ';' when extracting parameter values.
+  std::string_view const param_query = has_path_params ? query.substr(1) : 
query;
+
+  // Extract parameters.
+  auto const exp_val = find_param(param_query, EXP_QSTRING, delim);
+  auto const alg_val = find_param(param_query, ALG_QSTRING, delim);
+  auto const kin_val = find_param(param_query, KIN_QSTRING, delim);
+  auto const par_val = find_param(param_query, PAR_QSTRING, delim);
+  auto const sig_val = find_param(param_query, SIG_QSTRING, delim);
+  auto const cip_val = find_param(param_query, CIP_QSTRING, delim);
+
+  // Client IP check (optional parameter).
+  if (!cip_val.empty()) {
+    if (client_ip != cip_val) {
+      result.reason = "Client IP doesn't match signature";
+      return result;
+    }
+  }
+
+  // Expiration check.
+  if (!cfg.ignore_expiry) {
+    if (exp_val.empty()) {
+      result.reason = "Expiration query string not found";
+      return result;
+    }
+    uint64_t expiration = 0;
+    auto [ptr, ec]      = std::from_chars(exp_val.data(), exp_val.data() + 
exp_val.size(), expiration);
+    if (ec != std::errc{}) {
+      result.reason = "Invalid expiration";
+      return result;
+    }
+    time_t const current_time = (now != 0) ? now : time(nullptr);
+    if (static_cast<time_t>(expiration) < current_time) {
+      result.reason = "Invalid expiration, or expired";
+      return result;
+    }
+  }
+
+  // Algorithm.
+  if (alg_val.empty()) {
+    result.reason = "Algorithm query string not found";
+    return result;
+  }
+  int algorithm = 0;
+  {
+    auto [ptr, ec] = std::from_chars(alg_val.data(), alg_val.data() + 
alg_val.size(), algorithm);
+    if (ec != std::errc{}) {
+      result.reason = "Invalid algorithm";
+      return result;
+    }
+  }
+
+  // Key index.
+  if (kin_val.empty()) {
+    result.reason = "KeyIndex query string not found";
+    return result;
+  }
+  int keyindex = -1;
+  {
+    auto [ptr, ec] = std::from_chars(kin_val.data(), kin_val.data() + 
kin_val.size(), keyindex);
+    if (ec != std::errc{}) {
+      result.reason = "Invalid key index";
+      return result;
+    }
+  }
+  if (keyindex < 0 || static_cast<size_t>(keyindex) >= cfg.keys.size() || 
cfg.keys[keyindex].empty()) {
+    result.reason = "Invalid key index";
+    return result;
+  }
+
+  // Parts.
+  if (par_val.empty()) {
+    result.reason = "PartsSigned query string not found";
+    return result;
+  }
+
+  // Signature.
+  if (sig_val.empty()) {
+    result.reason = "Signature query string not found";
+    return result;
+  }
+  if ((algorithm == USIG_HMAC_SHA1 && sig_val.size() < SHA1_SIG_SIZE) ||
+      (algorithm == USIG_HMAC_MD5 && sig_val.size() < MD5_SIG_SIZE)) {
+    result.reason = "Signature query string too short";
+    return result;
+  }
+
+  // Build the signed string from parts.
+  // Skip scheme (find "://").
+  auto const scheme_end = working_url.find("://");
+  if (scheme_end == std::string_view::npos) {
+    result.reason = "Invalid URL format";
+    return result;
+  }
+  std::string_view const after_scheme = working_url.substr(scheme_end + 3);
+
+  // Find where query/params start.
+  std::string_view path_portion;
+  if (has_path_params) {
+    auto const semi_pos = after_scheme.find(';');
+    path_portion        = (semi_pos != std::string_view::npos) ? 
after_scheme.substr(0, semi_pos) : after_scheme;
+  } else {
+    auto const q_pos = after_scheme.find('?');
+    path_portion     = (q_pos != std::string_view::npos) ? 
after_scheme.substr(0, q_pos) : after_scheme;
+  }
+
+  // Split path into parts by '/', filtering empty segments (matches strtok_r 
behavior).
+  auto const                    raw_parts = split(path_portion, '/');
+  std::vector<std::string_view> url_parts;
+  for (auto const &p : raw_parts) {
+    if (!p.empty()) {
+      url_parts.push_back(p);
+    }
+  }
+
+  // Build signed_part using parts mask.
+  std::string signed_part;
+  size_t      j = 0;
+  for (size_t i = 0; i < url_parts.size(); i++) {
+    char const part_flag = (j < par_val.size()) ? par_val[j] : par_val.back();
+    if (part_flag == '1') {
+      signed_part.append(url_parts[i]);
+      signed_part.push_back('/');
+    }
+    if (j + 1 < par_val.size()) {

Review Comment:
   The parts-mask index (`j`) is advanced unconditionally, which changes legacy 
behavior for malformed `P=` values (the old code only advanced when the next 
character was `0` or `1`). To avoid behavior changes, either validate `P` 
contains only `0/1` and fail fast, or preserve the legacy advancement rule.
   ```suggestion
       if (j + 1 < par_val.size() && (par_val[j + 1] == '0' || par_val[j + 1] 
== '1')) {
   ```



##########
plugins/experimental/url_sig/url_sig.cc:
##########
@@ -77,766 +74,174 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int 
errbuf_size)
   return TS_SUCCESS;
 }
 
-// To force a config file reload touch remap.config and do a "traffic_ctl 
config reload"
 TSReturnCode
 TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int 
errbuf_size)
 {
-  char config_filepath_buf[PATH_MAX], *config_file;
-
-  if ((argc < 3) || (argc > 4)) {
+  if (argc < 3 || 4 < argc) {
     snprintf(errbuf, errbuf_size,
-             "[TSRemapNewInstance] - Argument count wrong (%d)... config file 
path is required first pparam, \"pristineurl\" is"
+             "[TSRemapNewInstance] - Argument count wrong (%d)... config file 
path is required first pparam, \"pristineurl\" is "
              "optional second pparam.",
              argc);
     return TS_ERROR;
   }
+
   Dbg(dbg_ctl, "Initializing remap function of %s -> %s with config from %s", 
argv[0], argv[1], argv[2]);
 
-  if (argv[2][0] == '/') {
-    config_file = argv[2];
-  } else {
+  // Resolve config file path.
+  char        config_filepath_buf[PATH_MAX];
+  char const *config_file = argv[2];
+  if (argv[2][0] != '/') {
     snprintf(config_filepath_buf, sizeof(config_filepath_buf), "%s/%s", 
TSConfigDirGet(), argv[2]);
     config_file = config_filepath_buf;
   }
+
   Dbg(dbg_ctl, "config file name: %s", config_file);
-  FILE *file = fopen(config_file, "r");
-  if (file == nullptr) {
+
+  std::ifstream file(config_file);
+  if (!file.is_open()) {
     snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Error opening file 
%s", config_file);
     return TS_ERROR;
   }
 
-  char line[300];
-  int  line_no = 0;
-  int  keynum;
-  bool eat_comment = false;
-
-  auto cfg = std::make_unique<config>();
-
-  while (fgets(line, sizeof(line), file) != nullptr) {
-    Dbg(dbg_ctl, "LINE: %s (%d)", line, (int)strlen(line));
-    line_no++;
+  std::string error;
+  auto        cfg = load_config(file, error);
+  if (!cfg) {
+    snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - %s", error.c_str());
+    return TS_ERROR;
+  }
 
-    if (eat_comment) {
-      // Check if final char is EOL, if so we are done eating
-      if (line[strlen(line) - 1] == '\n') {
-        eat_comment = false;
-      }
+  // Handle excl_regex: re-read file to find the pattern.
+  file.clear();
+  file.seekg(0);
+  std::string line;
+  while (std::getline(file, line)) {
+    if (line.empty() || line[0] == '#') {
       continue;
     }
-    if (line[0] == '#' || strlen(line) <= 1) {
-      // Check if we have a comment longer than the full buffer if no EOL
-      if (line[strlen(line) - 1] != '\n') {
-        eat_comment = true;
-      }
+    auto eq = line.find('=');
+    if (eq == std::string::npos) {
       continue;
     }
-    char *pos = strchr(line, '=');
-    if (pos == nullptr) {
-      TSError("[url_sig] Error parsing line %d of file %s (%s)", line_no, 
config_file, line);
-      continue;
-    }
-    *pos        = '\0';
-    char *value = pos + 1;
-    while (isspace(*value)) { // remove whitespace
-      value++;
-    }
-    pos = strchr(value, '\n'); // remove the new line, terminate the string
-    if (pos != nullptr) {
-      *pos = '\0';
-    }
-    if (pos == nullptr || strlen(value) >= MAX_KEY_LEN) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Maximum key length 
(%d) exceeded on line %d", MAX_KEY_LEN - 1, line_no);
-      fclose(file);
-      return TS_ERROR;
+    std::string_view key(line.data(), eq);
+    // Trim trailing whitespace from key.
+    while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
+      key.remove_suffix(1);
     }
-    if (strncmp(line, "key", 3) == 0) {
-      if (strncmp(line + 3, "0", 1) == 0) {
-        keynum = 0;
-      } else {
-        Dbg(dbg_ctl, ">>> %s <<<", line + 3);
-        keynum = atoi(line + 3);
-        if (keynum == 0) {
-          keynum = -1; // Not a Number
-        }
-      }
-      Dbg(dbg_ctl, "key number %d == %s", keynum, value);
-      if (keynum >= MAX_KEY_NUM || keynum < 0) {
-        snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Key number (%d) 
>= MAX_KEY_NUM (%d) or NaN", keynum, MAX_KEY_NUM);
-        fclose(file);
-        return TS_ERROR;
+    if (key == "excl_regex") {
+      std::string_view value(line.data() + eq + 1, line.size() - eq - 1);
+      // Trim whitespace.
+      while (!value.empty() && (value.front() == ' ' || value.front() == 
'\t')) {
+        value.remove_prefix(1);
       }
-      snprintf(&cfg->keys[keynum][0], MAX_KEY_LEN, "%s", value);
-    } else if (strncmp(line, "error_url", 9) == 0) {
-      if (atoi(value)) {
-        cfg->err_status = static_cast<TSHttpStatus>(atoi(value));
+      while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || 
value.back() == '\n')) {
+        value.remove_suffix(1);
       }
-      value += 3;
-      while (isspace(*value)) {
-        value++;
-      }
-      if (cfg->err_status == TS_HTTP_STATUS_MOVED_TEMPORARILY) {
-        cfg->err_url = value;
-      } else {
-        cfg->err_url.clear();
-      }
-    } else if (strncmp(line, "sig_anchor", 10) == 0) {
-      cfg->sig_anchor = value;
-    } else if (strncmp(line, "excl_regex", 10) == 0) {
-      // Compile regex.
-      std::string error;
-      int         erroffset = 0;
 
-      if (cfg->excl_regex) {
-        Dbg(dbg_ctl, "Skipping duplicate excl_regex");
-        continue;
-      }
-
-      cfg->excl_regex = std::make_unique<Regex>();
-      if (!cfg->excl_regex->compile(value, error, erroffset, 0)) {
-        Dbg(dbg_ctl, "Regex compilation failed with error (%s) at character 
%d", error.c_str(), erroffset);
-        cfg->excl_regex.reset();
-      }
-    } else if (strncmp(line, "ignore_expiry", 13) == 0) {
-      if (strncmp(value, "true", 4) == 0) {
-        cfg->ignore_expiry = true;
-        TSError("[url_sig] Plugin IGNORES sig expiration");
-      }
-    } else if (strncmp(line, "url_type", 8) == 0) {
-      if (strncmp(value, "pristine", 8) == 0) {
-        cfg->pristine_url_flag = true;
-        Dbg(dbg_ctl, "Pristine URLs (from config) will be used");
+      auto const  regex = std::make_shared<Regex>();
+      std::string re_error;
+      int         erroffset = 0;
+      if (regex->compile(std::string(value).c_str(), re_error, erroffset, 0)) {
+        cfg->excl_regex_match = [regex](std::string_view url) -> bool { return 
regex->exec(url); };
+      } else {
+        Dbg(dbg_ctl, "Regex compilation failed with error (%s) at character 
%d", re_error.c_str(), erroffset);
       }
-    } else {
-      TSError("[url_sig] Error parsing line %d of file %s (%s)", line_no, 
config_file, line);
+      break; // Only first excl_regex used.
     }
   }
+  file.close();
 
-  fclose(file);
-
-  if (argc > 3) {
+  // Handle pristineurl pparam override.
+  if (4 <= argc) {
     if (strcasecmp(argv[3], "pristineurl") == 0) {
       cfg->pristine_url_flag = true;
       Dbg(dbg_ctl, "Pristine URLs (from args) will be used");
-
     } else {
       snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - second pparam (if 
present) must be pristineurl");
       return TS_ERROR;
     }
   }
 
-  switch (cfg->err_status) {
-  case TS_HTTP_STATUS_MOVED_TEMPORARILY:
-    if (cfg->err_url.empty()) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Invalid config, 
err_status == 302, but err_url is empty");
-      return TS_ERROR;
-    }
-    break;
-  case TS_HTTP_STATUS_FORBIDDEN:
-    if (!cfg->err_url.empty()) {
-      snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Invalid config, 
err_status == 403, but err_url is not empty");
-      return TS_ERROR;
-    }
-    break;
-  default:
-    snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - Return code %d not 
supported", cfg->err_status);
-    return TS_ERROR;
+  if (cfg->ignore_expiry) {
+    TSError("[url_sig] Plugin IGNORES sig expiration");
   }
 
-  // Transfer ownership to ih which will later be deleted in 
TSRemapDeleteInstance.
-  *ih = (void *)cfg.release();
+  *ih = static_cast<void *>(cfg.release());
   return TS_SUCCESS;
 }
 
 void
 TSRemapDeleteInstance(void *ih)
 {
-  auto *cfg = static_cast<config *>(ih);
-  delete cfg;
-}
-
-static void
-err_log(const char *url, int url_len, const char *msg)
-{
-  if (msg && url) {
-    Dbg(dbg_ctl, "Test");
-
-    Dbg(dbg_ctl, "[URL=%.*s]: %s", url_len, url, msg);
-    TSError("[url_sig] [URL=%.*s]: %s", url_len, url, msg); // This goes to 
error.log
-  } else {
-    TSError("[url_sig] Invalid err_log request");
-  }
-}
-
-// See the README.  All Signing parameters must be concatenated to the end
-// of the url and any application query parameters.
-static char *
-getAppQueryString(const char *query_string, int query_length)
-{
-  int   done = 0;
-  char *p;
-  char  buf[MAX_QUERY_LEN + 1];
-
-  if (query_length > MAX_QUERY_LEN) {
-    Dbg(dbg_ctl, "Cannot process the query string as the length exceeds %d 
bytes", MAX_QUERY_LEN);
-    return nullptr;
-  }
-  memset(buf, 0, sizeof(buf));
-  memcpy(buf, query_string, query_length);
-  p = buf;
-
-  Dbg(dbg_ctl, "query_string: %s, query_length: %d", query_string, 
query_length);
-
-  do {
-    switch (*p) {
-    case 'A':
-    case 'C':
-    case 'E':
-    case 'K':
-    case 'P':
-    case 'S':
-      done = 1;
-      if ((p > buf) && (*(p - 1) == '&')) {
-        *(p - 1) = '\0';
-      } else {
-        (*p = '\0');
-      }
-      break;
-    default:
-      p = strchr(p, '&');
-      if (p == nullptr) {
-        done = 1;
-      } else {
-        p++;
-      }
-      break;
-    }
-  } while (!done);
-
-  if (strlen(buf) > 0) {
-    p = TSstrdup(buf);
-    return p;
-  } else {
-    return nullptr;
-  }
-}
-
-/** fixedBufferWrite safely writes no more than *dest_len bytes to *dest_end
- * from src. If copying src_len bytes to *dest_len would overflow, it returns
- * zero. *dest_end is advanced and *dest_len is decremented to account for the
- * written data. No null-terminators are written automatically (though they
- * could be copied with data).
- */
-static int
-fixedBufferWrite(char **dest_end, int *dest_len, const char *src, int src_len)
-{
-  if (src_len > *dest_len) {
-    return 0;
-  }
-  memcpy(*dest_end, src, src_len);
-  *dest_end += src_len;
-  *dest_len -= src_len;
-  return 1;
-}
-
-static char *
-urlParse(char const *const url_in, char const *anchor, char *new_path_seg, int 
new_path_seg_len, char *signed_seg,
-         unsigned int signed_seg_len)
-{
-  char         *segment[MAX_SEGMENTS];
-  char          url[8192]            = {'\0'};
-  unsigned char decoded_string[2048] = {'\0'};
-  char          new_url[8192]; /* new_url is not null_terminated */
-  char         *p = nullptr, *sig_anchor = nullptr, *saveptr = nullptr;
-  int           i = 0, numtoks = 0, sig_anchor_seg = 0;
-  size_t        decoded_len = 0;
-
-  strncat(url, url_in, sizeof(url) - strlen(url) - 1);
-
-  char *new_url_end      = new_url;
-  int   new_url_len_left = sizeof(new_url);
-
-  char *new_path_seg_end      = new_path_seg;
-  int   new_path_seg_len_left = new_path_seg_len;
-
-  char *skip = strchr(url, ':');
-  if (!skip || skip[1] != '/' || skip[2] != '/') {
-    return nullptr;
-  }
-  skip += 3;
-  // preserve the scheme in the new_url.
-  if (!fixedBufferWrite(&new_url_end, &new_url_len_left, url, skip - url)) {
-    TSError("insufficient space to copy schema into new_path_seg buffer.");
-    return nullptr;
-  }
-  Dbg(dbg_ctl, "%s:%d - new_url: %.*s\n", __FILE__, __LINE__, 
(int)(new_url_end - new_url), new_url);
-
-  // parse the url.
-  if ((p = strtok_r(skip, "/", &saveptr)) != nullptr) {
-    segment[numtoks++] = p;
-    do {
-      p = strtok_r(nullptr, "/", &saveptr);
-      if (p != nullptr) {
-        segment[numtoks] = p;
-        if (anchor != nullptr && sig_anchor_seg == 0) {
-          // look for the signed anchor string.
-          if ((sig_anchor = strcasestr(segment[numtoks], anchor)) != nullptr) {
-            // null terminate this segment just before he signing anchor, this 
should be a ';'.
-            *(sig_anchor - 1) = '\0';
-            if ((sig_anchor = strstr(sig_anchor, "=")) != nullptr) {
-              *sig_anchor = '\0';
-              sig_anchor++;
-              sig_anchor_seg = numtoks;
-            }
-          }
-        }
-        numtoks++;
-      }
-    } while (p != nullptr && numtoks < MAX_SEGMENTS);
-  } else {
-    return nullptr;
-  }
-  if ((numtoks >= MAX_SEGMENTS) || (numtoks < 3)) {
-    return nullptr;
-  }
-
-  // create a new path string for later use when dealing with query parameters.
-  // this string will not contain the signing parameters.  skips the fqdn by
-  // starting with segment 1.
-  for (i = 1; i < numtoks; i++) {
-    // if no signing anchor is found, skip the signed parameters segment.
-    if (sig_anchor == nullptr && i == numtoks - 2) {
-      // the signing parameters when no signature anchor is found, should be 
in the
-      // last path segment so skip them.
-      continue;
-    }
-    if (!fixedBufferWrite(&new_path_seg_end, &new_path_seg_len_left, 
segment[i], strlen(segment[i]))) {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-      return nullptr;
-    }
-    if (i != numtoks - 1) {
-      if (!fixedBufferWrite(&new_path_seg_end, &new_path_seg_len_left, "/", 
1)) {
-        TSError("insufficient space to copy into new_path_seg buffer.");
-        return nullptr;
-      }
-    }
-  }
-  *new_path_seg_end = '\0';
-  Dbg(dbg_ctl, "new_path_seg: %s", new_path_seg);
-
-  // save the encoded signing parameter data
-  if (sig_anchor != nullptr) { // a signature anchor string was found.
-    if (strlen(sig_anchor) < signed_seg_len) {
-      memcpy(signed_seg, sig_anchor, strlen(sig_anchor));
-    } else {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-    }
-  } else { // no signature anchor string was found, assume it is in the last 
path segment.
-    if (strlen(segment[numtoks - 2]) < signed_seg_len) {
-      memcpy(signed_seg, segment[numtoks - 2], strlen(segment[numtoks - 2]));
-    } else {
-      TSError("insufficient space to copy into new_path_seg buffer.");
-      return nullptr;
-    }
-  }
-  Dbg(dbg_ctl, "signed_seg: %s", signed_seg);
-
-  // no signature anchor was found so decode and save the signing parameters 
assumed
-  // to be in the last path segment.
-  if (sig_anchor == nullptr) {
-    if (TSBase64Decode(segment[numtoks - 2], strlen(segment[numtoks - 2]), 
decoded_string, sizeof(decoded_string), &decoded_len) !=
-        TS_SUCCESS) {
-      Dbg(dbg_ctl, "Unable to decode the  path parameter string.");
-    }
-  } else {
-    if (TSBase64Decode(sig_anchor, strlen(sig_anchor), decoded_string, 
sizeof(decoded_string), &decoded_len) != TS_SUCCESS) {
-      Dbg(dbg_ctl, "Unable to decode the  path parameter string.");
-    }
-  }
-  Dbg(dbg_ctl, "decoded_string: %s", decoded_string);
-
-  {
-    int oob = 0; /* Out Of Buffer */
-
-    for (i = 0; i < numtoks; i++) {
-      // cp the base64 decoded string.
-      if (i == sig_anchor_seg && sig_anchor != nullptr) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, segment[i], 
strlen(segment[i]))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, 
reinterpret_cast<char *>(decoded_string),
-                              strlen(reinterpret_cast<char 
*>(decoded_string)))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-
-        continue;
-      } else if (i == numtoks - 2 && sig_anchor == nullptr) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, 
reinterpret_cast<char *>(decoded_string),
-                              strlen(reinterpret_cast<char 
*>(decoded_string)))) {
-          oob = 1;
-          break;
-        }
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-        continue;
-      }
-      if (!fixedBufferWrite(&new_url_end, &new_url_len_left, segment[i], 
strlen(segment[i]))) {
-        oob = 1;
-        break;
-      }
-      if (i < numtoks - 1) {
-        if (!fixedBufferWrite(&new_url_end, &new_url_len_left, "/", 1)) {
-          oob = 1;
-          break;
-        }
-      }
-    }
-    if (oob) {
-      TSError("insufficient space to copy into new_url.");
-    }
-  }
-  return TSstrndup(new_url, new_url_end - new_url);
+  delete static_cast<UrlSigConfig *>(ih);
 }
 
 TSRemapStatus
 TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
 {
-  const struct config *cfg = static_cast<const struct config *>(ih);
-
-  int          url_len         = 0;
-  int          current_url_len = 0;
-  uint64_t     expiration      = 0;
-  int          algorithm       = -1;
-  int          keyindex        = -1;
-  int          cmp_res;
-  int          rval;
-  unsigned int i               = 0;
-  int          j               = 0;
-  unsigned int sig_len         = 0;
-  bool         has_path_params = false;
-
-  /* all strings are locally allocated except url... about 25k per instance */
-  char *const   current_url       = TSUrlStringGet(rri->requestBufp, 
rri->requestUrl, &current_url_len);
-  char         *url               = current_url;
-  char          path_params[8192] = {'\0'}, new_path[8192] = {'\0'};
-  char          signed_part[8192]           = {'\0'}; // this initializes the 
whole array and is needed
-  char          urltokstr[8192]             = {'\0'};
-  char          client_ip[INET6_ADDRSTRLEN] = {'\0'}; // chose the larger ipv6 
size
-  char          ipstr[INET6_ADDRSTRLEN]     = {'\0'}; // chose the larger ipv6 
size
-  unsigned char sig[MAX_SIG_SIZE + 1];
-  char          sig_string[2 * MAX_SIG_SIZE + 1];
-
-  if (current_url_len >= MAX_REQ_LEN - 1) {
-    err_log(current_url, current_url_len, "Request Url string too long");
-    goto deny;
-  }
+  auto const *const cfg = static_cast<UrlSigConfig *>(ih);
+
+  // Get URL.
+  int         url_len         = 0;
+  char *const current_url_raw = TSUrlStringGet(rri->requestBufp, 
rri->requestUrl, &url_len);
+  std::string url_to_check(current_url_raw, url_len);
+  TSfree(current_url_raw);
 
   if (cfg->pristine_url_flag) {
     TSMBuffer    mbuf;
     TSMLoc       ul;
     TSReturnCode rc = TSHttpTxnPristineUrlGet(txnp, &mbuf, &ul);
     if (rc != TS_SUCCESS) {
-      TSError("[url_sig] Failed call to TSHttpTxnPristineUrlGet()");
+      Dbg(dbg_ctl, "[url_sig] Failed call to TSHttpTxnPristineUrlGet()");
       goto deny;
     }
-    url = TSUrlStringGet(mbuf, ul, &url_len);
-    if (url_len >= MAX_REQ_LEN - 1) {
-      err_log(url, url_len, "Pristine URL string too long.");
+    int         pristine_len = 0;
+    char *const pristine_raw = TSUrlStringGet(mbuf, ul, &pristine_len);
+    url_to_check             = std::string(pristine_raw, pristine_len);
+    TSfree(pristine_raw);
+
+    if (static_cast<int>(url_to_check.size()) >= MAX_REQ_LEN - 1) {
+      Dbg(dbg_ctl, "[url_sig] Pristine URL string too long.");
       goto deny;
     }
-  } else {
-    url_len = current_url_len;
   }
 
-  Dbg(dbg_ctl, "%s", url);
-
-  if (cfg->excl_regex) {
-    /* Only search up to the first ? or # */
-    const char *base_url_end = url;
-    while (*base_url_end && !(*base_url_end == '?' || *base_url_end == '#')) {
-      ++base_url_end;
-    }
-    const size_t len = base_url_end - url;
+  Dbg(dbg_ctl, "%s", url_to_check.c_str());
 
-    if (cfg->excl_regex->exec(std::string_view(url, len))) {
-      // The user configured this URL to be excluded from signing checks.
-      goto allow;
-    }
-  }
-
-  // Block needed due to goto.
+  // Get client IP.
   {
-    const char *query = strchr(url, '?');
-
-    // check for path params.
-    if (query == nullptr || strstr(query, "E=") == nullptr) {
-      char *const parsed =
-        urlParse(url, cfg->sig_anchor.empty() ? nullptr : 
cfg->sig_anchor.c_str(), new_path, 8192, path_params, 8192);
-      if (parsed == nullptr) {
-        err_log(url, url_len, "Unable to parse/decode new url path 
parameters");
-        goto deny;
-      }
+    std::string const client_ip = get_client_ip_str(txnp);
 
-      has_path_params = true;
-      query           = strstr(parsed, ";");
+    // Validate.
+    UrlSigResult const result = validate_url(*cfg, url_to_check, client_ip);
 
-      if (query == nullptr) {
-        err_log(url, url_len, "Has no signing query string or signing path 
parameters.");
-        TSfree(parsed);
-        goto deny;
+    if (result.status == UrlSigStatus::ALLOW) {
+      // Apply path rewrite if path params mode.
+      if (result.has_path_params && !result.new_path.empty()) {
+        TSUrlPathSet(rri->requestBufp, rri->requestUrl, 
result.new_path.c_str(), result.new_path.size());
       }
 
-      if (url != current_url) {
-        TSfree(url);
+      // Set or clear query string.
+      if (!result.app_query.empty()) {
+        TSUrlHttpQuerySet(rri->requestBufp, rri->requestUrl, 
result.app_query.c_str(), result.app_query.size());
+      } else {
+        TSUrlHttpQuerySet(rri->requestBufp, rri->requestUrl, nullptr, 0);

Review Comment:
   The return value from `TSUrlHttpQuerySet()` is ignored. The prior 
implementation checked for `TS_SUCCESS` and logged failures; keeping that check 
helps avoid silently failing to strip/set the query string (which can affect 
cache behavior).
   ```suggestion
           TSReturnCode rc =
             TSUrlHttpQuerySet(rri->requestBufp, rri->requestUrl, 
result.app_query.c_str(), result.app_query.size());
           if (rc != TS_SUCCESS) {
             Dbg(dbg_ctl, "[url_sig] Failed call to TSUrlHttpQuerySet() while 
setting query string.");
           }
         } else {
           TSReturnCode rc = TSUrlHttpQuerySet(rri->requestBufp, 
rri->requestUrl, nullptr, 0);
           if (rc != TS_SUCCESS) {
             Dbg(dbg_ctl, "[url_sig] Failed call to TSUrlHttpQuerySet() while 
clearing query string.");
           }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to