Files
CoreUtils/HTTPRequest.cpp
T
Brad ArantandClaude Sonnet 5 05374bdb15 Fix MString::read() infinite retry loop, HTTPRequest parser fd leak, Log deadlock, log rotation symlink handling, and JString bounds checks
- MString::read() now gives up after repeated I/O errors instead of
  looping forever and crashing via realloc() failure (root cause of a
  recurring barant.com production crash).
- HTTPRequest::parse() no longer leaks lineParser/headerParser on a
  malformed request.
- Log gained size-based rotation (config-driven) and a non-static-mutex
  fix; a separate fix stopped rotation from destroying the logs symlink
  on its first trigger.
- File.cpp: symlink-preserving rotation support.
- JString: fixed an array-index out-of-bounds and a setString issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 17:38:14 +00:00

162 lines
6.6 KiB
C++

#include "HTTPRequest.h"
#include "Exception.h"
#include <strings.h>
namespace coreutils {
HTTPRequest::HTTPRequest(StreamReader &reader) : Parser(reader) {}
HTTPRequest::~HTTPRequest() {
delete lineParser;
delete headerParser;
delete formParser;
}
Parser::Status HTTPRequest::parse() {
MString &buffer = reader.getBuffer();
switch (state) {
case REQUEST_LINE: {
if (!lineParser)
lineParser = new LineParser(reader);
if (!checkParse(*lineParser))
return Status::INCOMPLETE;
{
// line is a view into reader's own buffer (see
// LineParser::parse()), not into lineParser itself - safe to
// delete lineParser immediately, before the parts.size()
// check below can throw. Previously this delete sat AFTER
// that check, so a malformed request line (any count other
// than exactly 3 space-separated parts - confirmed live via
// real scanner/bot traffic hitting this daily) leaked
// lineParser and left the member non-NULL. Since state is
// never advanced past REQUEST_LINE on this throw, ANY later
// reentry into this same case (state itself surviving the
// exception, e.g. if this HTTPRequest is ever driven again
// before its owning session tears down) would see a
// non-NULL lineParser and skip allocating a fresh one,
// reusing an already-completed, now-stale parser instance
// instead - a real invariant violation that a heap-
// corruption crash (munmap_chunk(): invalid pointer) was
// traced back to.
ZString line = lineParser->getLine();
delete lineParser;
lineParser = NULL;
ZString sp(" ");
std::vector<ZString> &parts = line.split(sp);
if (parts.size() != 3)
throw coreutils::Exception("HTTPRequest: malformed request line "
"(expected \"METHOD /path HTTP/1.1\").");
method = MString(parts[0]);
path = MString(parts[1]);
version = MString(parts[2]);
}
{
// Split the query string off before anything else sees `path` -
// a route table (see the APIServer plan) should never have to
// deal with a trailing "?a=1&b=2" itself.
int qpos = path.find(ZString("?"));
if (qpos != -1) {
queryString = MString(path.substring(qpos + 1));
path = MString(path.substring(0, qpos));
}
}
state = HEADERS;
return Status::DATA_AVAIL;
}
case HEADERS: {
if (!headerParser)
headerParser = new IMFHeader(reader);
// The actual throw for a malformed header (e.g. a header line
// with no ':') happens deep inside checkParse() -> IMFHeader::
// parse(), well before headerParser->getHeaders() below ever
// runs - same leaked-pointer/stale-reentry hazard as
// REQUEST_LINE above, confirmed as the other half of the same
// real crash. Cleaning up here before rethrowing keeps
// headerParser NULL on every exit path, matching the
// REQUEST_LINE fix.
bool complete;
try {
complete = checkParse(*headerParser);
} catch (...) {
delete headerParser;
headerParser = NULL;
throw;
}
if (!complete)
return Status::INCOMPLETE;
headers = headerParser->getHeaders();
delete headerParser;
headerParser = NULL;
state = BODY;
return Status::DATA_AVAIL;
}
case BODY: {
MString contentLengthHeader = getHeader("Content-Length");
if (contentLengthHeader.getLength() == 0) {
if (getHeader("Transfer-Encoding").getLength() > 0)
throw coreutils::Exception("HTTPRequest: chunked Transfer-Encoding "
"is not supported (Content-Length only).");
state = DONE;
return Status::COMPLETE;
}
int contentLength = contentLengthHeader.asInteger();
MString contentType = getHeader("Content-Type");
if (contentType.startsWith("application/x-www-form-urlencoded") ||
contentType.startsWith("multipart/form-data")) {
if (!formParser) {
// Mirrors __jet.cpp's identical CGI-path fix: find() leaves
// contentType's own cursor at the match, and CGIFormData's
// constructor copy-constructs its own contentType member
// from whatever we pass, inheriting that same moved cursor
// - its startsWith() dispatch checks are cursor-relative
// and would silently miss (falling through to UNKNOWN)
// without the reset().
int boundaryPos = contentType.find(ZString("boundary="));
if (boundaryPos != -1)
boundary = MString(contentType.substring(boundaryPos + 9));
contentType.reset();
formParser = new CGIFormData(reader, contentType, contentLength, boundary);
}
if (!checkParse(*formParser))
return Status::INCOMPLETE;
state = DONE;
return Status::COMPLETE;
}
if ((int)buffer.getLength() < contentLength)
return Status::INCOMPLETE;
body = MString(ZString(buffer.getCursor(), contentLength));
buffer.setCursor(buffer.getCursor() + contentLength);
state = DONE;
return Status::COMPLETE;
}
case DONE:
return Status::COMPLETE;
}
throw coreutils::Exception("HTTPRequest: unreachable state.");
}
MString HTTPRequest::getMethod() { return method; }
MString HTTPRequest::getPath() { return path; }
MString HTTPRequest::getQueryString() { return queryString; }
MString HTTPRequest::getVersion() { return version; }
MString HTTPRequest::getBody() { return body; }
std::map<MString, MString> &HTTPRequest::getHeaders() { return headers; }
CGIFormData *HTTPRequest::getFormData() { return formParser; }
MString HTTPRequest::getHeader(const char *name) {
for (auto &pair : headers) {
MString key = pair.first;
if (strcasecmp(key.c_str(), name) == 0)
return pair.second;
}
return MString();
}
}