- 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>
162 lines
6.6 KiB
C++
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();
|
|
}
|
|
|
|
}
|