From b894ec845d364c8767400e41259279cc638d0907 Mon Sep 17 00:00:00 2001 From: Alexey Ozeritskiy Date: Sat, 8 Nov 2025 19:40:11 +0000 Subject: [PATCH] Add http server --- coroio/CMakeLists.txt | 1 + coroio/http/httpd.cpp | 364 +++++++++++++++++++++++++++++++++++++++ coroio/http/httpd.hpp | 234 +++++++++++++++++++++++++ coroio/sockutils.hpp | 42 ++++- examples/CMakeLists.txt | 3 +- examples/hello_httpd.cpp | 43 +++++ tests/CMakeLists.txt | 1 + tests/test_http.cpp | 308 +++++++++++++++++++++++++++++++++ 8 files changed, 988 insertions(+), 8 deletions(-) create mode 100644 coroio/http/httpd.cpp create mode 100644 coroio/http/httpd.hpp create mode 100644 examples/hello_httpd.cpp create mode 100644 tests/test_http.cpp diff --git a/coroio/CMakeLists.txt b/coroio/CMakeLists.txt index 10b174c..3dfbf26 100644 --- a/coroio/CMakeLists.txt +++ b/coroio/CMakeLists.txt @@ -18,6 +18,7 @@ set(SOURCES backends/iocp.cpp ws/ws.cpp ws/utils.cpp + http/httpd.cpp ) if (WIN32) diff --git a/coroio/http/httpd.cpp b/coroio/http/httpd.cpp new file mode 100644 index 0000000..84d6764 --- /dev/null +++ b/coroio/http/httpd.cpp @@ -0,0 +1,364 @@ +#include "httpd.hpp" + +namespace NNet +{ + +namespace { + +std::string UrlDecode(const std::string& str) { + std::string result; + result.reserve(str.size()); + + for (size_t i = 0; i < str.size(); ++i) { + if (str[i] == '%' && i + 2 < str.size()) { + char hex[3] = { str[i + 1], str[i + 2], 0 }; + char decodedChar = static_cast(std::strtol(hex, nullptr, 16)); + result += decodedChar; + i += 2; + } else if (str[i] == '+') { + result += ' '; + } else { + result += str[i]; + } + } + + return result; +} + +} // namespace + +TUri::TUri(const std::string& uriStr) { + Parse(uriStr); +} + +void TUri::Parse(const std::string& uriStr) { + size_t pathEnd = uriStr.find_first_of("?#"); + Path_ = UrlDecode(uriStr.substr(0, pathEnd)); + + if (pathEnd != std::string::npos) { + if (uriStr[pathEnd] == '?') { + size_t queryEnd = uriStr.find('#', pathEnd); + std::string queryStr = uriStr.substr(pathEnd + 1, + queryEnd == std::string::npos ? std::string::npos : queryEnd - pathEnd - 1); + + size_t pos = 0; + while (pos < queryStr.size()) { + size_t ampPos = queryStr.find('&', pos); + std::string param = queryStr.substr(pos, ampPos - pos); + size_t eqPos = param.find('='); + if (eqPos != std::string::npos) { + std::string name = UrlDecode(param.substr(0, eqPos)); + std::string value = UrlDecode(param.substr(eqPos + 1)); + QueryParameters_[name] = value; + } else { + std::string name = UrlDecode(param); + QueryParameters_[name] = ""; + } + if (ampPos == std::string::npos) { + break; + } + pos = ampPos + 1; + } + + if (queryEnd != std::string::npos) { + Fragment_ = UrlDecode(uriStr.substr(queryEnd + 1)); + } + } else if (uriStr[pathEnd] == '#') { + Fragment_ = UrlDecode(uriStr.substr(pathEnd + 1)); + } + } +} + +const std::string& TUri::Fragment() const { + return Fragment_; +} + +const std::map& TUri::QueryParameters() const { + return QueryParameters_; +} + +const std::string& TUri::Path() const { + return Path_; +} + + +TRequest::TRequest(std::string&& header, + std::function(char*, size_t)> bodyReader, + std::function()> chunkHeaderReader) + : Header_(std::move(header)) + , BodyReader_(std::move(bodyReader)) + , ChunkHeaderReader_(std::move(chunkHeaderReader)) +{ + // Parse the request line and headers + ParseRequestLine(); + ParseHeaders(); +} + +void TRequest::ParseRequestLine() { + size_t lineEnd = Header_.find("\r\n"); + if (lineEnd == std::string::npos) { + throw std::runtime_error("Invalid HTTP request: no request line"); + } + HeaderStartPos_ = lineEnd + 2; + + std::string_view requestLine(Header_.data(), lineEnd); + size_t methodEnd = requestLine.find(' '); + if (methodEnd == std::string::npos) { + throw std::runtime_error("Invalid HTTP request: no method"); + } + Method_ = requestLine.substr(0, methodEnd); + + size_t uriStart = methodEnd + 1; + size_t uriEnd = requestLine.find(' ', uriStart); + if (uriEnd == std::string::npos) { + throw std::runtime_error("Invalid HTTP request: no URI"); + } + Uri_ = TUri(std::string(requestLine.substr(uriStart, uriEnd - uriStart))); + + size_t versionStart = uriEnd + 1; + if (versionStart >= requestLine.size()) { + throw std::runtime_error("Invalid HTTP request: no version"); + } + Version_ = requestLine.substr(versionStart); +} + +void TRequest::ParseHeaders() { + size_t pos = HeaderStartPos_; + while (pos < Header_.size()) { + size_t lineEnd = std::string_view(Header_.data() + pos, + Header_.size() - pos).find("\r\n"); + if (lineEnd == std::string::npos) { + break; + } + std::string_view headerLine(Header_.data() + pos, lineEnd); + size_t colonPos = headerLine.find(':'); + if (colonPos != std::string::npos) { + std::string_view name = headerLine.substr(0, colonPos); + std::string_view value = headerLine.substr(colonPos + 1); + // Trim leading spaces from value + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + Headers_[name] = value; + } + pos += lineEnd + 2; + } + + auto maybeContentLength = Headers_.find("Content-Length");; + if (maybeContentLength != Headers_.end()) { + ContentLength_ = std::stoi(std::string(maybeContentLength->second)); + HasBody_ = ContentLength_ > 0; + } + + auto maybeChunked = Headers_.find("Transfer-Encoding");; + if (maybeChunked != Headers_.end()) { + if (maybeChunked->second == "chunked") { + Chunked_ = true; + } + } +} + +bool TRequest::RequireConnectionClose() const { + auto connectionHeader = Headers_.find("Connection"); + if (connectionHeader != Headers_.end()) { + if (connectionHeader->second == "close") { + return true; + } + } + if (Version_ == "HTTP/1.0") { + return true; + } + return false; +} + +std::string_view TRequest::Method() const { + return Method_; +} + +const TUri& TRequest::Uri() const { + return Uri_; +} + +bool TRequest::HasBody() const { + return HasBody_; +} + +bool TRequest::BodyConsumed() const { + return BodyConsumed_; +} + +TFuture TRequest::ReadBodyFull() { + std::string body; + if (!Chunked_) { + body.reserve(ContentLength_); + while (ContentLength_ > 0) { + char buffer[4096]; + ssize_t bytesRead = co_await ReadBodySomeContentLength(buffer, std::min(sizeof(buffer), ContentLength_)); + if (bytesRead <= 0) { + throw std::runtime_error("Error reading request body"); + } + body.append(buffer, bytesRead); + } + } else { + while (!BodyConsumed_) { + char buffer[4096]; + ssize_t bytesRead = co_await ReadBodySomeChunked(buffer, sizeof(buffer)); + if (bytesRead < 0) { + throw std::runtime_error("Error reading request body"); + } + if (bytesRead == 0) { + break; + } + body.append(buffer, bytesRead); + } + } + co_return body; +} + +TFuture TRequest::ReadBodySome(char* buffer, size_t size) { + if (BodyConsumed_) { + co_return 0; + } + if (!Chunked_) { + co_return co_await ReadBodySomeContentLength(buffer, size); + } else { + co_return co_await ReadBodySomeChunked(buffer, size); + } +} + +TFuture TRequest::ReadBodySomeContentLength(char* buffer, size_t size) { + // read up to Content-Length + if (ContentLength_ == 0) { + BodyConsumed_ = true; + co_return 0; + } + ssize_t bytesRead = co_await BodyReader_(buffer, std::min(size, ContentLength_)); + if (bytesRead > 0) { + ContentLength_ -= bytesRead; + } + if (ContentLength_ == 0) { + BodyConsumed_ = true; + } + co_return bytesRead; +} + +TFuture TRequest::ReadBodySomeChunked(char* buffer, size_t size) { + // Read size\r\n + // Read data\r\n + + auto readCrLf = [&]() -> TFuture { + char crlf[2]; + auto size = co_await BodyReader_(crlf, 2); + if (size != 2 || crlf[0] != '\r' || crlf[1] != '\n') { + throw std::runtime_error("Invalid chunked encoding"); + } + co_return; + }; + + if (CurrentChunkSize_ == 0) { + auto line = co_await ChunkHeaderReader_(); + CurrentChunkSize_ = std::stoul(line, nullptr, 16); + if (CurrentChunkSize_ == 0) { + BodyConsumed_ = true; + co_await readCrLf(); + co_return 0; + } + } + + size_t toRead = std::min(size, CurrentChunkSize_); + ssize_t bytesRead = co_await BodyReader_(buffer, toRead); + if (bytesRead > 0) { + CurrentChunkSize_ -= bytesRead; + if (CurrentChunkSize_ == 0) { + co_await readCrLf(); + } + } + co_return bytesRead; +} + +void TResponse::SetStatus(int statusCode) { + StatusCode_ = statusCode; +} + +void TResponse::SetHeader(const std::string& name, const std::string& value) { + Headers_[name] = value; +} + +TFuture TResponse::CompleteWrite(const char* data, size_t size) { + const char* p = data; + size_t remaining = size; + while (remaining != 0) { + ssize_t written = co_await Writer_(p, remaining); + if (written <= 0) { + throw std::runtime_error("Error writing response body"); + } + p += written; + remaining -= written; + } + co_return; +} + +TFuture TResponse::SendHeaders() { + // send response line and headers + if (HeadersSent_) { + co_return; + } + HeadersSent_ = true; + + std::string headerStr = "HTTP/1.1 " + std::to_string(StatusCode_) + " OK\r\n"; + for (const auto& header : Headers_) { + headerStr += header.first + ": " + header.second + "\r\n"; + } + headerStr += "\r\n"; + + co_await CompleteWrite(headerStr.data(), headerStr.size()); + + auto maybeChunked = Headers_.find("Transfer-Encoding"); + if (maybeChunked != Headers_.end()) { + if (maybeChunked->second == "chunked") { + Chunked_ = true; + } + } + auto maybeIsClosed = Headers_.find("Connection"); + if (maybeIsClosed != Headers_.end()) { + if (maybeIsClosed->second == "close") { + IsClosed_ = true; + } + } + co_return; +} + +bool TResponse::IsClosed() const { + return IsClosed_; +} + +TFuture TResponse::WriteBodyChunk(const char* data, size_t size) { + // if chunked => send chunk size + \r\n + data + \r\n + // else => send part of body + if (Chunked_) { + std::string chunkHeader = std::to_string(size) + "\r\n"; + co_await CompleteWrite(chunkHeader.data(), chunkHeader.size()); + co_await CompleteWrite(data, size); + co_await CompleteWrite("\r\n", 2); + } else { + co_await CompleteWrite(data, size); + } +} + +TFuture TResponse::WriteBodyFull(const std::string& data) { + if (Chunked_) { + // If chunked, we need to send the data in chunks + size_t offset = 0; + constexpr size_t chunkSize = 8192; + while (offset < data.size()) { + size_t toSend = std::min(data.size() - offset, chunkSize); + co_await WriteBodyChunk(data.data() + offset, toSend); + offset += toSend; + } + } else { + // If not chunked, we can send the data all at once + co_await CompleteWrite(data.data(), data.size()); + } +} + +} // namespace NNet diff --git a/coroio/http/httpd.hpp b/coroio/http/httpd.hpp new file mode 100644 index 0000000..0ac9295 --- /dev/null +++ b/coroio/http/httpd.hpp @@ -0,0 +1,234 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace NNet +{ + +// /path?arg1=value1&arg2=value2#fragment +class TUri { +public: + TUri() = default; + TUri(const std::string& uriStr); + const std::string& Path() const; + const std::map& QueryParameters() const; + const std::string& Fragment() const; + +private: + void Parse(const std::string& uriStr); + std::string Path_; + std::map QueryParameters_; + std::string Fragment_; +}; + +class TRequest { +public: + TRequest(std::string&& header, + std::function(char*, size_t)> bodyReader, + std::function()> chunkHeaderReader = {}); + std::string_view Method() const; + const TUri& Uri() const; + std::string_view Version() const { return Version_; } + + bool HasBody() const; + TFuture ReadBodyFull(); + TFuture ReadBodySome(char* buffer, size_t size); // read up to Content-Length + bool BodyConsumed() const; + bool RequireConnectionClose() const; + +private: + void ParseRequestLine(); + void ParseHeaders(); + + TFuture ReadBodySomeContentLength(char* buffer, size_t size); + TFuture ReadBodySomeChunked(char* buffer, size_t size); + + std::string Header_; + size_t HeaderStartPos_ = 0; + std::map Headers_; + size_t ContentLength_ = 0; + bool HasBody_ = false; + bool Chunked_ = false; + bool BodyConsumed_ = false; + size_t CurrentChunkSize_ = 0; + std::function(char*, size_t)> BodyReader_; + std::function()> ChunkHeaderReader_; + std::string_view Method_; + TUri Uri_; + std::string_view Version_; +}; + +class TResponse { +public: + TResponse(std::function(const void*, size_t)> writer) + : Writer_(std::move(writer)) + {} + void SetStatus(int statusCode); + void SetHeader(const std::string& name, const std::string& value); + TFuture SendHeaders(); + TFuture WriteBodyChunk(const char* data, size_t size); // Chunked transfer encoding + TFuture WriteBodyFull(const std::string& data); // Content-Length + body + bool IsClosed() const; + int StatusCode() const { + return StatusCode_; + } + +private: + TFuture CompleteWrite(const char* data, size_t size); + + int StatusCode_ = 200; + std::map Headers_; + bool HeadersSent_ = false; + bool Chunked_ = false; + bool IsClosed_ = false; + std::function(const void*, size_t)> Writer_; +}; + +struct IRouter { + virtual TFuture HandleRequest(const TRequest& request, TResponse& response) = 0; +}; + +class THelloWorldRouter : public IRouter { +public: + TFuture HandleRequest(const TRequest& request, TResponse& response) override { + if (request.Uri().Path() == "/") { + response.SetStatus(200); + response.SetHeader("Content-Type", "text/plain"); + response.SetHeader("Connection", "close"); + co_await response.SendHeaders(); + co_await response.WriteBodyFull("Hello, World!"); + } else { + response.SetStatus(404); + response.SetHeader("Content-Type", "text/plain"); + response.SetHeader("Connection", "close"); + co_await response.SendHeaders(); + co_await response.WriteBodyFull("Not Found"); + } + } +}; + +template +class TWebServer { +public: + TWebServer(TSocket&& serverSocket, IRouter& router, std::function logger = {}) + : ServerSocket(std::move(serverSocket)) + , Router(router) + , Logger(std::move(logger)) + {} + + TVoidTask Start() { + while (true) { + auto clientSocket = co_await ServerSocket.Accept(); + HandleClient(std::move(clientSocket)); + } + } + +private: + TVoidTask HandleClient(TSocket clientSocket) { + auto byteReader = TByteReader(clientSocket); + + auto bodyReader = [&](char* buffer, size_t size) -> TFuture { + co_return co_await byteReader.ReadSome(buffer, size); + }; + + auto chunkHeaderReader = [&]() -> TFuture { + co_return co_await byteReader.ReadUntil("\r\n"); + }; + + auto bodyWriter = [&](const void* data, size_t size) -> TFuture { + co_return co_await clientSocket.WriteSome(data, size); + }; + + std::string clientString = clientSocket.RemoteAddr() ? clientSocket.RemoteAddr()->ToString() : "unknown"; + + try { + while (true) { + auto header = co_await byteReader.ReadUntil("\r\n\r\n"); + TRequest request(std::move(header), bodyReader, chunkHeaderReader); + TResponse response(bodyWriter); + co_await Router.HandleRequest(request, response); + Log(request, response, clientString); + if (response.IsClosed() || request.RequireConnectionClose()) { + break; + } + } + } catch (const std::exception& ex) { + if (Logger) { + Logger(std::string("Client handler exception: ") + ex.what()); + } + } + co_return; + } + + void Log(const TRequest& request, const TResponse& response, const std::string& clientString) { + if (!Logger) { + return; + } + LogStream.str(""); + LogStream.clear(); + + // Build full path with query parameters if any + std::string fullPath = request.Uri().Path(); + const auto& qp = request.Uri().QueryParameters(); + if (!qp.empty()) { + fullPath.push_back('?'); + bool first = true; + for (const auto& [k,v] : qp) { + if (!first) fullPath.push_back('&'); + first = false; + fullPath.append(k); + fullPath.push_back('='); + fullPath.append(v); + } + } + + // Timestamp in nginx style: [08/Nov/2025:15:23:10 +0100] + auto now = std::chrono::system_clock::now(); + std::time_t raw = std::chrono::system_clock::to_time_t(now); + std::tm localTm{}; + std::tm gmTm{}; +#if defined(_WIN32) + localtime_s(&localTm, &raw); + gmtime_s(&gmTm, &raw); +#else + localtime_r(&raw, &localTm); + gmtime_r(&raw, &gmTm); +#endif + // Compute timezone offset + // mktime converts tm in local time to time_t; difference gives offset vs UTC + std::time_t localTime = std::mktime(&localTm); + std::time_t gmTime = std::mktime(&gmTm); + long tzOffsetSec = static_cast(difftime(localTime, gmTime)); + int tzSign = tzOffsetSec >= 0 ? 1 : -1; + tzOffsetSec = std::labs(tzOffsetSec); + int tzHours = static_cast(tzOffsetSec / 3600); + int tzMins = static_cast((tzOffsetSec % 3600) / 60); + char tzBuf[8]; + std::snprintf(tzBuf, sizeof(tzBuf), "%c%02d%02d", tzSign > 0 ? '+' : '-', tzHours, tzMins); + + // Prepare HTTP version string + std::string_view ver = request.Version(); + bool hasHttpPrefix = ver.size() >= 5 && ver.substr(0,5) == "HTTP/"; + + LogStream << clientString << " - - [" << std::put_time(&localTm, "%d/%b/%Y:%H:%M:%S ") << tzBuf << "] \"" + << request.Method() << ' ' << fullPath << ' ' << (hasHttpPrefix ? std::string(ver) : (std::string("HTTP/") + std::string(ver))) << "\" " + << response.StatusCode() << ' ' << '-' << ' ' << "\"-\" \"-\""; + Logger(LogStream.str()); + } + + TSocket ServerSocket; + IRouter& Router; + std::function Logger; + std::ostringstream LogStream; +}; + +} // namespace NNet { \ No newline at end of file diff --git a/coroio/sockutils.hpp b/coroio/sockutils.hpp index f146c69..12c41f3 100644 --- a/coroio/sockutils.hpp +++ b/coroio/sockutils.hpp @@ -91,10 +91,10 @@ struct TByteReader { if (!Buffer.empty()) { size_t toCopy = std::min(size, Buffer.size()); - std::memcpy(p, Buffer.data(), toCopy); + std::copy(Buffer.begin(), Buffer.begin() + toCopy, p); p += toCopy; size -= toCopy; - Buffer.erase(0, toCopy); + Buffer.erase(Buffer.begin(), Buffer.begin() + toCopy); } while (size != 0) { @@ -110,6 +110,34 @@ struct TByteReader { } co_return; } + + TFuture ReadSome(void* data, size_t size) { + char tempBuffer[1024]; + // read by chunks, copy to user up-to size bytes + + if (!Buffer.empty()) { + size_t toCopy = std::min(size, Buffer.size()); + std::copy(Buffer.begin(), Buffer.begin() + toCopy, static_cast(data)); + Buffer.erase(Buffer.begin(), Buffer.begin() + toCopy); + co_return toCopy; + } + + auto readSize = co_await Socket.ReadSome(tempBuffer, sizeof(tempBuffer)); + if (readSize == 0) { + co_return 0; + } + if (readSize < 0) { + co_return -1; // retry + } + + size_t toCopy = std::min(static_cast(readSize), size); + std::copy(tempBuffer, tempBuffer + toCopy, static_cast(data)); + if (static_cast(readSize) > toCopy) { + Buffer.insert(Buffer.end(), tempBuffer + toCopy, tempBuffer + readSize); + } + co_return toCopy; + } + /** * @brief Reads data until the given @p delimiter is encountered. * @@ -136,12 +164,12 @@ struct TByteReader { auto pos = std::search(Buffer.begin(), Buffer.end(), delimiter.begin(), delimiter.end()); if (pos != Buffer.end()) { size_t delimiterOffset = std::distance(Buffer.begin(), pos); - result.append(Buffer.substr(0, delimiterOffset + delimiter.size())); - Buffer.erase(0, delimiterOffset + delimiter.size()); + result.insert(result.end(), Buffer.begin(), Buffer.begin() + delimiterOffset + delimiter.size()); + Buffer.erase(Buffer.begin(), Buffer.begin() + delimiterOffset + delimiter.size()); co_return result; } - result.append(Buffer); + result.insert(result.end(), Buffer.begin(), Buffer.end()); Buffer.clear(); auto readSize = co_await Socket.ReadSome(tempBuffer, sizeof(tempBuffer)); @@ -152,7 +180,7 @@ struct TByteReader { continue; // retry } - Buffer.append(tempBuffer, readSize); + Buffer.insert(Buffer.end(), tempBuffer, tempBuffer + readSize); } co_return result; @@ -160,7 +188,7 @@ struct TByteReader { private: TSocket& Socket; - std::string Buffer; + std::deque Buffer; }; /** diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 559687d..93a2894 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -14,4 +14,5 @@ target(bench bench.cpp) target(wsclient wsclient.cpp) target(bench_actors bench_actors.cpp) target(ping_actors ping_actors.cpp) -target(behavior_actors behavior_actors.cpp) \ No newline at end of file +target(behavior_actors behavior_actors.cpp) +target(hello_httpd hello_httpd.cpp) \ No newline at end of file diff --git a/examples/hello_httpd.cpp b/examples/hello_httpd.cpp new file mode 100644 index 0000000..8e965d0 --- /dev/null +++ b/examples/hello_httpd.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include + +using namespace NNet; + +int main(int argc, char** argv) { + NNet::TInitializer init; + int port = 8080; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--port") && i < argc-1) { + port = atoi(argv[++i]); + } else if (!strcmp(argv[i], "--help")) { + std::cout << "Usage: " << argv[0] << " [--port port]\n"; + return 0; + } + } + + TAddress address{"::", port}; + std::cerr << "Starting HTTP server on port " << port << "\n"; + + auto logger = [](const std::string& msg) { + std::cout << "[HTTPD] " << msg << std::endl; + }; + + using TPoller = TDefaultPoller; + using TSocket = typename TPoller::TSocket; + TLoop loop; + TSocket listenSocket(loop.Poller(), address.Domain()); + listenSocket.Bind(address); + listenSocket.Listen(); + std::cerr << "Listening on: " << listenSocket.LocalAddr()->ToString() << std::endl; + + THelloWorldRouter router; + TWebServer server(std::move(listenSocket), router, logger); + server.Start(); + + loop.Loop(); + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9dc116d..a6e8a74 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,4 +14,5 @@ endmacro() if (CMOCKA_FOUND) ut(tests tests.cpp) ut(test_actors test_actors.cpp) +ut(test_http test_http.cpp) endif () diff --git a/tests/test_http.cpp b/tests/test_http.cpp new file mode 100644 index 0000000..1c1f054 --- /dev/null +++ b/tests/test_http.cpp @@ -0,0 +1,308 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "testlib.h" +#include "perf.h" + +extern "C" { +#include +} + +using namespace NNet; + +void test_http_uri_parsing(void**) { + { + NNet::TUri uri("/path/to/resource?param1=value1¶m2=value2#fragment"); + assert_string_equal(uri.Path().c_str(), "/path/to/resource"); + auto params = uri.QueryParameters(); + assert_true(params.size() == 2); + assert_string_equal(params["param1"].c_str(), "value1"); + assert_string_equal(params["param2"].c_str(), "value2"); + assert_string_equal(uri.Fragment().c_str(), "fragment"); + } + { + NNet::TUri uri("/simple/path"); + assert_string_equal(uri.Path().c_str(), "/simple/path"); + auto params = uri.QueryParameters(); + assert_true(params.empty()); + assert_string_equal(uri.Fragment().c_str(), ""); + } + { + NNet::TUri uri("/path/with/fragment#onlyfragment"); + assert_string_equal(uri.Path().c_str(), "/path/with/fragment"); + auto params = uri.QueryParameters(); + assert_true(params.empty()); + assert_string_equal(uri.Fragment().c_str(), "onlyfragment"); + } + // encoded variants + { + NNet::TUri uri("/path%20with%20spaces?param%201=value%201#frag%20ment"); + assert_string_equal(uri.Path().c_str(), "/path with spaces"); + auto params = uri.QueryParameters(); + assert_true(params.size() == 1); + assert_string_equal(params["param 1"].c_str(), "value 1"); + assert_string_equal(uri.Fragment().c_str(), "frag ment"); + } +} + +void test_http_request_handling_basic(void**) { + // Basic request parsing test + std::string rawRequest = + "GET /test/path?arg=value#frag HTTP/1.1\r\n" + "Host: localhost\r\n" + "User-Agent: TestClient/1.0\r\n" + "Content-Length: 11\r\n" + "\r\n" + "Hello World"; + + std::vector requestData(rawRequest.begin(), rawRequest.end()); + size_t headerEndPos = rawRequest.find("\r\n\r\n") + 4; + std::string headerPart(rawRequest.begin(), rawRequest.begin() + headerEndPos); + + auto bodyReader = [requestData, headerEndPos](char* buffer, size_t size) -> TFuture { + size_t bodyStart = headerEndPos; + size_t bodySize = requestData.size() - bodyStart; + size_t toRead = std::min(size, bodySize); + std::memcpy(buffer, requestData.data() + bodyStart, toRead); + co_return toRead; + }; + + TRequest request(std::move(headerPart), bodyReader); + + assert_true(request.Method() == "GET"); + assert_string_equal(request.Uri().Path().c_str(), "/test/path"); + auto params = request.Uri().QueryParameters(); + assert_true(params.size() == 1); + assert_string_equal(params["arg"].c_str(), "value"); + + std::string body; + [](TRequest& request, std::string& body) -> TVoidTask { + body = co_await request.ReadBodyFull(); + } (request, body); + assert_string_equal(body.c_str(), "Hello World"); +} + +void test_http_request_handling_advanced(void**) { + // part of body in raw data, rest via body reader + std::string rawRequestPart = + "POST /submit/data HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 23\r\n" + "\r\n" + "PartialBody" + "MoreBodyData"; + + std::vector requestData(rawRequestPart.begin(), rawRequestPart.end()); + size_t headerEndPos = rawRequestPart.find("\r\n\r\n") + 4; + std::string headerPart(rawRequestPart.begin(), rawRequestPart.begin() + headerEndPos); + + auto bodyReader = [rawRequestPart, headerEndPos](char* buffer, size_t size) -> TFuture { + size_t bodyStart = headerEndPos; + size_t bodySize = rawRequestPart.size() - bodyStart; + size_t toRead = std::min(size, bodySize); + std::memcpy(buffer, rawRequestPart.data() + bodyStart, toRead); + co_return toRead; + }; + + TRequest request(std::move(headerPart), bodyReader); + + assert_true(request.Method() == "POST"); + assert_string_equal(request.Uri().Path().c_str(), "/submit/data"); + auto params = request.Uri().QueryParameters(); + assert_true(params.empty()); + + std::string body; + [](TRequest& request, std::string& body) -> TVoidTask { + try { + body = co_await request.ReadBodyFull(); + } catch (const std::exception& e) { + std::cerr << "Error reading body: " << e.what() << "\n"; + } + } (request, body); + assert_string_equal(body.c_str(), "PartialBodyMoreBodyData"); +} + +void test_http_request_handling_2requests_in_1buffer(void**) { + // Two requests in one buffer + std::string rawRequests = + "GET /first HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 5\r\n" + "\r\n" + "Hello" + "POST /second HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 5\r\n" + "\r\n" + "Hello"; + + std::vector requestData(rawRequests.begin(), rawRequests.end()); + // Check parse of first request + size_t firstHeaderEndPos = rawRequests.find("\r\n\r\n") + 4; + std::string firstHeaderPart(rawRequests.begin(), rawRequests.begin() + firstHeaderEndPos); + + auto firstBodyReader = [requestData, firstHeaderEndPos](char* buffer, size_t size) -> TFuture { + size_t bodyStart = firstHeaderEndPos; + size_t bodySize = 5; // known from Content-Length + size_t toRead = std::min(size, bodySize); + std::memcpy(buffer, requestData.data() + bodyStart, toRead); + co_return toRead; + }; + TRequest firstRequest(std::move(firstHeaderPart), firstBodyReader); + assert_true(firstRequest.Method() == "GET"); + assert_string_equal(firstRequest.Uri().Path().c_str(), "/first"); + std::string firstBody; + [](TRequest& request, std::string& body) -> TVoidTask { + body = co_await request.ReadBodyFull(); + } (firstRequest, firstBody); + assert_string_equal(firstBody.c_str(), "Hello"); +} + +void test_http_request_handling_chunked_body(void**) { + // Chunked transfer encoding request + std::string rawRequest = + "POST /chunked HTTP/1.1\r\n" + "Host: localhost\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\n" + "Hello\r\n" + "6\r\n" + " World\r\n" + "0\r\n" + "\r\n"; + + std::vector requestData(rawRequest.begin(), rawRequest.end()); + size_t headerEndPos = rawRequest.find("\r\n\r\n") + 4; + std::string headerPart(rawRequest.begin(), rawRequest.begin() + headerEndPos); + size_t bodyStartPos = headerEndPos; + std::string bodyPart(rawRequest.begin() + bodyStartPos, rawRequest.end()); + + auto bodyPos = 0; + + auto bodyReader = [&bodyPart, &bodyPos](char* buffer, size_t size) -> TFuture { + size_t bodySize = bodyPart.size() - bodyPos; + size_t toRead = std::min(size, bodySize); + std::memcpy(buffer, bodyPart.data() + bodyPos, toRead); + bodyPos += toRead; + co_return toRead; + }; + + auto chunkHeaderReader = [&bodyPart, &bodyPos]() -> TFuture { + // read until \r\n from bodyPart + std::string result; + auto oldPos = bodyPos; + bodyPos = bodyPart.find("\r\n", oldPos); + if (bodyPos != std::string::npos) { + result = bodyPart.substr(oldPos, bodyPos - oldPos + 2); + } + bodyPos += 2; + co_return result; + }; + + TRequest request(std::move(headerPart), bodyReader, chunkHeaderReader); + assert_true(request.Method() == "POST"); + assert_string_equal(request.Uri().Path().c_str(), "/chunked"); + + std::string body; + [](TRequest& request, std::string& body) -> TVoidTask { + try { + body = co_await request.ReadBodyFull(); + } catch (const std::exception& e) { + std::cerr << "Error reading request body: " << e.what() << std::endl; + } + } (request, body); + assert_string_equal(body.c_str(), "Hello World"); +} + +void test_http_response_handling_basic(void**) { + std::string responseData; + auto writer = [&](const void* data, size_t size) -> TFuture { + std::string str(static_cast(data), size); + responseData += str; + co_return size; + }; + + TResponse response(writer); + response.SetStatus(200); + response.SetHeader("Content-Type", "text/plain"); + response.SetHeader("Connection", "close"); + + [](TResponse& response) -> TVoidTask { + co_await response.SendHeaders(); + co_await response.WriteBodyFull("Hello, World!"); + } (response); + + std::string expectedResponse = + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "Hello, World!"; + assert_string_equal(responseData.c_str(), expectedResponse.c_str()); +} + +void test_http_response_handling_chunked(void**) { + std::string responseData; + auto writer = [&](const void* data, size_t size) -> TFuture { + std::string str(static_cast(data), size); + responseData += str; + co_return size; + }; + + TResponse response(writer); + response.SetStatus(200); + response.SetHeader("Content-Type", "text/plain"); + response.SetHeader("Transfer-Encoding", "chunked"); + + [](TResponse& response) -> TVoidTask { + co_await response.SendHeaders(); + co_await response.WriteBodyChunk("Hello, ", 7); + co_await response.WriteBodyChunk("World!", 6); + co_await response.WriteBodyChunk("", 0); // final chunk + } (response); + + std::string expectedResponse = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "7\r\n" + "Hello, \r\n" + "6\r\n" + "World!\r\n" + "0\r\n" + "\r\n"; + assert_string_equal(responseData.c_str(), expectedResponse.c_str()); +} + +int main(int argc, char** argv) { + TInitializer init; + + std::vector tests; + std::unordered_set filters; + tests.reserve(500); + + parse_filters(argc, argv, filters); + + ADD_TEST(cmocka_unit_test, test_http_uri_parsing); + ADD_TEST(cmocka_unit_test, test_http_request_handling_basic); + ADD_TEST(cmocka_unit_test, test_http_request_handling_advanced); + ADD_TEST(cmocka_unit_test, test_http_request_handling_2requests_in_1buffer); + ADD_TEST(cmocka_unit_test, test_http_request_handling_chunked_body); + ADD_TEST(cmocka_unit_test, test_http_response_handling_basic); + ADD_TEST(cmocka_unit_test, test_http_response_handling_chunked); + + return _cmocka_run_group_tests("test_http", tests.data(), tests.size(), NULL, NULL); +} \ No newline at end of file