#include #include #include #include #include #include "../StreamReader.h" #include "../HTTPRequest.h" #include "../HTTPResponse.h" using namespace coreutils; static int checks = 0; #define CHECK(cond) do { checks++; if(!(cond)) { std::cerr << "FAIL line " << __LINE__ << ": " #cond << std::endl; return 1; } } while(0) // Feeds a StreamReader one byte at a time and drives a Parser's doParse() // after every single byte - the harshest fragmentation test used elsewhere // in this subsystem (see CGIFormData's byte-at-a-time stress test) - to // prove HTTPRequest is genuinely resumable across arbitrary split points, // not just "works when handed the whole request at once". static Parser::Status feedByteAtATime(StreamReader &reader, Parser &parser, const std::string &data) { Parser::Status status = Parser::Status::INCOMPLETE; for (size_t i = 0; i < data.size() && status != Parser::Status::COMPLETE; ++i) { ZString chunk((char *)data.data() + i, 1); reader.feed(chunk); status = parser.doParse(); } return status; } int test_get_request_byte_at_a_time() { StreamReader reader; HTTPRequest req(reader); std::string raw = "GET /hello?x=1&y=two HTTP/1.1\r\n" "Host: example.com\r\n" "cookie: session=abc123\r\n" "\r\n"; CHECK(feedByteAtATime(reader, req, raw) == Parser::Status::COMPLETE); CHECK(req.getMethod() == "GET"); CHECK(req.getPath() == "/hello"); CHECK(req.getQueryString() == "x=1&y=two"); CHECK(req.getVersion() == "HTTP/1.1"); CHECK(req.getHeader("Host") == "example.com"); // Case-insensitive lookup: header arrived lowercase ("cookie:"), looked // up with the canonical HTTP capitalization ("Cookie") - real clients // and servers disagree on casing constantly, this must not care. CHECK(req.getHeader("Cookie") == "session=abc123"); CHECK(req.getBody().getLength() == 0); CHECK(req.getFormData() == NULL); return 0; } int test_get_request_no_query_string() { StreamReader reader; HTTPRequest req(reader); std::string raw = "GET /plain HTTP/1.1\r\nHost: h\r\n\r\n"; CHECK(feedByteAtATime(reader, req, raw) == Parser::Status::COMPLETE); CHECK(req.getPath() == "/plain"); CHECK(req.getQueryString().getLength() == 0); return 0; } int test_post_urlencoded_body_split_across_feeds() { StreamReader reader; HTTPRequest req(reader); std::string raw = "POST /submit HTTP/1.1\r\n" "Host: example.com\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-Length: 14\r\n" "\r\n" "name=A%20B&x=1"; // Split mid-body, not just mid-header - the whole point of building this // on the same StreamReader/checkParse() chain CGIFormData already uses. size_t splitAt = raw.size() - 5; ZString part1((char *)raw.data(), splitAt); reader.feed(part1); CHECK(req.doParse() == Parser::Status::INCOMPLETE); ZString part2((char *)raw.data() + splitAt, raw.size() - splitAt); reader.feed(part2); CHECK(req.doParse() == Parser::Status::COMPLETE); CHECK(req.getMethod() == "POST"); CHECK(req.getFormData() != NULL); MString name = (*req.getFormData())["name"]; CHECK(name == "A B"); MString x = (*req.getFormData())["x"]; CHECK(x == "1"); CHECK(req.getBody().getLength() == 0); // consumed by the form parser, not the raw-body path return 0; } int test_post_raw_json_body() { StreamReader reader; HTTPRequest req(reader); std::string body = "{\"a\":1}"; std::string raw = "POST /api HTTP/1.1\r\n" "Host: example.com\r\n" "Content-Type: application/json\r\n" "Content-Length: " + std::to_string(body.size()) + "\r\n" "\r\n" + body; CHECK(feedByteAtATime(reader, req, raw) == Parser::Status::COMPLETE); CHECK(req.getFormData() == NULL); CHECK(req.getBody() == body.c_str()); return 0; } int test_real_fixture_file() { // testing/http_request.data is a real captured request, but stored with // plain LF line endings (a text-editing convenience, confirmed via // `file`/`xxd` - not a literal wire capture); real HTTP requires CRLF // per RFC 7230, and IMFHeader (which HTTPRequest's HEADERS state reuses // as-is) enforces that strictly. Normalize LF -> CRLF before feeding, // same as any real client would actually send it. std::ifstream in("http_request.data"); CHECK((bool)in); std::stringstream ss; ss << in.rdbuf(); std::string raw = ss.str(); std::string crlf; crlf.reserve(raw.size() + 16); for (char c : raw) { if (c == '\n') crlf += '\r'; crlf += c; } StreamReader reader; HTTPRequest req(reader); CHECK(feedByteAtATime(reader, req, crlf) == Parser::Status::COMPLETE); CHECK(req.getMethod() == "GET"); CHECK(req.getPath() == "/"); CHECK(req.getHeader("Host") == "localhost:8080"); CHECK(req.getHeader("Cookie") == "_ga=GA1.1.1017526412.1628195401"); return 0; } int test_http_response_serialize() { HTTPResponse resp; resp.setStatus(200, "OK"); resp.setHeader("Content-Type", "text/plain"); resp.setBody("hello"); MString out = resp.serialize(); std::string s(out.getData(), out.getLength()); CHECK(s.find("HTTP/1.1 200 OK\r\n") == 0); CHECK(s.find("Content-Type: text/plain\r\n") != std::string::npos); CHECK(s.find("Content-Length: 5\r\n") != std::string::npos); CHECK(s.find("\r\n\r\nhello") != std::string::npos); return 0; } int test_http_response_explicit_content_length_respected() { HTTPResponse resp; resp.setStatus(404, "Not Found"); resp.setHeader("Content-Length", "0"); resp.setBody(""); MString out = resp.serialize(); std::string s(out.getData(), out.getLength()); CHECK(s.find("HTTP/1.1 404 Not Found\r\n") == 0); int firstCL = s.find("Content-Length:"); int lastCL = s.rfind("Content-Length:"); CHECK(firstCL == lastCL); // no duplicate Content-Length header return 0; } int main() { int rc = 0; rc |= test_get_request_byte_at_a_time(); rc |= test_get_request_no_query_string(); rc |= test_post_urlencoded_body_split_across_feeds(); rc |= test_post_raw_json_body(); rc |= test_real_fixture_file(); rc |= test_http_response_serialize(); rc |= test_http_response_explicit_content_length_respected(); if (rc == 0) std::cout << "ALL " << checks << " CHECKS PASSED" << std::endl; else std::cout << "SOME CHECKS FAILED (see above)" << std::endl; return rc; }