From 9d0a5193a523f7c02ec29c03be4efc98e9730998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Tue, 30 Sep 2025 18:34:14 +0400 Subject: [PATCH] feat(http): implement HTTP server with CLI, request, response, and session handling --- src/http/http_cli.cpp | 34 ++++++++++++ src/http/http_cli.hpp | 19 +++++++ src/http/http_request.cpp | 70 +++++++++++++++++++++++ src/http/http_request.hpp | 34 ++++++++++++ src/http/http_request_parser.cpp | 89 ++++++++++++++++++++++++++++++ src/http/http_request_parser.hpp | 16 ++++++ src/http/http_response.cpp | 95 ++++++++++++++++++++++++++++++++ src/http/http_response.hpp | 36 ++++++++++++ src/http/http_server.cpp | 15 +++++ src/http/http_server.hpp | 15 +++++ src/http/http_session.cpp | 61 ++++++++++++++++++++ src/http/http_session.hpp | 15 +++++ src/main/main_http.cpp | 29 ++++++++++ 13 files changed, 528 insertions(+) create mode 100644 src/http/http_cli.cpp create mode 100644 src/http/http_cli.hpp create mode 100644 src/http/http_request.cpp create mode 100644 src/http/http_request.hpp create mode 100644 src/http/http_request_parser.cpp create mode 100644 src/http/http_request_parser.hpp create mode 100644 src/http/http_response.cpp create mode 100644 src/http/http_response.hpp create mode 100644 src/http/http_server.cpp create mode 100644 src/http/http_server.hpp create mode 100644 src/http/http_session.cpp create mode 100644 src/http/http_session.hpp create mode 100644 src/main/main_http.cpp diff --git a/src/http/http_cli.cpp b/src/http/http_cli.cpp new file mode 100644 index 0000000..708bd0e --- /dev/null +++ b/src/http/http_cli.cpp @@ -0,0 +1,34 @@ +#include "http_cli.hpp" + +// helper for default values +constexpr auto withDefault = [](auto value) { + using T = decltype(value); + return boost::program_options::value()->default_value(value); +}; + +HttpCli::HttpCli() + : CliBase("HTTP Server Options") + , port_(8080) + , rootDir_("./www") +{ +} + +void HttpCli::setupOptions() +{ + // clang-format off + desc_.add_options() + ("help,h", "Show help message") + ("port,p", withDefault(8080), "Port to listen on") + ("root,r", withDefault(std::string("./www")), "Root directory for files"); + // clang-format on +} + +int HttpCli::getPort() const +{ + return vm_["port"].as(); +} + +std::string HttpCli::getRootDir() const +{ + return vm_["root"].as(); +} diff --git a/src/http/http_cli.hpp b/src/http/http_cli.hpp new file mode 100644 index 0000000..432201f --- /dev/null +++ b/src/http/http_cli.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "cli_base.hpp" + +class HttpCli : public CliBase +{ + public: + HttpCli(); + + int getPort() const; + std::string getRootDir() const; + + protected: + void setupOptions() override; + + private: + int port_; + std::string rootDir_; +}; diff --git a/src/http/http_request.cpp b/src/http/http_request.cpp new file mode 100644 index 0000000..e5ff7c4 --- /dev/null +++ b/src/http/http_request.cpp @@ -0,0 +1,70 @@ +#include "http_request.hpp" + +#include + +void HttpRequest::setMethod(const std::string_view method) +{ + method_ = method; +} + +const std::string HttpRequest::getMethod() const +{ + return method_; +} + +void HttpRequest::setPath(const std::string_view path) +{ + path_ = path; +} + +const std::string HttpRequest::getPath() const +{ + return path_; +} + +void HttpRequest::setVersion(const std::string_view version) +{ + version_ = version; +} + +const std::string HttpRequest::getVersion() const +{ + return version_; +} + +void HttpRequest::addHeader( + const std::string_view key, const std::string_view value +) +{ + headers_[std::string(key)] = std::string(value); +} + +bool HttpRequest::hasHeader(const std::string_view key) const +{ + return headers_.find(std::string(key)) != headers_.end(); +} + +std::string HttpRequest::getHeader(const std::string_view key) const +{ + auto it = headers_.find(std::string(key)); + if (it == headers_.end()) { + throw std::runtime_error("Header not found: " + std::string(key)); + } + return it->second; +} + +const std::unordered_map & +HttpRequest::getHeaders() const +{ + return headers_; +} + +void HttpRequest::setBody(const std::string_view body) +{ + body_ = body; +} + +const std::string HttpRequest::getBody() const +{ + return body_; +} diff --git a/src/http/http_request.hpp b/src/http/http_request.hpp new file mode 100644 index 0000000..0c0c632 --- /dev/null +++ b/src/http/http_request.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +class HttpRequest +{ + public: + HttpRequest() = default; + + void setMethod(const std::string_view method); + const std::string getMethod() const; + + void setPath(const std::string_view path); + const std::string getPath() const; + + void setVersion(const std::string_view version); + const std::string getVersion() const; + + void addHeader(const std::string_view key, const std::string_view value); + bool hasHeader(const std::string_view key) const; + std::string getHeader(const std::string_view key) const; + const std::unordered_map &getHeaders() const; + + void setBody(const std::string_view body); + const std::string getBody() const; + + private: + std::string method_; // GET, POST + std::string path_; // /index.html + std::string version_; // HTTP/1.1 + std::unordered_map headers_; + std::string body_; +}; diff --git a/src/http/http_request_parser.cpp b/src/http/http_request_parser.cpp new file mode 100644 index 0000000..1331b04 --- /dev/null +++ b/src/http/http_request_parser.cpp @@ -0,0 +1,89 @@ +#include "http_request_parser.hpp" + +#include +#include +#include + +HttpRequest HttpRequestParser::parse(const std::string &raw) +{ + std::istringstream stream(raw); + HttpRequest request; + + std::string line; + if (!std::getline(stream, line)) { + throw std::runtime_error("Invalid HTTP request: empty"); + } + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + + parseRequestLine(line, request); + parseHeaders(stream, request); + parseBody(stream, request); + + return request; +} + +// parse "GET /index.html HTTP/1.1" +void HttpRequestParser::parseRequestLine( + const std::string &line, HttpRequest &req +) +{ + std::istringstream requestLine(line); + std::string method, path, version; + requestLine >> method >> path >> version; + + if (method.empty() || path.empty() || version.empty()) { + throw std::runtime_error("Invalid HTTP request line"); + } + + req.setMethod(method); + req.setPath(path); + req.setVersion(version); +} + +// parse headers until empty line +void HttpRequestParser::parseHeaders( + std::istringstream &stream, HttpRequest &req +) +{ + std::string line; + while (std::getline(stream, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (line.empty()) { + break; // end of headers + } + parseHeaderLine(line, req); + } +} + +// parse single "Key: Value" header +void HttpRequestParser::parseHeaderLine( + const std::string &line, HttpRequest &req +) +{ + auto colon = line.find(':'); + if (colon == std::string::npos) { + return; // skip invalid header + } + + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + + boost::algorithm::trim(key); + boost::algorithm::trim(value); + + req.addHeader(key, value); +} + +// parse optional body after headers +void HttpRequestParser::parseBody(std::istringstream &stream, HttpRequest &req) +{ + std::string body; + std::getline(stream, body, '\0'); + if (!body.empty()) { + req.setBody(body); + } +} diff --git a/src/http/http_request_parser.hpp b/src/http/http_request_parser.hpp new file mode 100644 index 0000000..843fb2c --- /dev/null +++ b/src/http/http_request_parser.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "http_request.hpp" +#include + +class HttpRequestParser +{ + public: + static HttpRequest parse(const std::string &raw); + + private: + static void parseRequestLine(const std::string &line, HttpRequest &req); + static void parseHeaderLine(const std::string &line, HttpRequest &req); + static void parseHeaders(std::istringstream &stream, HttpRequest &req); + static void parseBody(std::istringstream &stream, HttpRequest &req); +}; diff --git a/src/http/http_response.cpp b/src/http/http_response.cpp new file mode 100644 index 0000000..070fb0a --- /dev/null +++ b/src/http/http_response.cpp @@ -0,0 +1,95 @@ +#include "http_response.hpp" + +#include +#include + +HttpResponse::HttpResponse() + : statusCode_(200) + , reasonPhrase_("OK") + , version_("HTTP/1.1") +{ +} + +void HttpResponse::setStatusCode(int code) +{ + statusCode_ = code; +} + +int HttpResponse::getStatusCode() const +{ + return statusCode_; +} + +void HttpResponse::setReasonPhrase(const std::string_view phrase) +{ + reasonPhrase_ = phrase; +} + +const std::string_view HttpResponse::getReasonPhrase() const +{ + return reasonPhrase_; +} + +void HttpResponse::setVersion(const std::string_view version) +{ + version_ = version; +} + +const std::string_view HttpResponse::getVersion() const +{ + return version_; +} + +void HttpResponse::addHeader( + const std::string_view key, const std::string_view value +) +{ + headers_[std::string(key)] = std::string(value); +} + +bool HttpResponse::hasHeader(const std::string_view key) const +{ + return headers_.find(std::string(key)) != headers_.end(); +} + +std::string HttpResponse::getHeader(const std::string_view key) const +{ + auto it = headers_.find(std::string(key)); + if (it == headers_.end()) { + throw std::runtime_error("Header not found: " + std::string(key)); + } + return it->second; +} + +const std::unordered_map & +HttpResponse::getHeaders() const +{ + return headers_; +} + +void HttpResponse::setBody(const std::string_view body) +{ + body_ = body; +} + +const std::string_view HttpResponse::getBody() const +{ + return body_; +} + +std::string HttpResponse::toString() const +{ + std::ostringstream oss; + + oss << version_ << " " << statusCode_ << " " << reasonPhrase_ << "\r\n"; + + for (const auto &[key, value] : headers_) { + oss << key << ": " << value << "\r\n"; + } + + oss << "\r\n"; + + oss << body_; + + return oss.str(); +} diff --git a/src/http/http_response.hpp b/src/http/http_response.hpp new file mode 100644 index 0000000..4fee4d2 --- /dev/null +++ b/src/http/http_response.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class HttpResponse +{ + public: + HttpResponse(); + + void setStatusCode(int code); + int getStatusCode() const; + + void setReasonPhrase(const std::string_view phrase); + const std::string_view getReasonPhrase() const; + + void setVersion(const std::string_view version); + const std::string_view getVersion() const; + + void addHeader(const std::string_view key, const std::string_view value); + bool hasHeader(const std::string_view key) const; + std::string getHeader(const std::string_view key) const; + const std::unordered_map &getHeaders() const; + + void setBody(const std::string_view body); + const std::string_view getBody() const; + + std::string toString() const; + + private: + int statusCode_; + std::string reasonPhrase_; + std::string version_; + std::unordered_map headers_; + std::string body_; +}; diff --git a/src/http/http_server.cpp b/src/http/http_server.cpp new file mode 100644 index 0000000..a31278f --- /dev/null +++ b/src/http/http_server.cpp @@ -0,0 +1,15 @@ +#include "http_server.hpp" + +#include "http_server.hpp" +#include "http_session.hpp" + +HttpServer::HttpServer(IoContext &io, unsigned short port, std::string rootDir) + : TcpServerBase(io, port) + , rootDir_(std::move(rootDir)) +{ +} + +void HttpServer::createSession(TcpSocket socket) +{ + std::make_shared(std::move(socket), rootDir_)->start(); +} diff --git a/src/http/http_server.hpp b/src/http/http_server.hpp new file mode 100644 index 0000000..e19149f --- /dev/null +++ b/src/http/http_server.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "tcp_server_base.hpp" + +class HttpServer : public TcpServerBase +{ + public: + HttpServer(IoContext &io, unsigned short port, std::string rootDir); + + protected: + void createSession(TcpSocket socket) override; + + private: + std::string rootDir_; +}; diff --git a/src/http/http_session.cpp b/src/http/http_session.cpp new file mode 100644 index 0000000..bf4a864 --- /dev/null +++ b/src/http/http_session.cpp @@ -0,0 +1,61 @@ +#include "http_session.hpp" + +#include "session_base.hpp" +#include "http_request.hpp" +#include "http_request_parser.hpp" +#include "http_response.hpp" +#include "logger.hpp" +#include + +HttpSession::HttpSession(TcpSocket socket, std::string rootDir) + : SessionBase(std::move(socket)) + , rootDir_(std::move(rootDir)) +{ +} + +void HttpSession::handleRequest(const std::string_view rawRequest) +{ + HttpRequest req = HttpRequestParser::parse(std::string(rawRequest)); + + HttpResponse res; + + if (req.getPath().rfind("/file/", 0) == 0) { + // remove "/file/" prefix + std::string filePath = rootDir_ + "/" + req.getPath().substr(6); + + std::ifstream file(filePath, std::ios::binary); + if (file) { + std::ostringstream ss; + ss << file.rdbuf(); + + std::string content = ss.str(); + res.setStatusCode(200); + res.setReasonPhrase("OK"); + res.setBody(content); + res.addHeader("Content-Type", "text/plain"); + res.addHeader("Content-Length", std::to_string(content.size())); + } else { + res.setStatusCode(404); + res.setReasonPhrase("Not Found"); + res.setBody("File not found"); + res.addHeader("Content-Type", "text/plain"); + res.addHeader( + "Content-Length", std::to_string(res.getBody().size()) + ); + } + } else { + res.setStatusCode(200); + res.setReasonPhrase("OK"); + res.setBody("Hello, World!"); + res.addHeader("Content-Type", "text/plain"); + res.addHeader("Content-Length", std::to_string(res.getBody().size())); + } + + std::string responseStr = res.toString(); + doWrite(responseStr); + + Logger::info( + req.getMethod() + " " + req.getPath() + " -> " + + std::to_string(res.getStatusCode()) + ); +} diff --git a/src/http/http_session.hpp b/src/http/http_session.hpp new file mode 100644 index 0000000..a8fea53 --- /dev/null +++ b/src/http/http_session.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "session_base.hpp" + +class HttpSession : public SessionBase +{ + public: + HttpSession(TcpSocket socket, std::string rootDir); + + protected: + void handleRequest(const std::string_view rawRequest) override; + + private: + std::string rootDir_; +}; diff --git a/src/main/main_http.cpp b/src/main/main_http.cpp new file mode 100644 index 0000000..bdad892 --- /dev/null +++ b/src/main/main_http.cpp @@ -0,0 +1,29 @@ +#include "http_cli.hpp" +#include "http_server.hpp" +#include "logger.hpp" + +int main(int argc, char **argv) +{ + HttpCli cli; + cli.parse(argc, argv); + + if (cli.isHelp()) { + cli.printHelp(); + return 0; + } + + Logger::init(true, "http_server.log"); + + IoContext io; + HttpServer server(io, cli.getPort(), cli.getRootDir()); + + Logger::info( + "Http server starting on port " + std::to_string(cli.getPort()) + + ", root dir: " + cli.getRootDir() + ); + + server.start(); + io.run(); + + return 0; +}