Loading src/http.cpp +31 −75 Changes for src/http.cpp: 31 added lines, 75 removed lines. Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading @@ -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)"; Loading @@ -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)"; Loading @@ -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); Loading @@ -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); Loading @@ -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); } Loading @@ -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; } Loading Loading
src/http.cpp +31 −75 Changes for src/http.cpp: 31 added lines, 75 removed lines. Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading @@ -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)"; Loading @@ -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)"; Loading @@ -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); Loading @@ -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); Loading @@ -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); } Loading @@ -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; } Loading