Commit 16670822 authored by jan.koester's avatar jan.koester
Browse files

bugfix

parent 4a55a5c4
Loading
Loading
Loading
Loading
+31 −75
Changes for src/http.cpp: 31 added lines, 75 removed lines.
Original line number Diff line number Diff line
@@ -4723,6 +4723,16 @@ size_t libhttppp::HttpRequest::parseH1() {
  auto trim_inplace = [&](std::string& s) { rtrim_inplace(s); ltrim_inplace(s); };

  try {
    // 0) GUARD: Check for TLS handshake record or binary garbage before waiting for \r\n\r\n.
    // TLS records typically start with content type 0x16 (Handshake) at byte 0, 
    // followed by legacy version major 0x03. If detected, fail fast and clear 
    // to prevent infinite loops and high CPU usage from protocol mismatch.
    if (!RecvData.empty() && static_cast<unsigned char>(RecvData[0]) == 0x16) {
      excep[HTTPException::Error] << "TLS connection error: received TLS record on plain HTTP port";
      RecvData.clear();
      throw excep;
    }

    // 1) Find end of headers: "\r\n\r\n"
    size_t endpos = RecvData.search("\r\n\r\n");
    if (endpos == std::string::npos) {
@@ -4731,20 +4741,11 @@ size_t libhttppp::HttpRequest::parseH1() {
      return 0;
    }

    // 1b) Discard whatever header state a *previous* request on this same keep-alive
    // connection left behind. The per-line loop below deliberately does getHeaderData()
    // before setHeaderData() (see its own comment) so that a header genuinely repeated
    // within *this* request's raw bytes (e.g. multiple Cookie: lines) appends rather than
    // clobbers -- but that same lookup can't distinguish "seen earlier in this request" from
    // "left over from the request before it" on a reused connection. Without this, every
    // single-value header (User-Agent, Accept, Referer, ...) silently accumulates one more
    // stale value per request handled on the connection, until a strict backend (observed
    // live: Home Assistant's aiohttp) rejects it outright as a duplicate header.
    // 1b) Discard previous header state on keep-alive connections
    HttpHeader::clear();
    _bodyStreamDeclined = false;

    // 2) Copy header bytes (include the first \r\n of the separator so
    //    every header line, including the last one, ends with \r\n)
    // 2) Copy header bytes
    header.assign(RecvData.begin(), RecvData.begin() + endpos + 2);

    // 3) Consume header (and the delimiter) from RecvData
@@ -4755,20 +4756,16 @@ size_t libhttppp::HttpRequest::parseH1() {
    size_t line_end = 0;
    while (line_end < header.size() && header[line_end] != '\r') ++line_end;
    if (line_end == 0 || line_end >= header.size()) {
      excep[HTTPException::Error] << "can't parse http head";
      excep[HTTPException::Error] << "can't parse http head: empty or malformed request line";
      throw excep;
    }

    // request line as string
    std::string reqline(header.begin(), header.begin() + line_end);
    // advance past "\r\n"
    size_t cursor = line_end + 2;

    // split request line into method, target, version
    std::string method, target, version;
    {
      size_t a = 0;
      // method
      size_t sp1 = reqline.find(' ', a);
      if (sp1 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (no spaces)";
@@ -4776,7 +4773,6 @@ size_t libhttppp::HttpRequest::parseH1() {
      }
      method = reqline.substr(a, sp1 - a);

      // target
      size_t sp2 = reqline.find(' ', sp1 + 1);
      if (sp2 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (missing HTTP version)";
@@ -4784,21 +4780,19 @@ size_t libhttppp::HttpRequest::parseH1() {
      }
      target = reqline.substr(sp1 + 1, sp2 - (sp1 + 1));

      // version
      version = reqline.substr(sp2 + 1);
      trim_inplace(version);
      
      // Ensure version actually looks like HTTP/x.y
      if (version.rfind("HTTP/", 0) != 0) {
        excep[HTTPException::Error] << "invalid request line (malformed HTTP version string)";
        throw excep;
      }
    }

    // 5) Set request type
    // Unrecognized methods map to GETREQUEST inside RequestTypeFromMethod rather than
    // PARSEREQUEST: the REQUESTHANDLING switch in httpd.cpp treats PARSEREQUEST as
    // "re-invoke parse() on whatever's left in RecvData" -- for an already-fully-parsed-but-
    // unrecognized method that reparses the request body (or nothing) as a fresh request
    // line, never reaching RequestEvent at all. The original method string stays available
    // via getRequestMethod() (":method" pseudo-header) either way.
    _RequestType = RequestTypeFromMethod(method);

    // 6) Store pseudo-headers in _firstHeaderData (unified storage)
    // Store pseudo-headers
    {
      auto *hMethod = setHeaderData(":method");
      if (hMethod) hMethod->push_back(method);
@@ -4810,7 +4804,6 @@ size_t libhttppp::HttpRequest::parseH1() {
      if (hVersion) hVersion->push_back(version);
    }

    // 7) Derive cached fields from stored pseudo-headers
    _cachedRequest = target;
    _cachedRequestVersion = version;
    _cachedRequestURL = extractPath(target);
@@ -4818,98 +4811,60 @@ size_t libhttppp::HttpRequest::parseH1() {
    size_t line_start = cursor;

    while (line_start < header.size()) {
      // find CR
      size_t cr = line_start;
      while (cr < header.size() && header[cr] != '\r') ++cr;

      // bounds check: ensure we have at least \r\n
      if (cr >= header.size()) {
        // no complete line found, stop parsing
        break;
      }

      // empty line guard (shouldn't occur inside since we cut at \r\n\r\n, but just in case)
      if (cr >= header.size()) break;
      if (cr == line_start) {
        if (cr + 1 < header.size()) {
          line_start = cr + 2; // skip "\r\n"
        } else {
          break;
        }
        if (cr + 1 < header.size()) line_start = cr + 2;
        else break;
        continue;
      }

      // extract raw line (without CR)
      std::string hline(header.begin() + line_start, header.begin() + cr);

      // move to next line (skip "\r\n")
      if (cr + 1 < header.size() && header[cr + 1] == '\n') {
        line_start = cr + 2;
      } else if (cr + 1 == header.size()) {
        // only CR at end, no LF
        break;
      } else {
        line_start = cr + 1; // tolerant
        line_start = cr + 1;
      }

      // find delimiter ':'
      size_t colon = hline.find(':');
      if (colon == std::string::npos) {
        // malformed header line; skip or treat as error
        // We'll be tolerant and skip
        continue;
      }
      if (colon == std::string::npos) continue;

      std::string key = hline.substr(0, colon);
      std::string value = (colon + 1 < hline.size()) ? hline.substr(colon + 1) : std::string();

      // trim spaces around key/value
      trim_inplace(key);
      trim_inplace(value);

      // lowercase key safely
      for (size_t it = 0; it < key.size(); ++it) {
        key[it] = static_cast<char>(std::tolower(static_cast<unsigned char>(key[it])));
      }

      // Use getHeaderData first so that duplicate header lines (e.g. repeated
      // Cookie: lines) APPEND values instead of clobbering earlier ones --
      // matches parseH2/parseH3 behaviour.
      HeaderData* ncontent = getHeaderData(key);
      if (!ncontent) ncontent = setHeaderData(key);
      if (!ncontent) continue;

      // Only Cookie's grammar (RFC 6265 SS4.2.1) actually packs several
      // "name=value" pairs into one line separated by ';'. Every other
      // header -- User-Agent above all, which is full of semicolons
      // ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...") -- is a single
      // value per physical line. Blanket-splitting on ';' (as this used to
      // do for every header) shredded those into bogus fragments that then
      // got re-serialized as literal duplicate header lines when forwarded
      // upstream. See HttpResponse::parse()'s identical fix above.
      if (key == "cookie") {
        size_t oldiv = 0, iv = 0;
        while (iv < value.size()) {
          if (value[iv] == ';') {
            std::string token = value.substr(oldiv, iv - oldiv);
            trim_inplace(token);
            if (!token.empty()) {
              ncontent->push_back(token);
            }

            ++iv;                            // skip ';'
            while (iv < value.size() && value[iv] == ' ')
              ++iv;                        // skip spaces after ';'
            if (!token.empty()) ncontent->push_back(token);
            ++iv;
            while (iv < value.size() && value[iv] == ' ') ++iv;
            oldiv = iv;
          } else {
            ++iv;
          }
        }

        std::string token = value.substr(oldiv);
        trim_inplace(token);
        if (!token.empty()) {
          ncontent->push_back(token);
        }
        if (!token.empty()) ncontent->push_back(token);
      } else if (!value.empty()) {
        ncontent->push_back(value);
      }
@@ -4922,7 +4877,8 @@ size_t libhttppp::HttpRequest::parseH1() {
    excep[HTTPException::Error] << "netplus error: " << e.what();
    throw excep;
  } catch (...) {
    // keep behavior similar to your original: on parse error, clean up input
    // CRITICAL: Ensure RecvData is fully cleared on parse errors 
    // so the event loop does not infinitely re-parse the same malformed chunk.
    RecvData.clear();
    throw;
  }