Ported from:

commit ea12a34d338b962707d5078d6d1fc7c6eb119a22
Author: Alex Rousskov <rousskov@measurement-factory.com>
Date:   2020-05-13 14:05:00 +0000

    Validate Content-Length value prefix (#629)
    
    The new code detects all invalid Content-Length prefixes but the old
    code was already rejecting most invalid prefixes using strtoll(). The
    newly covered (and now rejected) invalid characters are
    
    * explicit "+" sign;
    * explicit "-" sign in "-0" values;
    * isspace(3) characters that are not (relaxed) OWS characters.
    
    In most deployment environments, the last set is probably empty because
    the relaxed OWS set has all the POSIX/C isspace(3) characters but the
    new line, and the new line is unlikely to sneak in past other checks.
    
    Thank you, Amit Klein <amit.klein@safebreach.com>, for elevating the
    importance of this 2016 TODO (added in commit a1b9ec2).

Index: squid-3.5.21/CONTRIBUTORS
===================================================================
--- squid-3.5.21.orig/CONTRIBUTORS
+++ squid-3.5.21/CONTRIBUTORS
@@ -23,6 +23,7 @@ Thank you!
     Alex Wu <alex_wu2012@hotmail.com>
     Alin Nastac <mrness@gentoo.org>
     Alter <alter@alter.org.ua>
+    Amit Klein <amit.klein@safebreach.com>
     Amos Jeffries <amosjeffries@squid-cache.org>
     Amos Jeffries <squid3@treenet.co.nz>
     Amos <squid3@treenet.co.nz>
Index: squid-3.5.21/src/http/Makefile.am
===================================================================
--- squid-3.5.21.orig/src/http/Makefile.am
+++ squid-3.5.21/src/http/Makefile.am
@@ -11,8 +11,12 @@ include $(top_srcdir)/src/TestHeaders.am
 noinst_LTLIBRARIES = libsquid-http.la
 
 libsquid_http_la_SOURCES = \
+	ContentLengthInterpreter.cc \
+	ContentLengthInterpreter.h \
 	MethodType.cc \
 	MethodType.h \
+	Parser.h \
+	Parser.cc \
 	ProtocolVersion.h \
 	StatusCode.cc \
 	StatusCode.h \
Index: squid-3.5.21/src/http/ContentLengthInterpreter.cc
===================================================================
--- /dev/null
+++ squid-3.5.21/src/http/ContentLengthInterpreter.cc
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+/* DEBUG: section 55    HTTP Header */
+
+#include "squid.h"
+#include "base/CharacterSet.h"
+#include "Debug.h"
+#include "http/ContentLengthInterpreter.h"
+#include "http/Parser.h"
+#include "HttpHeaderTools.h"
+#include "SquidConfig.h"
+#include "SquidString.h"
+#include "StrList.h"
+
+Http::ContentLengthInterpreter::ContentLengthInterpreter(const int aDebugLevel):
+    value(-1),
+    headerWideProblem(nullptr),
+    debugLevel(aDebugLevel),
+    sawBad(false),
+    needsSanitizing(false),
+    sawGood(false)
+{
+}
+
+/// checks whether all characters before the Content-Length number are allowed
+/// \returns the start of the digit sequence (or nil on errors)
+const char *
+Http::ContentLengthInterpreter::findDigits(const char *prefix, const char * const valueEnd) const
+{
+    // skip leading OWS in RFC 7230's `OWS field-value OWS`
+    const CharacterSet &whitespace = Http::One::Parser::WhitespaceCharacters();
+    while (prefix < valueEnd) {
+        const auto ch = *prefix;
+        if (CharacterSet::DIGIT[ch])
+            return prefix; // common case: a pre-trimmed field value
+        if (!whitespace[ch])
+            return nullptr; // (trimmed) length does not start with a digit
+        ++prefix;
+    }
+    return nullptr; // empty or whitespace-only value
+}
+
+/// checks whether all characters after the Content-Length are allowed
+bool
+Http::ContentLengthInterpreter::goodSuffix(const char *suffix, const char * const end) const
+{
+    // optimize for the common case that does not need delimiters
+    if (suffix == end)
+        return true;
+
+    for (const CharacterSet &delimiters = Http::One::Parser::DelimiterCharacters();
+            suffix < end; ++suffix) {
+        if (!delimiters[*suffix])
+            return false;
+    }
+    // needsSanitizing = true; // TODO: Always remove trailing whitespace?
+    return true; // including empty suffix
+}
+
+/// handles a single-token Content-Length value
+/// rawValue null-termination requirements are those of httpHeaderParseOffset()
+bool
+Http::ContentLengthInterpreter::checkValue(const char *rawValue, const int valueSize)
+{
+    Must(!sawBad);
+
+    const auto valueEnd = rawValue + valueSize;
+
+    const auto digits = findDigits(rawValue, valueEnd);
+    if (!digits) {
+        debugs(55, debugLevel, "WARNING: Leading garbage or empty value in" << Raw("Content-Length", rawValue, valueSize));
+        sawBad = true;
+        return false;
+    }
+
+    int64_t latestValue = -1;
+    char *suffix = nullptr;
+
+    if (!httpHeaderParseOffset(digits, &latestValue, &suffix)) {
+        debugs(55, DBG_IMPORTANT, "WARNING: Malformed" << Raw("Content-Length", rawValue, valueSize));
+        sawBad = true;
+        return false;
+    }
+
+    if (latestValue < 0) {
+        debugs(55, debugLevel, "WARNING: Negative" << Raw("Content-Length", rawValue, valueSize));
+        sawBad = true;
+        return false;
+    }
+
+    // check for garbage after the number
+    if (!goodSuffix(suffix, valueEnd)) {
+        debugs(55, debugLevel, "WARNING: Trailing garbage in" << Raw("Content-Length", rawValue, valueSize));
+        sawBad = true;
+        return false;
+    }
+
+    if (sawGood) {
+        /* we have found at least two, possibly identical values */
+
+        needsSanitizing = true; // replace identical values with a single value
+
+        const bool conflicting = value != latestValue;
+        if (conflicting)
+            headerWideProblem = "Conflicting"; // overwrite any lesser problem
+        else if (!headerWideProblem) // preserve a possibly worse problem
+            headerWideProblem = "Duplicate";
+
+        // with relaxed_header_parser, identical values are permitted
+        sawBad = !Config.onoff.relaxed_header_parser || conflicting;
+        return false; // conflicting or duplicate
+    }
+
+    sawGood = true;
+    value = latestValue;
+    return true;
+}
+
+/// handles Content-Length: a, b, c
+bool
+Http::ContentLengthInterpreter::checkList(const String &list)
+{
+    Must(!sawBad);
+
+    if (!Config.onoff.relaxed_header_parser) {
+        debugs(55, debugLevel, "WARNING: List-like" << Raw("Content-Length", list.rawBuf(), list.size()));
+        sawBad = true;
+        return false;
+    }
+
+    needsSanitizing = true; // remove extra commas (at least)
+
+    const char *pos = nullptr;
+    const char *item = nullptr;;
+    int ilen = -1;
+    while (strListGetItem(&list, ',', &item, &ilen, &pos)) {
+        if (!checkValue(item, ilen) && sawBad)
+            break;
+        // keep going after a duplicate value to find conflicting ones
+    }
+    return false; // no need to keep this list field; it will be sanitized away
+}
+
+bool
+Http::ContentLengthInterpreter::checkField(const String &rawValue)
+{
+    if (sawBad)
+        return false; // one rotten apple is enough to spoil all of them
+
+    // TODO: Optimize by always parsing the first integer first.
+    return rawValue.pos(',') ?
+           checkList(rawValue) :
+           checkValue(rawValue.rawBuf(), rawValue.size());
+}
+
Index: squid-3.5.21/src/http/ContentLengthInterpreter.h
===================================================================
--- /dev/null
+++ squid-3.5.21/src/http/ContentLengthInterpreter.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#ifndef SQUID_SRC_HTTP_CONTENTLENGTH_INTERPRETER_H
+#define SQUID_SRC_HTTP_CONTENTLENGTH_INTERPRETER_H
+
+class String;
+
+namespace Http
+{
+
+/// Finds the intended Content-Length value while parsing message-header fields.
+/// Deals with complications such as value lists and/or repeated fields.
+class ContentLengthInterpreter
+{
+public:
+    explicit ContentLengthInterpreter(const int aDebugLevel);
+
+    /// updates history based on the given message-header field
+    /// \return true iff the field should be added/remembered for future use
+    bool checkField(const String &field);
+
+    /// intended Content-Length value if sawGood is set and sawBad is not set
+    /// meaningless otherwise
+    int64_t value;
+
+    /* for debugging (declared here to minimize padding) */
+    const char *headerWideProblem; ///< worst header-wide problem found (or nil)
+    const int debugLevel; ///< debugging level for certain warnings
+
+    /// whether a malformed Content-Length value was present
+    bool sawBad;
+
+    /// whether all remembered fields should be removed
+    /// removed fields ought to be replaced with the intended value (if known)
+    /// irrelevant if sawBad is set
+    bool needsSanitizing;
+
+    /// whether a valid field value was present, possibly among problematic ones
+    /// irrelevant if sawBad is set
+    bool sawGood;
+
+protected:
+    const char *findDigits(const char *prefix, const char *valueEnd) const;
+    bool goodSuffix(const char *suffix, const char * const end) const;
+    bool checkValue(const char *start, const int size);
+    bool checkList(const String &list);
+};
+
+} // namespace Http
+
+#endif /* SQUID_SRC_HTTP_CONTENTLENGTH_INTERPRETER_H */
+
Index: squid-3.5.21/src/http/Parser.h
===================================================================
--- /dev/null
+++ squid-3.5.21/src/http/Parser.h
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#ifndef _SQUID_SRC_HTTP_ONE_PARSER_H
+#define _SQUID_SRC_HTTP_ONE_PARSER_H
+
+#include "anyp/ProtocolVersion.h"
+// #include "http/one/forward.h"
+#include "http/StatusCode.h"
+#include "src/SBuf.h"
+
+namespace Http {
+namespace One {
+
+// Parser states
+enum ParseState {
+    HTTP_PARSE_NONE,      ///< initialized, but nothing usefully parsed yet
+    HTTP_PARSE_FIRST,     ///< HTTP/1 message first-line
+    HTTP_PARSE_CHUNK_SZ,  ///< HTTP/1.1 chunked encoding chunk-size
+    HTTP_PARSE_CHUNK_EXT, ///< HTTP/1.1 chunked encoding chunk-ext
+    HTTP_PARSE_CHUNK,     ///< HTTP/1.1 chunked encoding chunk-data
+    HTTP_PARSE_MIME,      ///< HTTP/1 mime-header block
+    HTTP_PARSE_DONE       ///< parsed a message header, or reached a terminal syntax error
+};
+
+/** HTTP/1.x protocol parser
+ *
+ * Works on a raw character I/O buffer and tokenizes the content into
+ * the major CRLF delimited segments of an HTTP/1 procotol message:
+ *
+ * \item first-line (request-line / simple-request / status-line)
+ * \item mime-header 0*( header-name ':' SP field-value CRLF)
+ */
+class Parser : public RefCountable
+{
+public:
+    typedef SBuf::size_type size_type;
+
+//    Parser() : parseStatusCode(Http::scNone), parsingStage_(HTTP_PARSE_NONE), hackExpectsMime_(false) {}
+//    virtual ~Parser() {}
+
+    /// Set this parser back to a default state.
+    /// Will DROP any reference to a buffer (does not free).
+//    virtual void clear() = 0;
+
+    /// attempt to parse a message from the buffer
+    /// \retval true if a full message was found and parsed
+    /// \retval false if incomplete, invalid or no message was found
+//    virtual bool parse(const SBuf &aBuf) = 0;
+
+    /** Whether the parser is waiting on more data to complete parsing a message.
+     * Use to distinguish between incomplete data and error results
+     * when parse() returns false.
+     */
+//    bool needsMoreData() const {return parsingStage_!=HTTP_PARSE_DONE;}
+
+    /// size in bytes of the first line including CRLF terminator
+//    virtual size_type firstLineSize() const = 0;
+
+    /// size in bytes of the message headers including CRLF terminator(s)
+    /// but excluding first-line bytes
+//    size_type headerBlockSize() const {return mimeHeaderBlock_.length();}
+
+    /// size in bytes of HTTP message block, includes first-line and mime headers
+    /// excludes any body/entity/payload bytes
+    /// excludes any garbage prefix before the first-line
+//    size_type messageHeaderSize() const {return firstLineSize() + headerBlockSize();}
+
+    /// buffer containing HTTP mime headers, excluding message first-line.
+//    SBuf mimeHeader() const {return mimeHeaderBlock_;}
+
+    /// the protocol label for this message
+//    const AnyP::ProtocolVersion & messageProtocol() const {return msgProtocol_;}
+
+    /**
+     * Scan the mime header block (badly) for a Host header.
+     *
+     * BUG: omits lines when searching for headers with obs-fold or multiple entries.
+     *
+     * BUG: limits output to just 1KB when Squid accepts up to 64KB line length.
+     *
+     * \return A pointer to a field-value of the first matching field-name, or NULL.
+     */
+//    char *getHostHeaderField();
+
+    /// the remaining unprocessed section of buffer
+//    const SBuf &remaining() const {return buf_;}
+
+    /**
+     * HTTP status code resulting from the parse process.
+     * to be used on the invalid message handling.
+     *
+     * Http::scNone indicates incomplete parse,
+     * Http::scOkay indicates no error,
+     * other codes represent a parse error.
+     */
+//    Http::StatusCode parseStatusCode;
+
+    /// Whitespace between regular protocol elements.
+    /// Seen in RFCs as OWS, RWS, BWS, SP/HTAB but may be "relaxed" by us.
+    /// See also: DelimiterCharacters().
+    static const CharacterSet &WhitespaceCharacters();
+
+    /// Whitespace between protocol elements in restricted contexts like
+    /// request line, status line, asctime-date, and credentials
+    /// Seen in RFCs as SP but may be "relaxed" by us.
+    /// See also: WhitespaceCharacters().
+    /// XXX: Misnamed and overused.
+    static const CharacterSet &DelimiterCharacters();
+
+//protected:
+    /**
+     * detect and skip the CRLF or (if tolerant) LF line terminator
+     * consume from the tokenizer.
+     *
+     * throws if non-terminator is detected.
+     * \retval true only if line terminator found.
+     * \retval false incomplete or missing line terminator, need more data.
+     */
+//    bool skipLineTerminator(Http1::Tokenizer &tok) const;
+
+    /**
+     * Scan to find the mime headers block for current message.
+     *
+     * \retval true   If mime block (or a blocks non-existence) has been
+     *                identified accurately within limit characters.
+     *                mimeHeaderBlock_ has been updated and buf_ consumed.
+     *
+     * \retval false  An error occurred, or no mime terminator found within limit.
+     */
+//    bool grabMimeBlock(const char *which, const size_t limit);
+
+    /// RFC 7230 section 2.6 - 7 magic octets
+//    static const SBuf Http1magic;
+
+    /// bytes remaining to be parsed
+//    SBuf buf_;
+
+    /// what stage the parser is currently up to
+//    ParseState parsingStage_;
+
+    /// what protocol label has been found in the first line (if any)
+//    AnyP::ProtocolVersion msgProtocol_;
+
+    /// buffer holding the mime headers (if any)
+//    SBuf mimeHeaderBlock_;
+
+    /// Whether the invalid HTTP as HTTP/0.9 hack expects a mime header block
+//    bool hackExpectsMime_;
+
+//private:
+//    void cleanMimePrefix();
+//    void unfoldMime();
+};
+
+/// skips and, if needed, warns about RFC 7230 BWS ("bad" whitespace)
+/// \returns true (always; unlike all the skip*() functions)
+//bool ParseBws(Tokenizer &tok);
+
+/// the right debugs() level for logging HTTP violation messages
+//int ErrorLevel();
+
+} // namespace One
+} // namespace Http
+
+#endif /*  _SQUID_SRC_HTTP_ONE_PARSER_H */
+
Index: squid-3.5.21/src/HttpHeaderTools.cc
===================================================================
--- squid-3.5.21.orig/src/HttpHeaderTools.cc
+++ squid-3.5.21/src/HttpHeaderTools.cc
@@ -187,13 +187,28 @@ httpHeaderParseInt(const char *start, in
 }
 
 int
-httpHeaderParseOffset(const char *start, int64_t * value)
+httpHeaderParseOffset(const char *start, int64_t * value, char **endPtr)
 {
+    char *end = nullptr;
     errno = 0;
-    int64_t res = strtoll(start, NULL, 10);
-    if (!res && EINVAL == errno)    /* maybe not portable? */
+    int64_t res = strtoll(start, &end, 10);
+
+    if (errno && !res) {
+        debugs(66, 7, "failed to parse malformed offset in " << start);
+        return 0;
+    }
+    if (errno == ERANGE && (res == LLONG_MIN || res == LLONG_MAX)) { // no overflow
+        debugs(66, 7, "failed to parse huge offset in " << start);
+        return 0;
+    }
+    if (start == end) {
+        debugs(66, 7, "failed to parse empty offset");
         return 0;
+    }
     *value = res;
+    if (endPtr)
+        *endPtr = end;
+    debugs(66, 7, "offset " << start << " parsed as " << res);
     return 1;
 }
 
Index: squid-3.5.21/src/HttpHeaderTools.h
===================================================================
--- squid-3.5.21.orig/src/HttpHeaderTools.h
+++ squid-3.5.21/src/HttpHeaderTools.h
@@ -113,7 +113,13 @@ public:
     bool quoted;
 };
 
-int httpHeaderParseOffset(const char *start, int64_t * off);
+/// A strtoll(10) wrapper that checks for strtoll() failures and other problems.
+/// XXX: This function is not fully compatible with some HTTP syntax rules.
+/// Just like strtoll(), allows whitespace prefix, a sign, and _any_ suffix.
+/// Requires at least one digit to be present.
+/// Sets "off" and "end" arguments if and only if no problems were found.
+/// \return true if and only if no problems were found.
+int httpHeaderParseOffset(const char *start, int64_t * off, char **endPtr = nullptr);
 
 HttpHeaderFieldInfo *httpHeaderBuildFieldsInfo(const HttpHeaderFieldAttrs * attrs, int count);
 void httpHeaderDestroyFieldsInfo(HttpHeaderFieldInfo * info, int count);
Index: squid-3.5.21/src/HttpHeader.cc
===================================================================
--- squid-3.5.21.orig/src/HttpHeader.cc
+++ squid-3.5.21/src/HttpHeader.cc
@@ -28,6 +28,7 @@
 #include "Store.h"
 #include "StrList.h"
 #include "TimeOrTag.h"
+#include "http/ContentLengthInterpreter.h"
 
 #include <algorithm>
 
@@ -588,7 +589,7 @@ int
 HttpHeader::parse(const char *header_start, const char *header_end)
 {
     const char *field_ptr = header_start;
-    HttpHeaderEntry *e, *e2;
+    HttpHeaderEntry *e;
     int warnOnError = (Config.onoff.relaxed_header_parser <= 0 ? DBG_IMPORTANT : 2);
 
     PROF_start(HttpHeaderParse);
@@ -605,6 +606,7 @@ HttpHeader::parse(const char *header_sta
         return reset();
     }
 
+    Http::ContentLengthInterpreter clen(warnOnError);
     /* common format headers are "<name>:[ws]<value>" lines delimited by <CRLF>.
      * continuation lines start with a (single) space or tab */
     while (field_ptr < header_end) {
@@ -690,43 +692,15 @@ HttpHeader::parse(const char *header_sta
             return reset();
         }
 
-        // XXX: RFC 7230 Section 3.3.3 item #4 requires sending a 502 error in
-        // several cases that we do not yet cover. TODO: Rewrite to cover more.
-        if (e->id == HDR_CONTENT_LENGTH && (e2 = findEntry(e->id)) != NULL) {
-            if (e->value != e2->value) {
-                int64_t l1, l2;
-                debugs(55, warnOnError, "WARNING: found two conflicting content-length headers in {" <<
-                       getStringPrefix(header_start, header_end) << "}");
-
-                if (!Config.onoff.relaxed_header_parser) {
-                    delete e;
-                    PROF_stop(HttpHeaderParse);
-                    return reset();
-                }
-
-                if (!httpHeaderParseOffset(e->value.termedBuf(), &l1)) {
-                    debugs(55, DBG_IMPORTANT, "WARNING: Unparseable content-length '" << e->value << "'");
-                    delete e;
-                    continue;
-                } else if (!httpHeaderParseOffset(e2->value.termedBuf(), &l2)) {
-                    debugs(55, DBG_IMPORTANT, "WARNING: Unparseable content-length '" << e2->value << "'");
-                    delById(e2->id);
-                } else {
-                    if (l1 != l2)
-                        conflictingContentLength_ = true;
-                    delete e;
-                    continue;
-                }
-            } else {
-                debugs(55, warnOnError, "NOTICE: found double content-length header");
-                delete e;
+        if (e->id == HDR_CONTENT_LENGTH && !clen.checkField(e->value)) {
+            delete e;
 
-                if (Config.onoff.relaxed_header_parser)
-                    continue;
+            if (Config.onoff.relaxed_header_parser)
+                continue; // clen has printed any necessary warnings
 
-                PROF_stop(HttpHeaderParse);
-                return reset();
-            }
+            PROF_stop(HttpHeaderParse);
+            clean();
+            return 0;
         }
 
         if (e->id == HDR_OTHER && stringHasWhitespace(e->name.termedBuf())) {
@@ -743,14 +717,29 @@ HttpHeader::parse(const char *header_sta
         addEntry(e);
     }
 
+    if (clen.headerWideProblem) {
+        debugs(55, warnOnError, "WARNING: " << clen.headerWideProblem <<
+               " Content-Length field values in" <<
+               Raw("header", header_start, header_end - header_start));
+    }
+
     if (chunked()) {
         // RFC 2616 section 4.4: ignore Content-Length with Transfer-Encoding
         delById(HDR_CONTENT_LENGTH);
         // RFC 7230 section 3.3.3 #4: ignore Content-Length conflicts with Transfer-Encoding
         conflictingContentLength_ = false;
-    } else if (conflictingContentLength_) {
-        // ensure our callers do not see the conflicting Content-Length value
+    } else if (clen.sawBad) {
+        // ensure our callers do not accidentally see bad Content-Length values
         delById(HDR_CONTENT_LENGTH);
+        conflictingContentLength_ = true; // TODO: Rename to badContentLength_.
+    } else if (clen.needsSanitizing) {
+        // RFC 7230 section 3.3.2: MUST either reject or ... [sanitize];
+        // ensure our callers see a clean Content-Length value or none at all
+        delById(HDR_CONTENT_LENGTH);
+        if (clen.sawGood) {
+            putInt64(HDR_CONTENT_LENGTH, clen.value);
+            debugs(55, 5, "sanitized Content-Length to be " << clen.value);
+        }
     }
 
     PROF_stop(HttpHeaderParse);
Index: squid-3.5.21/src/http/Parser.cc
===================================================================
--- /dev/null
+++ squid-3.5.21/src/http/Parser.cc
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+#include "Debug.h"
+#include "http/Parser.h"
+#include "base/CharacterSet.h"
+#include "mime_header.h"
+#include "SquidConfig.h"
+
+/// characters HTTP permits tolerant parsers to accept as delimiters
+static const CharacterSet &
+RelaxedDelimiterCharacters()
+{
+    // RFC 7230 section 3.5
+    // tolerant parser MAY accept any of SP, HTAB, VT (%x0B), FF (%x0C),
+    // or bare CR as whitespace between request-line fields
+    static const CharacterSet RelaxedDels =
+        (CharacterSet::SP +
+         CharacterSet::HTAB +
+         CharacterSet("VT,FF","\x0B\x0C") +
+         CharacterSet::CR).rename("relaxed-WSP");
+
+    return RelaxedDels;
+}
+
+const CharacterSet &
+Http::One::Parser::WhitespaceCharacters()
+{
+    return Config.onoff.relaxed_header_parser ?
+           RelaxedDelimiterCharacters() : CharacterSet::WSP;
+}
+
+const CharacterSet &
+Http::One::Parser::DelimiterCharacters()
+{
+    return Config.onoff.relaxed_header_parser ?
+           RelaxedDelimiterCharacters() : CharacterSet::SP;
+}
+
