]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Add %byte{value} logformat code for logging or sending any byte (#1588)
authorEduard Bagdasaryan <eduard.bagdasaryan@measurement-factory.com>
Wed, 29 Nov 2023 12:50:05 +0000 (12:50 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Wed, 29 Nov 2023 12:50:16 +0000 (12:50 +0000)
This feature is needed by at least the statsd tool receiving TCP log
info: https://github.com/statsd/statsd/blob/7c07eec/docs/server.md

No support for zero byte values yet because existing Format::assemble()
code does not support that out of the box, and there is no known need
for such support. It can be added later (without backward compatibility
problems) if needed.

src/cf.data.pre
src/format/ByteCode.h
src/format/Format.cc
src/format/Token.cc
src/format/Token.h

index a89022cc4d0c6d95f817b734a74899f6d5fa60dc..94e09480a0a9bff4f29af7b2ac9c1a3308258f6f 100644 (file)
@@ -4685,6 +4685,13 @@ DOC_START
        Format codes:
 
                %       a literal % character
+
+               byte{value}     Adds a single byte with the given value (e.g., %byte{10}
+                       adds an ASCII LF character a.k.a. "new line" or "\n"). The value
+                       parameter is required and must be a positive decimal integer not
+                       exceeding 255. Zero-valued bytes (i.e. ASCII NUL characters) are
+                       not yet supported.
+
                sn      Unique sequence number per log line entry
                err_code    The ID of an error response served by Squid or
                                a similar internal error identifier.
index 890abc0785e91d332b30007cc34bbadd7c859048..c8f7ace221682b01dc2e5d3d9a18f549de26beea 100644 (file)
@@ -33,6 +33,8 @@ typedef enum {
     /* arbitrary string between tokens */
     LFT_STRING,
 
+    LFT_BYTE,
+
     /* client TCP connection remote end details */
     LFT_CLIENT_IP_ADDRESS,
     LFT_CLIENT_FQDN,
index d0c6719f458edcd1fb1684f05914d58140e3a669..892c5f01ec8d63dae5a27875008a7366641e4d0e 100644 (file)
@@ -399,6 +399,12 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS
             out = "";
             break;
 
+        case LFT_BYTE:
+            tmp[0] = static_cast<char>(fmt->data.byteValue);
+            tmp[1] = '\0';
+            out = tmp;
+            break;
+
         case LFT_STRING:
             out = fmt->data.string;
             break;
index 2a24c8a3e5cc9816bbc743013634b8eded36a320..671cde55cc14b5da7a8c715ad7770dca6c329200 100644 (file)
@@ -11,6 +11,7 @@
 #include "format/Token.h"
 #include "format/TokenTableEntry.h"
 #include "globals.h"
+#include "parser/Tokenizer.h"
 #include "proxyp/Elements.h"
 #include "sbuf/Stream.h"
 #include "SquidConfig.h"
@@ -139,6 +140,7 @@ static TokenTableEntry TokenTable2C[] = {
 
 /// Miscellaneous >2 byte tokens
 static TokenTableEntry TokenTableMisc[] = {
+    TokenTableEntry("byte", LFT_BYTE),
     TokenTableEntry(">eui", LFT_CLIENT_EUI),
     TokenTableEntry(">qos", LFT_CLIENT_LOCAL_TOS),
     TokenTableEntry("<qos", LFT_SERVER_LOCAL_TOS),
@@ -285,6 +287,66 @@ Format::Token::scanForToken(TokenTableEntry const table[], const char *cur)
     return cur;
 }
 
+// TODO: Reduce code duplication across this and other custom integer parsers.
+/// interprets input as an unsigned decimal integer that fits the specified Integer type
+template <typename Integer>
+static Integer
+ParseUnsignedDecimalInteger(const char *description, const SBuf &rawInput)
+{
+    constexpr auto minValue = std::numeric_limits<Integer>::min();
+    constexpr auto maxValue = std::numeric_limits<Integer>::max();
+
+    Parser::Tokenizer tok(rawInput);
+    if (tok.skip('0')) {
+        if (!tok.atEnd()) {
+            // e.g., 077, 0xFF, 0b101, or 0.1
+            throw TextException(ToSBuf("Malformed ", description,
+                                       ": Expected a decimal integer without leading zeros but got '",
+                                       rawInput, "'"), Here());
+        }
+        // for simplicity, we currently assume that zero is always in range
+        static_assert(minValue <= 0);
+        static_assert(0 <= maxValue);
+        return Integer(0);
+    }
+    // else the value might still be zero (e.g., -0)
+
+    // check that our caller is compatible with Tokenizer::int64() use below
+    using ParsedInteger = int64_t;
+    static_assert(minValue >= std::numeric_limits<ParsedInteger>::min());
+    static_assert(maxValue <= std::numeric_limits<ParsedInteger>::max());
+
+    ParsedInteger rawValue = 0;
+    if (!tok.int64(rawValue, 10, false)) {
+        // e.g., FF, -1, or 18446744073709551616
+        // TODO: Provide better diagnostic for values exceeding int64_t maximum.
+        throw TextException(ToSBuf("Malformed ", description,
+                                   ": Expected an unsigned decimal integer but got '",
+                                   rawInput, "'"), Here());
+    }
+
+    if (!tok.atEnd()) {
+        // e.g., 1,000, 1.0, or 1e6
+        throw TextException(ToSBuf("Malformed ", description,
+                                   ": Trailing garbage after ", rawValue, " in '",
+                                   rawInput, "'"), Here());
+    }
+
+    if (rawValue > maxValue) {
+        throw TextException(ToSBuf("Malformed ", description,
+                                   ": Expected an integer value not exceeding ", maxValue,
+                                   " but got ", rawValue), Here());
+    }
+
+    if (rawValue < minValue) {
+        throw TextException(ToSBuf("Malformed ", description,
+                                   ": Expected an integer value not below ", minValue,
+                                   " but got ", rawValue), Here());
+    }
+
+    return Integer(rawValue);
+}
+
 /* parses a single token. Returns the token length in characters,
  * and fills in the lt item with the token information.
  * def is for sure null-terminated
@@ -476,6 +538,16 @@ Format::Token::parse(const char *def, Quoting *quoting)
 
     switch (type) {
 
+    case LFT_BYTE:
+        if (!data.string)
+            throw TextException("logformat %byte requires a parameter (e.g., %byte{10})", Here());
+        // TODO: Convert Format::Token::data.string to SBuf.
+        if (const auto v = ParseUnsignedDecimalInteger<uint8_t>("logformat %byte{value}", SBuf(data.string)))
+            data.byteValue = v;
+        else
+            throw TextException("logformat %byte{n} does not support zero n values yet", Here());
+        break;
+
 #if USE_ADAPTATION
     case LFT_ADAPTATION_LAST_HEADER:
 #endif
@@ -682,6 +754,7 @@ Format::Token::Token() : type(LFT_NONE),
     data.header.element = nullptr;
     data.header.separator = ',';
     data.headerId = ProxyProtocol::Two::htUnknown;
+    data.byteValue = 0;
 }
 
 Format::Token::~Token()
index d69cd464242094c7b2b4a1246d29a5fc1cd0448c..fc483baeff95116992724e6d46131f9539de0e4b 100644 (file)
@@ -60,6 +60,8 @@ public:
             char *element;
             char separator;
         } header;
+
+        uint8_t byteValue; // %byte{} parameter or zero
     } data;
     int widthMin; ///< minimum field width
     int widthMax; ///< maximum field width