Add HTTPRequest/HTTPResponse: real HTTP parsing on the Parser/StreamReader stack
Stage 2 of the APIServer plan. This codebase has only ever sat behind a CGI gateway - IMFRequest/IMFResponse/IMFMessage predate the StreamReader/Parser rebuild and are stubs (IMFResponse's header-emission calls addHeader()/output(), neither of which exist on the class; IMFRequest is a fragile non-resumable 3-token line split). Nothing in this repo has ever actually parsed a raw HTTP request off a socket. HTTPRequest : public Parser fills that gap the same way every other Parser here is built: REQUEST_LINE -> HEADERS -> BODY -> DONE, chaining LineParser (request line) and IMFHeader (headers, reused as-is - HTTP header syntax is IMF-derived, nothing email-specific in IMFHeader itself) via checkParse() over one shared StreamReader, exactly the pattern CGIFormData already established for chaining IMFHeader/ IMFBoundary. The BODY state dispatches on Content-Type as CGIFormData itself does for CGI bodies - application/x-www-form-urlencoded and multipart/form-data delegate directly to CGIFormData (reused, not reimplemented), anything else is captured as a raw body (e.g. JSON). Query string is split off the path before anything else sees it. getHeader() does a case-insensitive lookup (HTTP header names are case-insensitive per RFC 7230, unlike IMFHeader's own std::map, which compares byte-for-byte) since a route handler constantly needs this and getting it wrong would be a silent framing bug, not a cosmetic one. The boundary-extraction code mirrors __jet.cpp's own fix exactly (same cursor-mutation trap: ZString::find() leaves the searched string's cursor at the match, and CGIFormData's constructor copy-constructs its own contentType member from whatever's passed in, inheriting that same moved cursor unless reset() first - would otherwise silently miss its own startsWith() dispatch checks). HTTPResponse is a small, new, output-only builder (status line + a real headers container + body, Content-Length auto-computed unless set explicitly) - deliberately not patched from IMFResponse, which has nothing safe to extend. v1 limitations, flagged not fixed (matching the standard already set by other Parser subclasses here for known-but-out-of-scope gaps): chunked Transfer-Encoding isn't supported, and repeated header names last-wins (inherited from IMFHeader's own std::map). Verified live via testing/http_test.cpp (34 assertions, new - now wired into testing/compile): byte-at-a-time GET with a query string and case-insensitive header lookup; a GET with no query string; a POST with an urlencoded body deliberately split mid-body (not just mid-header) across two feed() calls, confirming the CGIFormData delegation resumes correctly; a raw JSON POST body; testing/http_request.data (a real fixture, previously unused - normalized from its stored LF line endings to real CRLF, since HTTP requires CRLF and IMFHeader enforces that strictly) fed byte-at-a-time; and HTTPResponse::serialize() both with an auto-computed and an explicitly-set Content-Length (no duplicate header). Also reran the full existing testing/compile suite (zstring/mstring/jstring/mfile/jfile) against the rebuilt libCoreUtils.a to confirm no regression from the two new files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e4e463a96
commit
cb845efe69
7 files changed
+521
No files matched your search
+127
@@ -0,0 +1,127 @@
|
||||
#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;
|
||||
{
|
||||
ZString line = lineParser->getLine();
|
||||
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]);
|
||||
}
|
||||
delete lineParser;
|
||||
lineParser = NULL;
|
||||
{
|
||||
// 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);
|
||||
if (!checkParse(*headerParser))
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user