feat(http): implement HTTP server with CLI, request, response, and session

handling
This commit is contained in:
Ваше Имя
2025-09-30 18:34:14 +04:00
parent ba91359db3
commit 9d0a5193a5
13 changed files with 528 additions and 0 deletions
+34
View File
@@ -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<T>()->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<int>();
}
std::string HttpCli::getRootDir() const
{
return vm_["root"].as<std::string>();
}
+19
View File
@@ -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_;
};
+70
View File
@@ -0,0 +1,70 @@
#include "http_request.hpp"
#include <stdexcept>
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<std::string, std::string> &
HttpRequest::getHeaders() const
{
return headers_;
}
void HttpRequest::setBody(const std::string_view body)
{
body_ = body;
}
const std::string HttpRequest::getBody() const
{
return body_;
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <unordered_map>
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<std::string, std::string> &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<std::string, std::string> headers_;
std::string body_;
};
+89
View File
@@ -0,0 +1,89 @@
#include "http_request_parser.hpp"
#include <boost/algorithm/string/trim.hpp>
#include <sstream>
#include <stdexcept>
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);
}
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "http_request.hpp"
#include <string>
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);
};
+95
View File
@@ -0,0 +1,95 @@
#include "http_response.hpp"
#include <sstream>
#include <stdexcept>
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<std::string, std::string> &
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();
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <unordered_map>
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<std::string, std::string> &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<std::string, std::string> headers_;
std::string body_;
};
+15
View File
@@ -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<HttpSession>(std::move(socket), rootDir_)->start();
}
+15
View File
@@ -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_;
};
+61
View File
@@ -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 <fstream>
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())
);
}
+15
View File
@@ -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_;
};
+29
View File
@@ -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;
}