From 221cc710a8abe6b4a005e9f3ec8088c0afd550b4 Mon Sep 17 00:00:00 2001 From: Quinten Mennen Date: Tue, 23 Sep 2025 21:00:26 +0200 Subject: [PATCH] feat(http): add headers class and allow for chunked data --- webserv/http/HttpHeaders.cpp | 80 ++++++++++++++++++ webserv/http/HttpHeaders.hpp | 42 ++++++++++ webserv/http/HttpRequest.cpp | 157 +++++++++++++++++++++++------------ webserv/http/HttpRequest.hpp | 24 ++++-- 4 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 webserv/http/HttpHeaders.cpp create mode 100644 webserv/http/HttpHeaders.hpp diff --git a/webserv/http/HttpHeaders.cpp b/webserv/http/HttpHeaders.cpp new file mode 100644 index 0000000..91f3b8f --- /dev/null +++ b/webserv/http/HttpHeaders.cpp @@ -0,0 +1,80 @@ +#include "webserv/http/HttpHeaders.hpp" + +#include "webserv/config/utils.hpp" +#include "webserv/http/HttpConstants.hpp" + +std::optional HttpHeaders::getContentLength() const +{ + const auto &value = get("Content-Length"); + if (value.empty()) + { + return std::nullopt; + } + try + { + return std::stoul(value); + } + catch (...) + { + return std::nullopt; + } +} + +void HttpHeaders::add(const std::string &name, const std::string &value) +{ + headers_[name] = value; +} + +void HttpHeaders::remove(const std::string &name) +{ + headers_.erase(name); +} + +const std::string &HttpHeaders::get(const std::string &name) const +{ + auto it = headers_.find(name); + if (it != headers_.end()) + { + return it->second; + } + static const std::string empty; + return empty; +} + +bool HttpHeaders::has(const std::string &name) const +{ + return headers_.contains(name); +} + +void HttpHeaders::parse(const std::string &rawHeaders) +{ + size_t start = 0; + size_t end = rawHeaders.find(Http::Protocol::CRLF); + + while (end != std::string::npos) + { + std::string line = rawHeaders.substr(start, end - start); + size_t col = line.find(':'); + if (col != std::string::npos) + { + std::string name = line.substr(0, col); + std::string value = line.substr(col + 1); + name = trim(name); + value = trim(value); + this->add(name, value); + } + start = end + Http::Protocol::CRLF.size(); + end = rawHeaders.find(Http::Protocol::CRLF, start); + } +} + +std::string HttpHeaders::toString() const +{ + std::string result; + for (const auto &pair : headers_) + { + result += pair.first + ": " + pair.second + "\r\n"; + } + result += "\r\n"; + return result; +} \ No newline at end of file diff --git a/webserv/http/HttpHeaders.hpp b/webserv/http/HttpHeaders.hpp new file mode 100644 index 0000000..865284f --- /dev/null +++ b/webserv/http/HttpHeaders.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +/** + * @file HttpHeaders.hpp + * @brief Declaration of the HttpHeaders class for managing HTTP headers. + * + * This class provides functionality to store, retrieve, and manage HTTP headers + * in a structured manner. + * + * Without this class the HttpRequest and Response classes would become too bloated, and we'd end up adding members + * to those classes for every new header we want to support. + */ +class HttpHeaders +{ + public: + HttpHeaders() = default; + + HttpHeaders(const HttpHeaders &other) = delete; + HttpHeaders(HttpHeaders &&other) noexcept = delete; + HttpHeaders &operator=(const HttpHeaders &other) = delete; + HttpHeaders &operator=(HttpHeaders &&other) noexcept = delete; + + ~HttpHeaders() = default; + + const std::string &get(const std::string &name) const; + bool has(const std::string &name) const; + + void parse(const std::string &rawHeaders); + void add(const std::string &name, const std::string &value); + void remove(const std::string &name); + + std::string toString() const; + + std::optional getContentLength() const; + + private: + std::unordered_map headers_; +}; \ No newline at end of file diff --git a/webserv/http/HttpRequest.cpp b/webserv/http/HttpRequest.cpp index 4316f7a..cf3cf08 100644 --- a/webserv/http/HttpRequest.cpp +++ b/webserv/http/HttpRequest.cpp @@ -4,24 +4,27 @@ #include #include +#include +#include + HttpRequest::HttpRequest(const ServerConfig *serverConfig, const Client *client) : serverConfig_(serverConfig), client_(client) { - Log::trace("HttpRequest constructor called"); + Log::trace(LOCATION); } HttpRequest::~HttpRequest() { - Log::trace("HttpRequest destructor called"); + Log::trace(LOCATION); } HttpRequest::State HttpRequest::getState() const { - Log::trace("HttpRequest::getState() called"); + Log::trace(LOCATION); return state_; } -const std::string &HttpRequest::getHeaders() const +const HttpHeaders &HttpRequest::getHeaders() const { return headers_; } @@ -31,39 +34,13 @@ const std::string &HttpRequest::getBody() const return body_; } -size_t HttpRequest::getContentLength() const -{ - return contentLength_; -} - void HttpRequest::receiveData(const char *data, size_t length) { - Log::trace("HttpRequest::receiveData() called"); + Log::trace(LOCATION); buffer_.append(data, length); parseBuffer(); } -void HttpRequest::parseContentLength() -{ - // Parse headers to find Content-Length - size_t pos = headers_.find(Http::Header::CONTENT_LENGTH); - if (pos != std::string::npos) - { - pos += Http::Header::CONTENT_LENGTH.size() + Http::Protocol::HEADER_SEPARATOR.size(); - while (pos < headers_.size() && (headers_[pos] == ' ' || headers_[pos] == '\t')) - { - ++pos; // Skip whitespace - } - size_t endPos = headers_.find(Http::Protocol::CRLF, pos); - std::string contentLengthValue = headers_.substr(pos, endPos - pos); - contentLength_ = std::stoul(contentLengthValue); - } - else - { - contentLength_ = 0; // No body expected - } -} - void HttpRequest::parseBuffer() { while (true) @@ -88,6 +65,12 @@ void HttpRequest::parseBuffer() return; // Wait for more data } break; + case State::Chunked: + if (!parseBufferforChunkedBody()) + { + return; // Wait for more data + } + break; case State::Complete: Log::debug("HttpRequest::parseBuffer() request is complete"); return; // Request is complete @@ -97,57 +80,125 @@ void HttpRequest::parseBuffer() bool HttpRequest::parseBufferforRequestLine() { - Log::trace("HttpRequest::parseBufferforRequestLine() called"); + Log::trace(LOCATION); size_t pos = buffer_.find(Http::Protocol::CRLF); if (pos == std::string::npos) { - Log::debug("HttpRequest::parseBuffer() in state RequestLine waiting for more data"); - return false; // Wait for more data + Log::debug("RequestLine waiting for more data : " + LOCATION); + return false; } - requestLine_ = buffer_.substr(0, pos); + std::string requestLine_ = buffer_.substr(0, pos); + buffer_.erase(0, pos + Http::Protocol::CRLF.size()); state_ = State::Headers; + std::vector parts; + std::string part; + std::stringstream ss(requestLine_); + while (ss >> part) + { + parts.push_back(part); + } + if (parts.size() != 3) + { + Log::warning("Invalid request line: " + requestLine_); + state_ = State::Complete; // Mark as complete to avoid further processing + return true; + } + method_ = parts[0]; + target_ = parts[1]; + httpVersion_ = parts[2]; + Log::debug("Parsed Request Line: Method=" + method_ + " Target=" + target_ + " Version=" + httpVersion_); return true; } bool HttpRequest::parseBufferforHeaders() { - Log::trace("HttpRequest::parseBufferforHeaders() called"); + Log::trace(LOCATION); size_t pos = buffer_.find(Http::Protocol::DOUBLE_CRLF); if (pos == std::string::npos) { - Log::debug("HttpRequest::parseBuffer() in state Headers waiting for more data"); + Log::debug("Headers waiting for more data: " + LOCATION); return false; // Wait for more data } - headers_ = buffer_.substr(0, pos + Http::Protocol::CRLF.size()); // Include the last \r\n + // headers_ = buffer_.substr(0, pos + Http::Protocol::CRLF.size()); // Include the last \r\n + headers_.parse(buffer_.substr(0, pos + Http::Protocol::CRLF.size())); buffer_.erase(0, pos + Http::Protocol::DOUBLE_CRLF.size()); - parseContentLength(); + // parseContentLength(); - if (contentLength_ > 0) + if (this->headers_.getContentLength() > 0) { state_ = State::Body; + return true; } - else + if (this->headers_.has("Transfer-Encoding") && this->headers_.get("Transfer-Encoding") == "chunked") { - Log::debug("HttpRequest::parseBuffer() in state Headers no body to read"); - state_ = State::Complete; - return false; // No body to read + state_ = State::Chunked; + return true; } + Log::debug("HttpRequest::parseBuffer() in state Headers no body to read"); + state_ = State::Complete; + return false; // No body to read +} + +bool HttpRequest::parseBufferforChunkedBody() +{ + Log::trace(LOCATION); + while (true) + { + size_t pos = buffer_.find(Http::Protocol::CRLF); + if (pos == std::string::npos) + { + Log::debug("Chunked body waiting for more data: " + LOCATION); + return false; + } + std::string chunkSizeStr = buffer_.substr(0, pos); + size_t chunkSize = 0; + try + { + chunkSize = std::stoul(chunkSizeStr, nullptr, 16); + } + catch (const std::exception &e) + { + Log::warning("Invalid chunk size: " + chunkSizeStr + " (" + e.what() + ")"); + state_ = State::Complete; // Mark as complete to avoid further processing + return true; + } + if (chunkSize == 0) + { + state_ = State::Complete; // Last chunk + buffer_.erase(0, pos + Http::Protocol::CRLF.size()); + return true; + } + // TODO: Copilot added this but I don't understand why it's needed + // if (buffer_.size() < pos + Http::Protocol::CRLF.size() + chunkSize + Http::Protocol::CRLF.size()) + // { + // Log::debug("Chunked body waiting for more data: " + LOCATION); + // return false; // Wait for more data + // } + body_ += buffer_.substr(pos + Http::Protocol::CRLF.size(), chunkSize); + buffer_.erase(0, pos + Http::Protocol::CRLF.size() + chunkSize + Http::Protocol::CRLF.size()); + } return true; } bool HttpRequest::parseBufferforBody() { - Log::trace("HttpRequest::parseBufferforBody() called"); - if (buffer_.size() < contentLength_) + if (!headers_.getContentLength().has_value()) { - Log::debug("HttpRequest::parseBuffer() in state Body waiting for more data"); + Log::warning("HttpRequest::parseBuffer() in state Body but no Content-Length header found"); + state_ = State::Complete; + return true; + } + Log::trace(LOCATION); + if (buffer_.size() < *headers_.getContentLength()) + { + Log::debug("Body waiting for more data: " + LOCATION); return false; // Wait for more data } - body_ = buffer_.substr(0, contentLength_); - buffer_.erase(0, contentLength_); + body_ = buffer_.substr(0, *headers_.getContentLength()); + buffer_.erase(0, *headers_.getContentLength()); state_ = State::Complete; return true; @@ -155,11 +206,11 @@ bool HttpRequest::parseBufferforBody() void HttpRequest::reset() { - Log::trace("HttpRequest::reset() called"); + Log::trace(LOCATION); state_ = State::RequestLine; buffer_.clear(); - requestLine_.clear(); - headers_.clear(); + // requestLine_.clear(); + // headers_.clear(); body_.clear(); - contentLength_ = 0; + // contentLength_ = 0; } diff --git a/webserv/http/HttpRequest.hpp b/webserv/http/HttpRequest.hpp index bb04ace..9e21b43 100644 --- a/webserv/http/HttpRequest.hpp +++ b/webserv/http/HttpRequest.hpp @@ -1,5 +1,7 @@ #pragma once +#include "webserv/http/HttpHeaders.hpp" + #include #include @@ -16,6 +18,7 @@ class HttpRequest RequestLine, Headers, Body, + Chunked, Complete }; @@ -28,9 +31,12 @@ class HttpRequest ~HttpRequest(); [[nodiscard]] State getState() const; - [[nodiscard]] const std::string &getHeaders() const; + [[nodiscard]] const HttpHeaders &getHeaders() const; [[nodiscard]] const std::string &getBody() const; - [[nodiscard]] size_t getContentLength() const; + + [[nodiscard]] const std::string &getMethod() const { return method_; } + [[nodiscard]] const std::string &getTarget() const { return target_; } + [[nodiscard]] const std::string &getHttpVersion() const { return httpVersion_; } void receiveData(const char *data, size_t length); void reset(); @@ -39,7 +45,9 @@ class HttpRequest void parseBuffer(); [[nodiscard]] bool parseBufferforRequestLine(); [[nodiscard]] bool parseBufferforHeaders(); + [[nodiscard]] bool parseHeaderLine(); [[nodiscard]] bool parseBufferforBody(); + [[nodiscard]] bool parseBufferforChunkedBody(); void parseContentLength(); @@ -48,10 +56,14 @@ class HttpRequest State state_ = State::RequestLine; - std::string buffer_; + HttpHeaders headers_; - std::string requestLine_; - std::string headers_; + std::string buffer_; std::string body_; - size_t contentLength_ = 0; + std::string method_; + std::string target_; + std::string httpVersion_; + // std::string requestLine_; + // std::string headers_; + // size_t contentLength_ = 0; };