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>
110 lines
3.4 KiB
C++
110 lines
3.4 KiB
C++
#ifndef __HTTPRequest_h__
|
|
#define __HTTPRequest_h__
|
|
|
|
#include "Parser.h"
|
|
#include "LineParser.h"
|
|
#include "IMFHeader.h"
|
|
#include "CGIFormData.h"
|
|
#include "MString.h"
|
|
#include <map>
|
|
|
|
namespace coreutils {
|
|
|
|
///
|
|
/// HTTPRequest parses a full HTTP/1.1 request - request line, headers,
|
|
/// and (for a Content-Length body) either a raw body or, for
|
|
/// application/x-www-form-urlencoded / multipart/form-data, a parsed
|
|
/// CGIFormData - off a shared StreamReader. Resumable across any number
|
|
/// of StreamReader::feed() calls, exactly like every other Parser in
|
|
/// this subsystem: built by chaining LineParser (request line) and
|
|
/// IMFHeader (headers) via checkParse(), the same way CGIFormData itself
|
|
/// chains IMFHeader/IMFBoundary over one shared StreamReader.
|
|
///
|
|
/// This codebase predates ever parsing raw HTTP off a socket (it has
|
|
/// only ever sat behind a CGI gateway) - IMFRequest/IMFResponse/
|
|
/// IMFMessage are stubs from before this Parser/StreamReader subsystem
|
|
/// existed and are not built on here.
|
|
///
|
|
/// v1 limitations, deliberately not addressed (flagged, not fixed -
|
|
/// matching the standard already set by other Parser subclasses here for
|
|
/// known-but-out-of-scope gaps):
|
|
/// - chunked Transfer-Encoding is not supported, only Content-Length.
|
|
/// - repeated header names (e.g. multiple Cookie: lines) last-wins -
|
|
/// the same limitation IMFHeader's std::map already has, inherited
|
|
/// here since HEADERS delegates to IMFHeader directly.
|
|
///
|
|
|
|
class HTTPRequest : public Parser {
|
|
|
|
public:
|
|
|
|
HTTPRequest(StreamReader &reader);
|
|
virtual ~HTTPRequest();
|
|
|
|
///
|
|
/// Valid once doParse() has returned Status::COMPLETE.
|
|
///
|
|
|
|
MString getMethod();
|
|
MString getPath();
|
|
MString getQueryString();
|
|
MString getVersion();
|
|
|
|
///
|
|
/// Case-insensitive header lookup (HTTP header names are
|
|
/// case-insensitive per RFC 7230, unlike IMFHeader's underlying
|
|
/// std::map, which compares byte-for-byte) - returns an empty
|
|
/// MString if the header wasn't present. Valid once the HEADERS
|
|
/// state has completed (i.e. from BODY parsing onward).
|
|
///
|
|
|
|
MString getHeader(const char *name);
|
|
|
|
///
|
|
/// Every header as parsed, keyed exactly as received (not
|
|
/// case-normalized) - prefer getHeader() for lookups.
|
|
///
|
|
|
|
std::map<MString, MString> &getHeaders();
|
|
|
|
///
|
|
/// Valid once doParse() reports COMPLETE. Populated only when the
|
|
/// request had a body that was neither application/
|
|
/// x-www-form-urlencoded nor multipart/form-data (e.g. a raw JSON
|
|
/// body) - empty otherwise, including when there was no body at all.
|
|
///
|
|
|
|
MString getBody();
|
|
|
|
///
|
|
/// Valid once doParse() reports COMPLETE, when the body's
|
|
/// Content-Type was application/x-www-form-urlencoded or
|
|
/// multipart/form-data - NULL otherwise (including no body at all).
|
|
/// Owned by this HTTPRequest - do not delete.
|
|
///
|
|
|
|
CGIFormData *getFormData();
|
|
|
|
protected:
|
|
|
|
Status parse() override;
|
|
|
|
private:
|
|
|
|
enum State { REQUEST_LINE, HEADERS, BODY, DONE } state = REQUEST_LINE;
|
|
|
|
MString method, path, queryString, version;
|
|
std::map<MString, MString> headers;
|
|
MString body;
|
|
MString boundary;
|
|
|
|
LineParser *lineParser = NULL;
|
|
IMFHeader *headerParser = NULL;
|
|
CGIFormData *formParser = NULL;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
#endif
|