]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
RFC 7230 compliant request-line parser based on Tokenizer API
authorAmos Jeffries <squid3@treenet.co.nz>
Thu, 22 Jan 2015 12:54:08 +0000 (04:54 -0800)
committerAmos Jeffries <squid3@treenet.co.nz>
Thu, 22 Jan 2015 12:54:08 +0000 (04:54 -0800)
Refactor the request-line parser using a Tokenizer.

RFC 7230 requirements provide field terminator/delimiter limitations and
character sets for token validation. Also provides definitions of
boundaries for relaxed/tollerant parsing without needing Squid-specific
RFC violations.

This implementation is slightly stricter regarding whitespace in URLs
than previous implementation. It obeys a SHOULD requirement in RFC 7230
regarding responding with 400 status to those broken request messages.

src/http/one/RequestParser.cc
src/http/one/RequestParser.h
src/tests/testHttp1Parser.cc
src/tests/testHttp1Parser.h

index 4d55204d7ae109fc3d2acd39d52b4321567692e9..321c49b67c48b2ab1c7eca483fcc03e3d839e4f5 100644 (file)
 #include "http/one/RequestParser.h"
 #include "http/ProtocolVersion.h"
 #include "mime_header.h"
+#include "parser/Tokenizer.h"
 #include "profiler/Profiler.h"
 #include "SquidConfig.h"
 
 Http::One::RequestParser::RequestParser() :
     Parser(),
-    request_parse_status(Http::scNone)
+    request_parse_status(Http::scNone),
+    firstLineGarbage_(0)
+{}
+
+Http1::Parser::size_type
+Http::One::RequestParser::firstLineSize() const
 {
-    req.start = req.end = -1;
-    req.m_start = req.m_end = -1;
-    req.u_start = req.u_end = -1;
-    req.v_start = req.v_end = -1;
+    // RFC 7230 section 2.6
+    /* method SP request-target SP "HTTP/" DIGIT "." DIGIT CRLF */
+    return method_.image().length() + uri_.length() + 12;
 }
 
 /**
@@ -51,240 +56,303 @@ Http::One::RequestParser::skipGarbageLines()
             buf_.consume(1);
         }
     }
+}
 
-    /* XXX: this is a Squid-specific tolerance
-     * it appears never to have been relevant outside out unit-tests
-     * because the ConnStateData parser loop starts with consumeWhitespace()
-     * which absorbs any SP HTAB VTAB CR LF characters.
-     * But unit-tests called the HttpParser method directly without that pruning.
-     */
-#if USE_HTTP_VIOLATIONS
-    if (Config.onoff.relaxed_header_parser) {
-        if (Config.onoff.relaxed_header_parser < 0 && buf_[0] == ' ')
-            debugs(74, DBG_IMPORTANT, "WARNING: Invalid HTTP Request: " <<
-                   "Whitespace bytes received ahead of method. " <<
-                   "Ignored due to relaxed_header_parser.");
-        // Be tolerant of prefix spaces (other bytes are valid method values)
-        while (!buf_.isEmpty() && buf_[0] == ' ') {
-            buf_.consume(1);
-        }
-    }
-#endif
+/// detect and skip the CRLF or LF line terminator
+/// consume from the tokenizer and return true only if found
+bool
+Http::One::RequestParser::skipLineTerminator(::Parser::Tokenizer &tok) const
+{
+    static const SBuf crlf("\r\n");
+    if (tok.skip(crlf))
+        return true;
+
+    if (Config.onoff.relaxed_header_parser && tok.skipOne(CharacterSet::LF))
+        return true;
+
+    return false;
 }
 
 /**
- * Attempt to parse the first line of a new request message.
+ * Attempt to parse the method field out of an HTTP message request-line.
  *
  * Governed by:
  *  RFC 1945 section 5.1
- *  RFC 7230 section 3.1 and 3.5
+ *  RFC 7230 section 2.6, 3.1 and 3.5
  *
- * Parsing state is stored between calls. However the current implementation
- * begins parsing from scratch on every call.
- * The return value tells you whether the parsing state fields are valid or not.
+ * Parsing state is stored between calls. The current implementation uses
+ * checkpoints after each successful request-line field.
+ * The return value tells you whether the parsing is completed or not.
  *
  * \retval -1  an error occurred. request_parse_status indicates HTTP status result.
- * \retval  1  successful parse. member fields contain the request-line items
+ * \retval  1  successful parse. method_ is filled and buffer consumed including first delimiter.
  * \retval  0  more data is needed to complete the parse
  */
 int
-Http::One::RequestParser::parseRequestFirstLine()
+Http::One::RequestParser::parseMethodField(::Parser::Tokenizer &tok, const CharacterSet &WspDelim)
 {
-    int second_word = -1; // track the suspected URI start
-    int first_whitespace = -1, last_whitespace = -1; // track the first and last SP byte
-    int line_end = -1; // tracks the last byte BEFORE terminal \r\n or \n sequence
+    // scan for up to 16 valid method characters.
+    static const size_t maxMethodLength = 16;
 
-    debugs(74, 5, "parsing possible request: buf.length=" << buf_.length());
-    debugs(74, DBG_DATA, buf_);
+    SBuf methodFound;
 
-    // Single-pass parse: (provided we have the whole line anyways)
+    // method field is a sequence of TCHAR.
+    // NP: prefix-with-limit returns true if it finds ANY valid chars
+    if (!tok.prefix(methodFound, CharacterSet::TCHAR, maxMethodLength)) {
+        // missing/invalid 'method'.
+        request_parse_status = Http::scBadRequest;
+        debugs(33, 5, "invalid request-line. missing method");
+        return -1;
+    }
 
-    req.start = 0;
-    req.end = -1;
-    for (SBuf::size_type i = 0; i < buf_.length(); ++i) {
-        // track first and last whitespace (SP only)
-        if (buf_[i] == ' ') {
-            last_whitespace = i;
-            if (first_whitespace < req.start)
-                first_whitespace = i;
-        }
+    // we may be at the end if we found exactly maxMethodLength bytes
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data to find method");
+        return 0;
+    }
 
-        // track next non-SP/non-HT byte after first_whitespace
-        if (second_word < first_whitespace && buf_[i] != ' ' && buf_[i] != '\t') {
-            second_word = i;
+    // ... followed by at least one whitespace character.
+    if (!tok.skipOne(WspDelim)) {
+        // non-delimiter found after accepted method bytes means ...
+        if (methodFound.length() == maxMethodLength) {
+            // method longer than acceptible.
+            // RFC 7230 section 3.1.1 mandatory (SHOULD) 501 response
+            request_parse_status = Http::scNotImplemented;
+            debugs(33, 5, "invalid request-line. method too long");
+        } else {
+            // invalid character in the URL
+            // RFC 7230 section 3.1.1 required (SHOULD) 400 response
+            request_parse_status = Http::scBadRequest;
+            debugs(33, 5, "invalid request-line. missing method delimiter");
         }
+        return -1;
+    }
+    method_ = HttpRequestMethod(methodFound);
+    buf_ = tok.remaining(); // incremental parse checkpoint
+    return 1;
+}
 
-        // locate line terminator
-        if (buf_[i] == '\n') {
-            req.end = i;
-            line_end = i - 1;
-            break;
-        }
-        if (i < buf_.length() - 1 && buf_[i] == '\r') {
-            if (Config.onoff.relaxed_header_parser) {
-                if (Config.onoff.relaxed_header_parser < 0 && buf_[i + 1] == '\r')
-                    debugs(74, DBG_IMPORTANT, "WARNING: Invalid HTTP Request: " <<
-                           "Series of carriage-return bytes received prior to line terminator. " <<
-                           "Ignored due to relaxed_header_parser.");
-
-                // Be tolerant of invalid multiple \r prior to terminal \n
-                if (buf_[i + 1] == '\n' || buf_[i + 1] == '\r')
-                    line_end = i - 1;
-                while (i < buf_.length() - 1 && buf_[i + 1] == '\r')
-                    ++i;
-
-                if (buf_[i + 1] == '\n') {
-                    req.end = i + 1;
-                    break;
-                }
-            } else {
-                if (buf_[i + 1] == '\n') {
-                    req.end = i + 1;
-                    line_end = i - 1;
-                    break;
-                }
-            }
+int
+Http::One::RequestParser::parseUriField(::Parser::Tokenizer &tok, const CharacterSet &WspDelim)
+{
+    // URI field is a sequence of ... what? segments all have different valid charset
+    // go with non-whitespace non-binary characters for now
+    static CharacterSet UriChars("URI-Chars","");
+    if (!UriChars['a']) { // if it needs initializing...
+        /* RFC 3986 section 2:
+         * "
+         *   A URI is composed from a limited set of characters consisting of
+         *   digits, letters, and a few graphic symbols.
+         * "
+         */
+        // RFC 3986 section 2.1 - percent encoding "%" HEXDIG
+        UriChars.add('%');
+        UriChars += CharacterSet::HEXDIG;
+        // RFC 3986 section 2.2 - reserved characters
+        UriChars += CharacterSet("gen-delims", ":/?#[]@");
+        UriChars += CharacterSet("sub-delims", "!$&'()*+,;=");
+        // RFC 3986 section 2.3 - unreserved characters
+        UriChars += CharacterSet::ALPHA;
+        UriChars += CharacterSet::DIGIT;
+        UriChars += CharacterSet("unreserved", "-._~");
+    }
 
-            // RFC 7230 section 3.1.1 does not prohibit embeded CR like RFC 2616 used to.
-            // However it does explicitly state an exact syntax which omits un-encoded CR
-            // and defines 400 (Bad Request) as the required action when
-            // handed an invalid request-line.
-            request_parse_status = Http::scBadRequest;
-            return -1;
-        }
+    /* Arbitrary 64KB URI upper length limit.
+     *
+     * Not quite as arbitrary as it seems though. Old SquidString objects
+     * cannot store strings larger than 64KB, so we must limit until they
+     * have all been replaced with SBuf.
+     *
+     * Not that it matters but RFC 7230 section 3.1.1 requires (RECOMMENDED)
+     * at least 8000 octets for the whole line, including method and version.
+     */
+    const size_t maxUriLength = min(static_cast<size_t>(Config.maxRequestHeaderSize) - firstLineSize(),
+                                    static_cast<size_t>((64*1024)-1));
+
+    SBuf uriFound;
+    // NP: prefix-with-limit returns true if it finds ANY valid chars
+    if (!tok.prefix(uriFound, UriChars, maxUriLength)) {
+        // else did not find any valid TCHAR
+        debugs(33, 5, "invalid request-line. missing URL");
+        request_parse_status = Http::scBadRequest;
+        return -1;
+    }
 
-        // We are expecting printable ascii characters for method/first word
-        if (first_whitespace < 0 && (!xisascii(buf_[i]) || !xisprint(buf_[i]))) {
-            request_parse_status = Http::scBadRequest;
-            return -1;
-        }
+    // we may be at the end if we found exactly maxUriLength bytes
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data to find URI");
+        return 0;
     }
 
-    if (req.end == -1) {
-        // DoS protection against long first-line
-        if ((size_t)buf_.length() >= Config.maxRequestHeaderSize) {
-            debugs(33, 5, "Too large request-line");
-            // RFC 7230 section 3.1.1 mandatory 414 response if URL longer than acceptible.
+    // RFC 1945 - for GET the line terminator may follow URL instead of a delimiter
+    if (method_ == Http::METHOD_GET && skipLineTerminator(tok)) {
+        debugs(33, 5, "HTTP/0.9 syntax request-line detected");
+        msgProtocol_ = Http::ProtocolVersion(0,9);
+        uri_ = uriFound;
+        request_parse_status = Http::scOkay;
+        buf_ = tok.remaining(); // incremental parse checkpoint
+        return 1;
+    }
+
+    // ... followed by at least one whitespace character.
+    if (!tok.skipOne(WspDelim)) {
+        // non-delimiter found after accepted URL bytes means ...
+        if (uriFound.length() == maxUriLength) {
+            // URL longer than acceptible.
+            // RFC 7230 section 3.1.1 mandatory (MUST) 414 response
             request_parse_status = Http::scUriTooLong;
+            debugs(33, 5, "invalid request-line. URI longer than " << maxUriLength << " bytes");
+            return -1;
+        } else {
+            // invalid non-delimiter character ended the URL
+            // RFC 7230 section 3.1.1 required (SHOULD) 400 response
+            request_parse_status = Http::scBadRequest;
+            debugs(33, 5, "invalid request-line. missing URI delimiter");
             return -1;
         }
+    }
+    uri_ = uriFound;
+    buf_ = tok.remaining(); // incremental parse checkpoint
+    return 1;
+}
 
-        debugs(74, 5, "Parser: retval 0: from " << req.start <<
-               "->" << req.end << ": needs more data to complete first line.");
+int
+Http::One::RequestParser::parseHttpVersionField(::Parser::Tokenizer &tok)
+{
+    // partial match of HTTP/1 magic prefix
+    if (tok.remaining().length() < Http1magic.length() && Http1magic.startsWith(tok.remaining())) {
+        debugs(74, 5, "Parser needs more data to find version");
         return 0;
     }
 
-    // NP: we have now seen EOL, more-data (0) cannot occur.
-    //     From here on any failure is -1, success is 1
-
-    // Input Validation:
-
-    // DoS protection against long first-line
-    if ((size_t)(req.end-req.start) >= Config.maxRequestHeaderSize) {
-        debugs(33, 5, "Too large request-line");
-        request_parse_status = Http::scUriTooLong;
+    if (!tok.skip(Http1magic)) {
+        debugs(74, 5, "invalid request-line. not HTTP/1 protocol");
+        request_parse_status = Http::scHttpVersionNotSupported;
         return -1;
     }
 
-    // Process what we now know about the line structure into field offsets
-    // generating HTTP status for any aborts as we go.
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data to find version");
+        return 0;
+    }
 
-    // First non-whitespace = beginning of method
-    if (req.start > line_end) {
-        request_parse_status = Http::scBadRequest;
+    // get the version minor DIGIT
+    SBuf digit;
+    if (!tok.prefix(digit, CharacterSet::DIGIT, 1)) {
+        // non-DIGIT. invalid version number.
+        request_parse_status = Http::scHttpVersionNotSupported;
+        debugs(33, 5, "invalid request-line. non-numeric or too-large HTTP minor version");
         return -1;
     }
-    req.m_start = req.start;
 
-    // First whitespace = end of method
-    if (first_whitespace > line_end || first_whitespace < req.start) {
-        request_parse_status = Http::scBadRequest; // no method
-        return -1;
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data to find version");
+        return 0;
     }
-    req.m_end = first_whitespace - 1;
-    if (req.m_end < req.m_start) {
-        request_parse_status = Http::scBadRequest; // missing URI?
+
+    // version is always followed by the terminator
+    if (!skipLineTerminator(tok)) {
+        if (tok.skipOne(CharacterSet::CR) && tok.atEnd()) {
+            debugs(74, 5, "Parser needs more data to find version");
+            return 0;
+        }
+        request_parse_status = Http::scHttpVersionNotSupported;
+        debugs(33, 5, "invalid request-line. garabge before line terminator");
         return -1;
     }
 
-    /* Set method_ */
-    const SBuf tmp = buf_.substr(req.m_start, req.m_end - req.m_start + 1);
-    method_ = HttpRequestMethod(tmp);
+    // found version fully AND terminator
+    msgProtocol_ = Http::ProtocolVersion(1, (*digit.rawContent() - '0'));
+    request_parse_status = Http::scOkay;
+    buf_ = tok.remaining(); // incremental parse checkpoint
+    return 1;
+}
 
-    // First non-whitespace after first SP = beginning of URL+Version
-    if (second_word > line_end || second_word < req.start) {
-        request_parse_status = Http::scBadRequest; // missing URI
-        return -1;
-    }
-    req.u_start = second_word;
+/**
+ * Attempt to parse the first line of a new request message.
+ *
+ * Governed by:
+ *  RFC 1945 section 5.1
+ *  RFC 7230 section 2.6, 3.1 and 3.5
+ *
+ * Parsing state is stored between calls. The current implementation uses
+ * checkpoints after each successful request-line field.
+ * The return value tells you whether the parsing is completed or not.
+ *
+ * \retval -1  an error occurred. request_parse_status indicates HTTP status result.
+ * \retval  1  successful parse. member fields contain the request-line items
+ * \retval  0  more data is needed to complete the parse
+ */
+int
+Http::One::RequestParser::parseRequestFirstLine()
+{
+    ::Parser::Tokenizer tok(buf_);
 
-    // RFC 1945: SP and version following URI are optional, marking version 0.9
-    // we identify this by the last whitespace being earlier than URI start
-    if (last_whitespace < second_word && last_whitespace >= req.start) {
-        msgProtocol_ = Http::ProtocolVersion(0,9);
-        req.u_end = line_end;
-        uri_ = buf_.substr(req.u_start, req.u_end - req.u_start + 1);
-        request_parse_status = Http::scOkay; // HTTP/0.9
-        return 1;
-    } else {
-        // otherwise last whitespace is somewhere after end of URI.
-        req.u_end = last_whitespace;
-        // crop any trailing whitespace in the area we think of as URI
-        for (; req.u_end >= req.u_start && xisspace(buf_[req.u_end]); --req.u_end);
-    }
-    if (req.u_end < req.u_start) {
-        request_parse_status = Http::scBadRequest; // missing URI
-        return -1;
-    }
-    uri_ = buf_.substr(req.u_start, req.u_end - req.u_start + 1);
+    debugs(74, 5, "parsing possible request: buf.length=" << buf_.length());
+    debugs(74, DBG_DATA, buf_);
 
-    // Last whitespace SP = before start of protocol/version
-    if (last_whitespace >= line_end) {
-        request_parse_status = Http::scBadRequest; // missing version
-        return -1;
+    CharacterSet WspDelim = CharacterSet::SP; // strict parse only accepts SP
+
+    if (Config.onoff.relaxed_header_parser) {
+        // 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
+        WspDelim += CharacterSet::HTAB
+                  + CharacterSet("VT,FF","\x0B\x0C")
+                  + CharacterSet::CR;
     }
-    req.v_start = last_whitespace + 1;
-    req.v_end = line_end;
 
-    /* RFC 7230 section 2.6 : handle unsupported HTTP major versions cleanly. */
-    if ((req.v_end - req.v_start +1) < (int)Http1magic.length() || !buf_.substr(req.v_start, SBuf::npos).startsWith(Http1magic)) {
-        // non-HTTP/1 protocols not supported / implemented.
-        request_parse_status = Http::scHttpVersionNotSupported;
-        return -1;
+    // only search for method if we have not yet found one
+    if (method_ == Http::METHOD_NONE) {
+        const int res = parseMethodField(tok, WspDelim);
+        if (res < 1)
+            return res;
+        // else keep going...
     }
-    // NP: magic octets include the protocol name and major version DIGIT.
-    msgProtocol_.protocol = AnyP::PROTO_HTTP;
-    msgProtocol_.major = 1;
 
-    int i = req.v_start + Http1magic.length() -1;
+    // tolerant parser allows multiple whitespace characters between fields
+    if (Config.onoff.relaxed_header_parser) {
+        const size_t garbage = tok.skipAll(WspDelim);
+        if (garbage > 0) {
+            firstLineGarbage_ += garbage;
+            buf_ = tok.remaining(); // re-checkpoint after garbage
+        }
+    }
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data");
+        return 0;
+    }
 
-    // catch missing minor part
-    if (++i > line_end) {
-        request_parse_status = Http::scHttpVersionNotSupported;
-        return -1;
+    // only search for request-target (URL) if we have not yet found one
+    if (uri_.isEmpty()) {
+        const int res = parseUriField(tok, WspDelim);
+        if (res < 1 || msgProtocol_.protocol == AnyP::PROTO_HTTP)
+            return res;
+        // else keep going...
     }
-    /* next should be one or more digits */
-    if (!isdigit(buf_[i])) {
-        request_parse_status = Http::scHttpVersionNotSupported;
-        return -1;
+
+    // tolerant parser allows multiple whitespace characters between fields
+    if (Config.onoff.relaxed_header_parser) {
+        const size_t garbage = tok.skipAll(WspDelim);
+        if (garbage > 0) {
+            firstLineGarbage_ += garbage;
+            buf_ = tok.remaining(); // re-checkpoint after garbage
+        }
     }
-    int min = 0;
-    for (; i <= line_end && (isdigit(buf_[i])) && min < 65536; ++i) {
-        min = min * 10;
-        min = min + (buf_[i]) - '0';
+    if (tok.atEnd()) {
+        debugs(74, 5, "Parser needs more data");
+        return 0;
     }
-    // catch too-big values or trailing garbage
-    if (min >= 65536 || i < line_end) {
-        request_parse_status = Http::scHttpVersionNotSupported;
-        return -1;
+
+    // HTTP/1 version suffix (protocol magic) followed by CR*LF
+    if (msgProtocol_.protocol == AnyP::PROTO_NONE) {
+        return parseHttpVersionField(tok);
     }
-    msgProtocol_.minor = min;
 
-    /*
-     * Rightio - we have all the schtuff. Return true; we've got enough.
-     */
-    request_parse_status = Http::scOkay;
-    return 1;
+    // If we got here this method has been called too many times
+    request_parse_status = Http::scInternalServerError;
+    debugs(33, 5, "ERROR: Parser already processed request-line");
+    return -1;
 }
 
 bool
@@ -311,15 +379,13 @@ Http::One::RequestParser::parse(const SBuf &aBuf)
 
         // first-line (or a look-alike) found successfully.
         if (retcode > 0) {
-            buf_.consume(firstLineSize()); // first line bytes including CRLF terminator are now done.
             parsingStage_ = HTTP_PARSE_MIME;
         }
 
-        debugs(74, 5, "request-line: retval " << retcode << ": from " << req.start << "->" << req.end <<
-               " line={" << aBuf.length() << ", data='" << aBuf << "'}");
-        debugs(74, 5, "request-line: method " << req.m_start << "->" << req.m_end << " (" << method_ << ")");
-        debugs(74, 5, "request-line: url " << req.u_start << "->" << req.u_end << " (" << uri_ << ")");
-        debugs(74, 5, "request-line: proto " << req.v_start << "->" << req.v_end << " (" << msgProtocol_ << ")");
+        debugs(74, 5, "request-line: retval " << retcode << ": line={" << aBuf.length() << ", data='" << aBuf << "'}");
+        debugs(74, 5, "request-line: method: " << method_);
+        debugs(74, 5, "request-line: url: " << uri_);
+        debugs(74, 5, "request-line: proto: " << msgProtocol_);
         debugs(74, 5, "Parser: bytes processed=" << (aBuf.length()-buf_.length()));
         PROF_stop(HttpParserParseReqLine);
 
index 87b27d7da50420656ab9ad82be43c9d511dcf43a..236cd1c51c1e0cf2391b41eb3f01f2566cd2e6d7 100644 (file)
 #include "http/RequestMethod.h"
 #include "http/StatusCode.h"
 
+namespace Parser {
+class Tokenizer;
+}
+
 namespace Http {
 namespace One {
 
@@ -32,7 +36,7 @@ public:
 
     /* Http::One::Parser API */
     virtual void clear() {*this = RequestParser();}
-    virtual Http1::Parser::size_type firstLineSize() const {return req.end - req.start + 1;}
+    virtual Http1::Parser::size_type firstLineSize() const;
     virtual bool parse(const SBuf &aBuf);
 
     /// the HTTP method if this is a request message
@@ -50,21 +54,20 @@ public:
 private:
     void skipGarbageLines();
     int parseRequestFirstLine();
-
-    /// Offsets for pieces of the (HTTP request) Request-Line as per RFC 7230 section 3.1.1.
-    /// only valid before and during parse stage HTTP_PARSE_FIRST
-    struct request_offsets {
-        int start, end;
-        int m_start, m_end; // method
-        int u_start, u_end; // url
-        int v_start, v_end; // version (full text)
-    } req;
+    int parseMethodField(::Parser::Tokenizer &, const CharacterSet &);
+    int parseUriField(::Parser::Tokenizer &, const CharacterSet &);
+    int parseHttpVersionField(::Parser::Tokenizer &);
+    bool skipLineTerminator(::Parser::Tokenizer &) const;
 
     /// what request method has been found on the first line
     HttpRequestMethod method_;
 
     /// raw copy of the original client reqeust-line URI field
     SBuf uri_;
+
+    /// amount of garbage bytes tolerantly skipped inside the request-line
+    /// may be -1 if sender only omitted CR on terminator
+    int64_t firstLineGarbage_;
 };
 
 } // namespace One
index e94429f8ac87fd32f554ccc4f3815b03abf82a90..0fda570cef95ff0265d4f4a4501a96cd2728ddc5 100644 (file)
@@ -13,6 +13,7 @@
 #define private public
 #define protected public
 
+#include "Debug.h"
 #include "http/one/RequestParser.h"
 #include "http/RequestMethod.h"
 #include "MemBuf.h"
@@ -46,17 +47,9 @@ struct resultSet {
     bool needsMore;
     Http1::ParseState parserState;
     Http::StatusCode status;
-    int msgStart;
-    int msgEnd;
     SBuf::size_type suffixSz;
-    int methodStart;
-    int methodEnd;
     HttpRequestMethod method;
-    int uriStart;
-    int uriEnd;
     const char *uri;
-    int versionStart;
-    int versionEnd;
     AnyP::ProtocolVersion version;
 };
 
@@ -67,24 +60,21 @@ testResults(int line, const SBuf &input, Http1::RequestParser &output, struct re
     printf("TEST @%d, in=%u: " SQUIDSBUFPH "\n", line, input.length(), SQUIDSBUFPRINT(input));
 #endif
 
+    // runs the parse
     CPPUNIT_ASSERT_EQUAL(expect.parsed, output.parse(input));
-    CPPUNIT_ASSERT_EQUAL(expect.needsMore, output.needsMoreData());
-    if (output.needsMoreData())
-        CPPUNIT_ASSERT_EQUAL(expect.parserState, output.parsingStage_);
-    CPPUNIT_ASSERT_EQUAL(expect.status, output.request_parse_status);
-    CPPUNIT_ASSERT_EQUAL(expect.msgStart, output.req.start);
-    CPPUNIT_ASSERT_EQUAL(expect.msgEnd, output.req.end);
-    CPPUNIT_ASSERT_EQUAL(expect.suffixSz, output.buf_.length());
-    CPPUNIT_ASSERT_EQUAL(expect.methodStart, output.req.m_start);
-    CPPUNIT_ASSERT_EQUAL(expect.methodEnd, output.req.m_end);
+
+    // check easily visible field outputs
     CPPUNIT_ASSERT_EQUAL(expect.method, output.method_);
-    CPPUNIT_ASSERT_EQUAL(expect.uriStart, output.req.u_start);
-    CPPUNIT_ASSERT_EQUAL(expect.uriEnd, output.req.u_end);
     if (expect.uri != NULL)
         CPPUNIT_ASSERT_EQUAL(0, output.uri_.cmp(expect.uri));
-    CPPUNIT_ASSERT_EQUAL(expect.versionStart, output.req.v_start);
-    CPPUNIT_ASSERT_EQUAL(expect.versionEnd, output.req.v_end);
     CPPUNIT_ASSERT_EQUAL(expect.version, output.msgProtocol_);
+    CPPUNIT_ASSERT_EQUAL(expect.status, output.request_parse_status);
+
+    // check more obscure states
+    CPPUNIT_ASSERT_EQUAL(expect.needsMore, output.needsMoreData());
+    if (output.needsMoreData())
+        CPPUNIT_ASSERT_EQUAL(expect.parserState, output.parsingStage_);
+    CPPUNIT_ASSERT_EQUAL(expect.suffixSz, output.buf_.length());
 }
 #endif /* __cplusplus >= 200103L */
 
@@ -96,18 +86,10 @@ testHttp1Parser::testParserConstruct()
         Http1::RequestParser output;
         CPPUNIT_ASSERT_EQUAL(true, output.needsMoreData());
         CPPUNIT_ASSERT_EQUAL(Http1::HTTP_PARSE_NONE, output.parsingStage_);
-        CPPUNIT_ASSERT_EQUAL(Http::scNone, output.request_parse_status); // XXX: clear() not being called.
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.start);
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.end);
+        CPPUNIT_ASSERT_EQUAL(Http::scNone, output.request_parse_status);
         CPPUNIT_ASSERT(output.buf_.isEmpty());
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.m_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.m_end);
         CPPUNIT_ASSERT_EQUAL(HttpRequestMethod(Http::METHOD_NONE), output.method_);
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.u_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.u_end);
         CPPUNIT_ASSERT(output.uri_.isEmpty());
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.v_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output.req.v_end);
         CPPUNIT_ASSERT_EQUAL(AnyP::ProtocolVersion(), output.msgProtocol_);
     }
 
@@ -117,17 +99,9 @@ testHttp1Parser::testParserConstruct()
         CPPUNIT_ASSERT_EQUAL(true, output->needsMoreData());
         CPPUNIT_ASSERT_EQUAL(Http1::HTTP_PARSE_NONE, output->parsingStage_);
         CPPUNIT_ASSERT_EQUAL(Http::scNone, output->request_parse_status);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.start);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.end);
         CPPUNIT_ASSERT(output->buf_.isEmpty());
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.m_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.m_end);
         CPPUNIT_ASSERT_EQUAL(HttpRequestMethod(Http::METHOD_NONE), output->method_);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.u_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.u_end);
         CPPUNIT_ASSERT(output->uri_.isEmpty());
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.v_start);
-        CPPUNIT_ASSERT_EQUAL(-1, output->req.v_end);
         CPPUNIT_ASSERT_EQUAL(AnyP::ProtocolVersion(), output->msgProtocol_);
         delete output;
     }
@@ -144,7 +118,7 @@ testHttp1Parser::testParseRequestLineProtocols()
     Http1::RequestParser output;
 
     // TEST: Do we comply with RFC 1945 section 5.1 ?
-    // TEST: Do we comply with RFC 2616 section 5.1 ?
+    // TEST: Do we comply with RFC 7230 sections 2.6, 3.1.1 and 3.5 ?
 
     // RFC 1945 : HTTP/0.9 simple-request
     {
@@ -154,17 +128,9 @@ testHttp1Parser::testParseRequestLineProtocols()
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
         };
         output.clear();
@@ -173,33 +139,24 @@ testHttp1Parser::testParseRequestLineProtocols()
     }
 
     // RFC 1945 : invalid HTTP/0.9 simple-request (only GET is valid)
-#if WHEN_RFC_COMPLIANT
     {
-        input.append("POST /\r\n", 7);
+        input.append("POST /\r\n", 8);
         struct resultSet expect = {
-            .parsed = true,
+            .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
-            .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 3,
+            .status = Http::scBadRequest,
+            .suffixSz = 3,
             .method = HttpRequestMethod(Http::METHOD_POST),
-            .uriStart = 5,
-            .uriEnd = 5,
-            .uri = "/",
-            .versionStart = -1,
-            .versionEnd = -1,
+            .uri = NULL,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
         input.clear();
     }
-#endif
-    // RFC 1945 and 2616 : HTTP/1.0 request
+
+    // RFC 1945 and 7230 : HTTP/1.0 request
     {
         input.append("GET / HTTP/1.0\r\n", 16);
         struct resultSet expect = {
@@ -207,17 +164,9 @@ testHttp1Parser::testParseRequestLineProtocols()
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,0)
         };
         output.clear();
@@ -225,7 +174,7 @@ testHttp1Parser::testParseRequestLineProtocols()
         input.clear();
     }
 
-    // RFC 2616 : HTTP/1.1 request
+    // RFC 7230 : HTTP/1.1 request
     {
         input.append("GET / HTTP/1.1\r\n", 16);
         struct resultSet expect = {
@@ -233,17 +182,9 @@ testHttp1Parser::testParseRequestLineProtocols()
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
@@ -251,7 +192,7 @@ testHttp1Parser::testParseRequestLineProtocols()
         input.clear();
     }
 
-    // RFC 2616 : future version full-request
+    // RFC 7230 : future version full-request
     {
         input.append("GET / HTTP/1.2\r\n", 16);
         struct resultSet expect = {
@@ -259,17 +200,9 @@ testHttp1Parser::testParseRequestLineProtocols()
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,2)
         };
         output.clear();
@@ -279,49 +212,48 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // RFC 7230 : future versions do not use request-line syntax
     {
+        input.append("GET / HTTP/2.0\r\n", 16);
+        struct resultSet expectA = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_MIME,
+            .status = Http::scHttpVersionNotSupported,
+            .suffixSz = input.length()-6,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = "/",
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expectA);
+        input.clear();
+
         input.append("GET / HTTP/10.12\r\n", 18);
-        struct resultSet expect = {
+        struct resultSet expectB = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 15,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
-        testResults(__LINE__, input, output, expect);
+        testResults(__LINE__, input, output, expectB);
         input.clear();
     }
 
     // unknown non-HTTP protocol names
     {
-        input.append("GET / FOO/1.0\n", 14);
+        input.append("GET / FOO/1.0\r\n", 15);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 12,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -329,25 +261,17 @@ testHttp1Parser::testParseRequestLineProtocols()
         input.clear();
     }
 
-    // no version
+    // no version digits
     {
-        input.append("GET / HTTP/\n", 12);
+        input.append("GET / HTTP/\r\n", 13);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 10,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -357,23 +281,15 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // no major version
     {
-        input.append("GET / HTTP/.1\n", 14);
+        input.append("GET / HTTP/.1\r\n", 15);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 12,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -383,23 +299,15 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // no version dot
     {
-        input.append("GET / HTTP/11\n", 14);
+        input.append("GET / HTTP/11\r\n", 15);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 12,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -409,23 +317,15 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // negative major version (bug 3062)
     {
-        input.append("GET / HTTP/-999999.1\n", 21);
+        input.append("GET / HTTP/-999999.1\r\n", 22);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 19,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -435,24 +335,16 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // no minor version
     {
-        input.append("GET / HTTP/1.\n", 14);
+        input.append("GET / HTTP/1.\r\n", 15);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 12,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,0)
+            .version = AnyP::ProtocolVersion()
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
@@ -461,24 +353,16 @@ testHttp1Parser::testParseRequestLineProtocols()
 
     // negative major version (bug 3062 corollary)
     {
-        input.append("GET / HTTP/1.-999999\n", 21);
+        input.append("GET / HTTP/1.-999999\r\n", 22);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scHttpVersionNotSupported,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 19,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,0)
+            .version = AnyP::ProtocolVersion()
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
@@ -498,80 +382,89 @@ testHttp1Parser::testParseRequestLineStrange()
     // space padded URL
     {
         input.append("GET  /     HTTP/1.1\r\n", 21);
+        // when being tolerant extra (sequential) SP delimiters are acceptable
+        Config.onoff.relaxed_header_parser = 1;
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 5,
-            .uriEnd = 5,
             .uri = "/",
-            .versionStart = 11,
-            .versionEnd = 18,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
+
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = input.length()-4,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
 
+#if 0 // XXX: RFC compliant parser does not have tolerance for this (yet)
     // whitespace inside URI. (nasty but happens)
-    // XXX: depends on tolerant parser...
     {
-        input.append("GET /fo o/ HTTP/1.1\n", 20);
+        input.append("GET /fo o/ HTTP/1.1\r\n", 21);
+        Config.onoff.relaxed_header_parser = 1;
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 9,
             .uri = "/fo o/",
-            .versionStart = 11,
-            .versionEnd = 18,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
+
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = 0,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
+#endif
 
     // additional data in buffer
     {
-        input.append("GET /     HTTP/1.1\nboo!", 23);
+        input.append("GET / HTTP/1.1\r\nboo!", 20);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-5,
             .suffixSz = 4, // strlen("boo!")
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 10,
-            .versionEnd = 17,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
         input.clear();
+        Config.onoff.relaxed_header_parser = 0;
     }
 }
 
@@ -585,101 +478,67 @@ testHttp1Parser::testParseRequestLineTerminators()
     Http1::RequestParser output;
 
     // alternative EOL sequence: NL-only
+    // RFC 7230 tolerance permits omitted CR
     {
         input.append("GET / HTTP/1.1\n", 15);
+        Config.onoff.relaxed_header_parser = 1;
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
-        input.clear();
-    }
 
-    // alternative EOL sequence: double-NL-only
-    {
-        input.append("GET / HTTP/1.1\n\n", 16);
-        struct resultSet expect = {
-            .parsed = true,
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
+            .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
-            .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-2,
-            .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
+            .status = Http::scHttpVersionNotSupported,
+            .suffixSz = 9,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
+            .version = AnyP::ProtocolVersion()
         };
         output.clear();
-        testResults(__LINE__, input, output, expect);
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
 
-    // alternative EOL sequence: multi-CR-NL
+    // alternative EOL sequence: double-NL-only
+    // RFC 7230 tolerance permits omitted CR
+    // NP: represents a request with no mime headers
     {
-        input.append("GET / HTTP/1.1\r\r\r\n", 18);
-        // Being tolerant we can ignore and elide these apparently benign CR
+        input.append("GET / HTTP/1.1\n\n", 16);
         Config.onoff.relaxed_header_parser = 1;
-        struct resultSet expectRelaxed = {
-            .parsed = false,
-            .needsMore = true,
-            .parserState = Http1::HTTP_PARSE_MIME,
+        struct resultSet expect = {
+            .parsed = true,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
-        testResults(__LINE__, input, output, expectRelaxed);
+        testResults(__LINE__, input, output, expect);
 
-        // strict mode treats these as several bare-CR in the request line which is explicitly invalid.
         Config.onoff.relaxed_header_parser = 0;
         struct resultSet expectStrict = {
             .parsed = false,
             .needsMore = false,
-            .parserState = Http1::HTTP_PARSE_MIME,
-            .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1,
-            .suffixSz = input.length(),
-            .methodStart =-1,
-            .methodEnd = -1,
-            .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
-            .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scHttpVersionNotSupported,
+            .suffixSz = 10,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = "/",
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -689,25 +548,16 @@ testHttp1Parser::testParseRequestLineTerminators()
 
     // space padded version
     {
-        // RFC 1945 and 2616 specify version is followed by CRLF. No intermediary bytes.
-        // NP: the terminal whitespace is a special case: invalid for even HTTP/0.9 with no version tag
-        input.append("GET / HTTP/1.1 \n", 16);
+        // RFC 7230 specifies version is followed by CRLF. No intermediary bytes.
+        input.append("GET / HTTP/1.1 \r\n", 17);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
-            .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = 2,
+            .status = Http::scHttpVersionNotSupported,
+            .suffixSz = input.length()-6,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 13,
-            .uri = "/ HTTP/1.1",
-            .versionStart = -1,
-            .versionEnd = -1,
+            .uri = "/",
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -725,25 +575,35 @@ testHttp1Parser::testParseRequestLineMethods()
     SBuf input;
     Http1::RequestParser output;
 
-    // RFC 2616 : . method
+    // RFC 7230 : dot method
     {
-        input.append(". / HTTP/1.1\n", 13);
+        input.append(". / HTTP/1.1\r\n", 14);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 0,
             .method = HttpRequestMethod(SBuf(".")),
-            .uriStart = 2,
-            .uriEnd = 2,
             .uri = "/",
-            .versionStart = 4,
-            .versionEnd = 11,
+            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expect);
+        input.clear();
+    }
+
+    // RFC 7230 : special TCHAR method chars
+    {
+        input.append("!#$%&'*+-.^_`|~ / HTTP/1.1\r\n", 28);
+        struct resultSet expect = {
+            .parsed = false,
+            .needsMore = true,
+            .parserState = Http1::HTTP_PARSE_MIME,
+            .status = Http::scOkay,
+            .suffixSz = 0,
+            .method = HttpRequestMethod(SBuf("!#$%&'*+-.^_`|~")),
+            .uri = "/",
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
@@ -753,23 +613,15 @@ testHttp1Parser::testParseRequestLineMethods()
 
     // OPTIONS with * URL
     {
-        input.append("OPTIONS * HTTP/1.1\n", 19);
+        input.append("OPTIONS * HTTP/1.1\r\n", 20);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 6,
             .method = HttpRequestMethod(Http::METHOD_OPTIONS),
-            .uriStart = 8,
-            .uriEnd = 8,
             .uri = "*",
-            .versionStart = 10,
-            .versionEnd = 17,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
@@ -779,23 +631,15 @@ testHttp1Parser::testParseRequestLineMethods()
 
     // unknown method
     {
-        input.append("HELLOWORLD / HTTP/1.1\n", 22);
+        input.append("HELLOWORLD / HTTP/1.1\r\n", 23);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 9,
             .method = HttpRequestMethod(SBuf("HELLOWORLD")),
-            .uriStart = 11,
-            .uriEnd = 11,
             .uri = "/",
-            .versionStart = 13,
-            .versionEnd = 20,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
@@ -803,6 +647,24 @@ testHttp1Parser::testParseRequestLineMethods()
         input.clear();
     }
 
+    // too-long method (over 16 bytes)
+    {
+        input.append("HELLOSTRANGEWORLD / HTTP/1.1\r\n", 31);
+        struct resultSet expect = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scNotImplemented,
+            .suffixSz = input.length(),
+            .method = HttpRequestMethod(),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expect);
+        input.clear();
+    }
+
     // method-only
     {
         input.append("A\n", 2);
@@ -811,17 +673,9 @@ testHttp1Parser::testParseRequestLineMethods()
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -836,17 +690,9 @@ testHttp1Parser::testParseRequestLineMethods()
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -854,52 +700,50 @@ testHttp1Parser::testParseRequestLineMethods()
         input.clear();
     }
 
-    // space padded method (in strict mode SP is reserved so invalid as a method byte)
+    // space padded method (SP is reserved so invalid as a method byte)
+    {
+        input.append(" GET / HTTP/1.1\r\n", 17);
+        struct resultSet expect = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = input.length(),
+            .method = HttpRequestMethod(),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expect);
+        input.clear();
+    }
+
+    // RFC 7230 defined tolerance: ignore empty line(s) prefix on messages
     {
-        input.append(" GET / HTTP/1.1\n", 16);
-        // RELAXED mode Squid custom tolerance ignores SP
-#if USE_HTTP_VIOLATIONS
+        input.append("\r\n\r\n\nGET / HTTP/1.1\r\n", 21);
         Config.onoff.relaxed_header_parser = 1;
-        struct resultSet expectRelaxed = {
+        struct resultSet expect = {
             .parsed = false,
             .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 0, // garbage collection consumes the SP
-            .msgEnd = (int)input.length()-2,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
-        testResults(__LINE__, input, output, expectRelaxed);
-#endif
+        testResults(__LINE__, input, output, expect);
 
-        // STRICT mode obeys RFC syntax
         Config.onoff.relaxed_header_parser = 0;
         struct resultSet expectStrict = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -907,57 +751,89 @@ testHttp1Parser::testParseRequestLineMethods()
         input.clear();
     }
 
-    // RFC 2616 defined tolerance: ignore empty line(s) prefix on messages
-#if WHEN_RFC_COMPLIANT
+    // forbidden character in method
     {
-        input.append("\r\n\r\n\nGET / HTTP/1.1\r\n", 21);
+        input.append("\tGET / HTTP/1.1\r\n", 17);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = input.length(),
+            .method = HttpRequestMethod(),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expect);
+        input.clear();
+    }
+
+    // CR in method delimiters
+    {
+        // RFC 7230 section 3.5 permits CR in whitespace but only for tolerant parsers
+        input.append("GET\r / HTTP/1.1\r\n", 17);
+        Config.onoff.relaxed_header_parser = 1;
+        struct resultSet expect = {
+            .parsed = false,
+            .needsMore = true,
             .parserState = Http1::HTTP_PARSE_MIME,
             .status = Http::scOkay,
-            .msgStart = 5,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = 5,
-            .methodStart = 0,
-            .methodEnd = 2,
+            .suffixSz = 0,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 4,
             .uri = "/",
-            .versionStart = 6,
-            .versionEnd = 13,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
+
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = input.length(),
+            .method = HttpRequestMethod(),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
-#endif
 
-    // tab padded method (NP: tab is not SP so treated as any other binary)
+    // tolerant parser delimiters
     {
-        input.append("\tGET / HTTP/1.1\n", 16);
+        // RFC 7230 section 3.5 permits certain binary characters as whitespace delimiters
+        input.append("GET\r\t\x0B\x0C / HTTP/1.1\r\n", 20);
+        Config.onoff.relaxed_header_parser = 1;
         struct resultSet expect = {
+            .parsed = false,
+            .needsMore = true,
+            .parserState = Http1::HTTP_PARSE_MIME,
+            .status = Http::scOkay,
+            .suffixSz = 0,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = "/",
+            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1)
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expect);
+
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, //Halt on \t, no valid method char
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
-        testResults(__LINE__, input, output, expect);
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
 }
@@ -971,154 +847,54 @@ testHttp1Parser::testParseRequestLineInvalid()
     SBuf input;
     Http1::RequestParser output;
 
-    // no method (but in a form which is ambiguous with HTTP/0.9 simple-request)
+    // no method (or method delimiter)
     {
-        // XXX: HTTP/0.9 requires method to be "GET"
+        // HTTP/0.9 requires method to be "GET"
         input.append("/ HTTP/1.0\n", 11);
         struct resultSet expect = {
-            .parsed = true,
-            .needsMore = false,
-            .parserState = Http1::HTTP_PARSE_MIME,
-            .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
-            .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 0,
-            .method = HttpRequestMethod(SBuf("/")),
-            .uriStart = 2,
-            .uriEnd = 9,
-            .uri = "HTTP/1.0",
-            .versionStart = -1,
-            .versionEnd = -1,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
-        };
-        output.clear();
-        testResults(__LINE__, input, output, expect);
-        input.clear();
-    }
-
-    // no method (an invalid format)
-    {
-        input.append(" / HTTP/1.0\n", 12);
-#if USE_HTTP_VIOLATIONS
-        // squid custom tolerance consumes initial SP.
-        Config.onoff.relaxed_header_parser = 1;
-        struct resultSet expectRelaxed = {
-            .parsed = true,
-            .needsMore = false,
-            .parserState = Http1::HTTP_PARSE_MIME,
-            .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-2,
-            .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 0,
-            .method = HttpRequestMethod(SBuf("/")),
-            .uriStart = 2,
-            .uriEnd = 9,
-            .uri = "HTTP/1.0",
-            .versionStart = -1,
-            .versionEnd = -1,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
-        };
-        output.clear();
-        testResults(__LINE__, input, output, expectRelaxed);
-#endif
-
-#if !USE_HTTP_VIOLATIONS
-        // a compliant or strict parse, detects as invalid
-        Config.onoff.relaxed_header_parser = 0;
-        struct resultSet expectStrict = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = input.length(),
-            .methodStart = 0,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
-#else
-        // XXX: for now Squid confuses this with HTTP/0.9
-        struct resultSet expectStrict = {
-            .parsed = true,
-            .needsMore = false,
-            .parserState = Http1::HTTP_PARSE_DONE,
-            .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-2,
-            .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 0,
-            .method = HttpRequestMethod(SBuf("/")),
-            .uriStart = 2,
-            .uriEnd = 9,
-            .uri = "HTTP/1.0",
-            .versionStart = -1,
-            .versionEnd = -1,
-            .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
-        };
-#endif
         output.clear();
-        testResults(__LINE__, input, output, expectStrict);
+        testResults(__LINE__, input, output, expect);
         input.clear();
     }
 
-    // binary code in method (invalid)
+    // no method (with method delimiter)
     {
-        input.append("GET\x0B / HTTP/1.1\n", 16);
-        struct resultSet expect = {
+        input.append(" / HTTP/1.0\n", 12);
+        struct resultSet expectStrict = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, //Halt on \x0B, no valid method char
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
-        testResults(__LINE__, input, output, expect);
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
 
-    // CR in method
+    // binary code in method (invalid)
     {
-        // RFC 2616 sec 5.1 prohibits CR other than in terminator.
-        input.append("GET\r / HTTP/1.1\r\n", 16);
+        input.append("GET\x0A / HTTP/1.1\r\n", 17);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, // halt at the first \r
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -1126,25 +902,17 @@ testHttp1Parser::testParseRequestLineInvalid()
         input.clear();
     }
 
-    // binary code NUL! in method (strange but ...)
+    // binary code NUL! in method (always invalid)
     {
-        input.append("GET\0 / HTTP/1.1\n", 16);
+        input.append("GET\0 / HTTP/1.1\r\n", 17);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, // halt at the \0
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -1154,49 +922,49 @@ testHttp1Parser::testParseRequestLineInvalid()
 
     // no URL (grammer invalid, ambiguous with RFC 1945 HTTP/0.9 simple-request)
     {
-        input.append("GET  HTTP/1.1\n", 14);
+        input.append("GET  HTTP/1.1\r\n", 15);
+        // RFC 7230 tolerance allows sequence of SP to make this ambiguous
+        Config.onoff.relaxed_header_parser = 1;
         struct resultSet expect = {
             .parsed = true,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 5,
-            .uriEnd = 12,
             .uri = "HTTP/1.1",
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
         };
         output.clear();
         testResults(__LINE__, input, output, expect);
+
+        Config.onoff.relaxed_header_parser = 0;
+        struct resultSet expectStrict = {
+            .parsed = false,
+            .needsMore = false,
+            .parserState = Http1::HTTP_PARSE_DONE,
+            .status = Http::scBadRequest,
+            .suffixSz = 11,
+            .method = HttpRequestMethod(Http::METHOD_GET),
+            .uri = NULL,
+            .version = AnyP::ProtocolVersion()
+        };
+        output.clear();
+        testResults(__LINE__, input, output, expectStrict);
         input.clear();
     }
 
     // no URL (grammer invalid, ambiguous with RFC 1945 HTTP/0.9 simple-request)
     {
-        input.append("GET HTTP/1.1\n", 13);
+        input.append("GET HTTP/1.1\r\n", 14);
         struct resultSet expect = {
             .parsed = true,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scOkay,
-            .msgStart = 0,
-            .msgEnd = (int)input.length()-1,
             .suffixSz = 0,
-            .methodStart = 0,
-            .methodEnd = 2,
             .method = HttpRequestMethod(Http::METHOD_GET),
-            .uriStart = 4,
-            .uriEnd = 11,
             .uri = "HTTP/1.1",
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,0,9)
         };
         output.clear();
@@ -1212,17 +980,9 @@ testHttp1Parser::testParseRequestLineInvalid()
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, //Halt on \xB, no valid method char
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -1232,25 +992,15 @@ testHttp1Parser::testParseRequestLineInvalid()
 
     // mixed whitespace line
     {
-        // We accept non-space binary bytes for method so first \t shows up as that
-        // but remaining space and tabs are skipped searching for URI-start
         input.append("\t \t \t\n", 6);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, //Halt on \t, no valid method char
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -1258,27 +1008,17 @@ testHttp1Parser::testParseRequestLineInvalid()
         input.clear();
     }
 
-    // mixed whitespace line with CR middle
+    // mixed whitespace line with CR
     {
-        // CR aborts on sight, so even initial \t method is not marked as above
-        // (not when parsing clean with whole line available anyway)
-        input.append("\t  \r \n", 6);
+        input.append("\r  \t \n", 6);
         struct resultSet expect = {
             .parsed = false,
             .needsMore = false,
             .parserState = Http1::HTTP_PARSE_DONE,
             .status = Http::scBadRequest,
-            .msgStart = 0,
-            .msgEnd = -1, // halt on the \r
             .suffixSz = input.length(),
-            .methodStart = -1,
-            .methodEnd = -1,
             .method = HttpRequestMethod(),
-            .uriStart = -1,
-            .uriEnd = -1,
             .uri = NULL,
-            .versionStart = -1,
-            .versionEnd = -1,
             .version = AnyP::ProtocolVersion()
         };
         output.clear();
@@ -1295,13 +1035,13 @@ testHttp1Parser::testDripFeed()
     // calling the parser repeatedly as visible data grows.
 
     SBuf data;
-#if USE_HTTP_VIOLATIONS
-    data.append("            ", 12);
-#else
     data.append("\n\n\n\n\n\n\n\n\n\n\n\n", 12);
-#endif
     SBuf::size_type garbageEnd = data.length();
-    data.append("GET http://example.com/ HTTP/1.1\r\n", 34);
+    data.append("GET ", 4);
+    SBuf::size_type methodEnd = data.length()-1;
+    data.append("http://example.com/ ", 20);
+    SBuf::size_type uriEnd = data.length()-1;
+    data.append("HTTP/1.1\r\n", 10);
     SBuf::size_type reqLineEnd = data.length() - 1;
     data.append("Host: example.com\r\n\r\n", 21);
     SBuf::size_type mimeEnd = data.length() - 1;
@@ -1319,17 +1059,9 @@ testHttp1Parser::testDripFeed()
         .needsMore = true,
         .parserState = Http1::HTTP_PARSE_NONE,
         .status = Http::scNone,
-        .msgStart = -1,
-        .msgEnd = -1,
         .suffixSz = 0,
-        .methodStart = -1,
-        .methodEnd = -1,
         .method = HttpRequestMethod(),
-        .uriStart = -1,
-        .uriEnd = -1,
         .uri = NULL,
-        .versionStart = -1,
-        .versionEnd = -1,
         .version = AnyP::ProtocolVersion()
     };
 
@@ -1343,28 +1075,32 @@ testHttp1Parser::testDripFeed()
         // when the garbage is passed we expect to start seeing first-line bytes
         if (pos == garbageEnd) {
             expect.parserState = Http1::HTTP_PARSE_FIRST;
-            expect.msgStart = 0;
         }
 
         // all points after garbage start to see accumulated bytes looking for end of current section
         if (pos >= garbageEnd)
             expect.suffixSz = ioBuf.length();
 
+        // at end of request line expect to see method details
+        if (pos == methodEnd) {
+            expect.suffixSz = 0; // and a checkpoint buffer reset
+            expect.method = HttpRequestMethod(Http::METHOD_GET);
+        }
+
+        // at end of URI expect to see method, URI details
+        if (pos == uriEnd) {
+            expect.suffixSz = 0; // and a checkpoint buffer reset
+            expect.uri = "http://example.com/";
+        }
+
         // at end of request line expect to see method, URI, version details
         // and switch to seeking Mime header section
         if (pos == reqLineEnd) {
             expect.parserState = Http1::HTTP_PARSE_MIME;
-            expect.suffixSz = 0;
-            expect.msgEnd = reqLineEnd-garbageEnd;
+            expect.suffixSz = 0; // and a checkpoint buffer reset
             expect.status = Http::scOkay;
-            expect.methodStart = 0;
-            expect.methodEnd = 2;
             expect.method = HttpRequestMethod(Http::METHOD_GET);
-            expect.uriStart = 4;
-            expect.uriEnd = 22;
             expect.uri = "http://example.com/";
-            expect.versionStart = 24;
-            expect.versionEnd = 31;
             expect.version = AnyP::ProtocolVersion(AnyP::PROTO_HTTP,1,1);
         }
 
@@ -1373,7 +1109,7 @@ testHttp1Parser::testDripFeed()
         if (pos == mimeEnd) {
             expect.parsed = true;
             expect.needsMore = false;
-            expect.suffixSz = 0;
+            expect.suffixSz = 0; // and a checkpoint buffer reset
         }
 
         testResults(__LINE__, ioBuf, hp, expect);
index f5c0fed29ec2803cc3785dd48af6b29d9760596e..041f6ce66bf023dbd63b02e3606482d07a46d44d 100644 (file)
 class testHttp1Parser : public CPPUNIT_NS::TestFixture
 {
     CPPUNIT_TEST_SUITE( testHttp1Parser );
+    // object basics are working, just in case.
     CPPUNIT_TEST( testParserConstruct );
 
 #if __cplusplus >= 201103L
-    CPPUNIT_TEST( testParseRequestLineTerminators );
+    CPPUNIT_TEST( testDripFeed );
     CPPUNIT_TEST( testParseRequestLineMethods );
     CPPUNIT_TEST( testParseRequestLineProtocols );
+    CPPUNIT_TEST( testParseRequestLineTerminators );
     CPPUNIT_TEST( testParseRequestLineStrange );
     CPPUNIT_TEST( testParseRequestLineInvalid );
-    CPPUNIT_TEST( testDripFeed );
 #endif
     CPPUNIT_TEST_SUITE_END();