7 Commits

Author SHA1 Message Date
Ваше Имя 091ef6c8bf chore(test): make test scripts executable 2025-09-30 19:28:31 +04:00
Ваше Имя cda2d9a0ff test: add DNS, File, and HTTP server test scripts 2025-09-30 19:04:44 +04:00
Ваше Имя c02ba25371 refactor(build): restructure into separate server targets
Update xmake.lua to split into HTTP, file, and DNS server targets with
individual source directories. Remove old main.cpp and add test.txt for new
functionality.
2025-09-30 18:57:16 +04:00
Ваше Имя 7dac7fb0de feat(dns): add DNS server with configurable port and IP via CLI 2025-09-30 18:55:58 +04:00
Ваше Имя 46574026fe feat(file server): implement file server with CLI options
Implement file server with CLI options for port and root directory, server
initializes sessions, session handles file requests, and main function
orchestrates setup and execution
2025-09-30 18:55:18 +04:00
Ваше Имя 9d0a5193a5 feat(http): implement HTTP server with CLI, request, response, and session
handling
2025-09-30 18:34:14 +04:00
Ваше Имя ba91359db3 feat(common): introduce common networking and CLI infrastructure 2025-09-30 18:33:06 +04:00
43 changed files with 1268 additions and 25 deletions
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
// --- Error handling ---
using ErrorCode = boost::system::error_code;
// --- IO Context ---
using IoContext = boost::asio::io_context;
// --- TCP ---
using TcpSocket = boost::asio::ip::tcp::socket;
using TcpAcceptor = boost::asio::ip::tcp::acceptor;
using TcpEndpoint = boost::asio::ip::tcp::endpoint;
// --- UDP (для DNS) ---
using UdpSocket = boost::asio::ip::udp::socket;
using UdpEndpoint = boost::asio::ip::udp::endpoint;
// --- Таймеры ---
using SteadyTimer = boost::asio::steady_timer;
// --- Program options ---
using OptionsDescription = boost::program_options::options_description;
using VariablesMap = boost::program_options::variables_map;
+34
View File
@@ -0,0 +1,34 @@
#include "cli_base.hpp"
#include <boost/program_options/parsers.hpp>
#include <iostream>
CliBase::CliBase(const std::string &title)
: desc_(title)
, help_(false)
{
}
void CliBase::parse(int argc, char *argv[])
{
using namespace boost::program_options;
setupOptions(); // must be first
parsed_options parsed = parse_command_line(argc, argv, desc_);
store(parsed, vm_);
notify(vm_);
if (vm_.count("help")) {
help_ = true;
}
}
bool CliBase::isHelp() const
{
return help_;
}
void CliBase::printHelp() const
{
std::cout << desc_ << "\n";
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>
#include <string>
class CliBase
{
public:
CliBase(const std::string &title);
virtual ~CliBase() = default;
// parse argc/argv
void parse(int argc, char *argv[]);
bool isHelp() const;
void printHelp() const;
protected:
// children add their specific options here
virtual void setupOptions() = 0;
boost::program_options::options_description desc_;
boost::program_options::variables_map vm_;
bool help_;
};
+63
View File
@@ -0,0 +1,63 @@
#include "logger.hpp"
#include <boost/date_time/posix_time/posix_time_io.hpp>
void Logger::init(bool toConsole, const std::string_view filePath)
{
using namespace boost::log;
using namespace boost::posix_time;
add_common_attributes();
if (toConsole) {
add_console_log(
std::clog,
keywords::format =
(expressions::stream
<< "["
<< expressions::attr<boost::posix_time::ptime>("TimeStamp")
<< "] "
<< "<" << trivial::severity << "> " << expressions::smessage)
);
}
if (!filePath.empty()) {
add_file_log(
keywords::file_name = filePath,
keywords::auto_flush = true,
keywords::format =
(expressions::stream
<< "["
<< expressions::attr<boost::posix_time::ptime>("TimeStamp")
<< "] "
<< "<" << trivial::severity << "> " << expressions::smessage)
);
}
core::get()->set_filter(trivial::severity >= trivial::info);
}
void Logger::info(const std::string_view msg)
{
BOOST_LOG_TRIVIAL(info) << msg;
}
void Logger::warn(const std::string_view msg)
{
BOOST_LOG_TRIVIAL(warning) << msg;
}
void Logger::error(const std::string_view msg)
{
BOOST_LOG_TRIVIAL(error) << msg;
}
void Logger::debug(const std::string_view msg)
{
BOOST_LOG_TRIVIAL(debug) << msg;
}
void Logger::fatal(const std::string_view msg)
{
BOOST_LOG_TRIVIAL(fatal) << msg;
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <boost/log/core.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/utility/setup/console.hpp>
#include <boost/log/utility/setup/file.hpp>
class Logger
{
public:
static void
init(bool toConsole = true, const std::string_view filePath = "");
static void info(const std::string_view msg);
static void warn(const std::string_view msg);
static void error(const std::string_view msg);
static void debug(const std::string_view msg);
static void fatal(const std::string_view msg);
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
class Server
{
public:
virtual ~Server() = default;
virtual void start() = 0;
virtual void stop() = 0;
};
+70
View File
@@ -0,0 +1,70 @@
#include "session_base.hpp"
#include "logger.hpp"
#include <boost/asio/write.hpp>
SessionBase::SessionBase(TcpSocket socket)
: socket_(std::move(socket))
{
}
void SessionBase::start()
{
doRead();
}
void SessionBase::doRead()
{
using namespace boost::asio;
auto self = shared_from_this();
auto handler = [this, self](ErrorCode ec, std::size_t bytesTransferred) {
onRead(ec, bytesTransferred);
};
socket_.async_read_some(buffer(buffer_), handler);
}
void SessionBase::onRead(ErrorCode ec, std::size_t bytesTransferred)
{
if (ec) {
Logger::error("Read error: " + ec.message());
return;
}
std::string rawRequest(buffer_.data(), bytesTransferred);
Logger::info("Received request:\n" + rawRequest);
try {
handleRequest(rawRequest);
} catch (const std::exception &ex) {
Logger::error(std::string("Request handling failed: ") + ex.what());
}
}
void SessionBase::doWrite(const std::string &response)
{
using namespace boost::asio;
auto self = shared_from_this();
auto onWriteHandler =
[this, self](ErrorCode ec, std::size_t /*bytesTransferred*/) {
if (ec) {
Logger::error("Write error: " + ec.message());
}
ErrorCode shutdownEc;
if (socket_.shutdown(TcpSocket::shutdown_both, shutdownEc)) {
Logger::warn("Shutdown error: " + shutdownEc.message());
}
ErrorCode closeEc;
if (socket_.close(closeEc)) {
Logger::warn("Close error: " + closeEc.message());
} else {
Logger::info("Connection closed");
}
};
async_write(socket_, buffer(response), onWriteHandler);
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "aliases.hpp"
#include <array>
#include <memory>
#include <string>
class SessionBase : public std::enable_shared_from_this<SessionBase>
{
public:
explicit SessionBase(TcpSocket socket);
virtual ~SessionBase() = default;
void start();
protected:
virtual void handleRequest(const std::string_view rawRequest) = 0;
void doRead();
void onRead(ErrorCode ec, std::size_t bytesTransferred);
void doWrite(const std::string &response);
TcpSocket socket_;
std::array<char, 8192> buffer_;
};
+23
View File
@@ -0,0 +1,23 @@
#include "tcp_server_base.hpp"
#include "logger.hpp"
TcpServerBase::TcpServerBase(IoContext &io, unsigned short port)
: acceptor_(io, TcpEndpoint(boost::asio::ip::tcp::v4(), port))
{
}
void TcpServerBase::start()
{
auto doAccept = [this]() {
acceptor_.async_accept([this](ErrorCode ec, TcpSocket socket) {
if (!ec) {
Logger::info("New client connected");
createSession(std::move(socket));
} else {
Logger::error("Accept error: " + ec.message());
}
start(); // continue accepting
});
};
doAccept();
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "aliases.hpp"
class TcpServerBase
{
public:
TcpServerBase(IoContext &io, unsigned short port);
virtual ~TcpServerBase() = default;
void start();
protected:
virtual void createSession(TcpSocket socket) = 0;
TcpAcceptor acceptor_;
};
+46
View File
@@ -0,0 +1,46 @@
#include "udp_server_base.hpp"
#include "logger.hpp"
UdpServerBase::UdpServerBase(IoContext &io, unsigned short port)
: socket_(io, UdpEndpoint(boost::asio::ip::udp::v4(), port))
{
}
void UdpServerBase::start()
{
doReceive();
}
void UdpServerBase::doReceive()
{
using namespace boost::asio;
auto onReceive = [this](ErrorCode ec, std::size_t bytesReceived) {
if (!ec && bytesReceived > 0) {
std::string_view data(buffer_.data(), bytesReceived);
handleRequest(data, remoteEndpoint_);
} else if (ec) {
Logger::error("UDP receive error: " + ec.message());
}
doReceive(); // continue listening
};
socket_.async_receive_from(buffer(buffer_), remoteEndpoint_, onReceive);
}
void UdpServerBase::doSend(
const std::string &response, const UdpEndpoint &target
)
{
using namespace boost::asio;
auto bufferToSend = std::make_shared<std::string>(response);
auto onSend = [bufferToSend](ErrorCode ec, std::size_t /*bytesSent*/) {
if (ec) {
Logger::error("UDP send error: " + ec.message());
}
};
socket_.async_send_to(buffer(*bufferToSend), target, onSend);
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "aliases.hpp"
class UdpServerBase
{
public:
UdpServerBase(IoContext &io, unsigned short port);
virtual ~UdpServerBase() = default;
void start();
protected:
virtual void
handleRequest(const std::string_view data, const UdpEndpoint &sender) = 0;
void doReceive();
void doSend(const std::string &response, const UdpEndpoint &target);
UdpSocket socket_;
std::array<char, 4096> buffer_;
UdpEndpoint remoteEndpoint_;
};
+34
View File
@@ -0,0 +1,34 @@
#include "dns_cli.hpp"
// helper for default values
constexpr auto withDefault = [](auto value) {
using T = decltype(value);
return boost::program_options::value<T>()->default_value(value);
};
DnsCli::DnsCli()
: CliBase("DNS Server Options")
, port_(5300)
, ip_("127.0.0.1")
{
}
void DnsCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help message")
("port,p", withDefault(5300), "UDP port to listen on")
("ip,i", withDefault(std::string("127.0.0.1")), "IP address to return in A record");
// clang-format on
}
int DnsCli::getPort() const
{
return vm_["port"].as<int>();
}
std::string DnsCli::getIp() const
{
return vm_["ip"].as<std::string>();
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "cli_base.hpp"
#include <string>
class DnsCli : public CliBase
{
public:
DnsCli();
int getPort() const;
std::string getIp() const;
protected:
void setupOptions() override;
private:
int port_;
std::string ip_;
};
+57
View File
@@ -0,0 +1,57 @@
#include "dns_server.hpp"
#include "logger.hpp"
DnsServer::DnsServer(IoContext &io, unsigned short port, std::string answerIp)
: UdpServerBase(io, port)
, answerIp_(std::move(answerIp))
{
}
void DnsServer::handleRequest(
const std::string_view data, const UdpEndpoint &sender
)
{
if (data.size() < 12) {
Logger::warn("Invalid DNS packet");
return;
}
// response buffer
std::string resp;
// copy ID
resp.append(data.substr(0, 2));
// flags: QR=1 (response), AA=1
resp.push_back('\x81');
resp.push_back('\x80');
// QDCOUNT=1, ANCOUNT=1, NSCOUNT=0, ARCOUNT=0
resp.append("\0\1\0\1\0\0\0\0", 8);
// copy question (from offset 12 till end of query)
resp.append(data.substr(12));
// answer section
resp.append("\xc0\x0c"); // name pointer to question
resp.append("\0\1"); // TYPE A
resp.append("\0\1"); // CLASS IN
resp.append("\0\0\0\x3c"); // TTL = 60
resp.append("\0\x04"); // RDLENGTH = 4
// answer IP (from CLI or default 127.0.0.1)
unsigned int b1, b2, b3, b4;
if (sscanf(answerIp_.c_str(), "%u.%u.%u.%u", &b1, &b2, &b3, &b4) != 4) {
b1 = 127;
b2 = 0;
b3 = 0;
b4 = 1;
}
resp.push_back(static_cast<char>(b1));
resp.push_back(static_cast<char>(b2));
resp.push_back(static_cast<char>(b3));
resp.push_back(static_cast<char>(b4));
Logger::info("DNS query answered with A " + answerIp_);
doSend(resp, sender);
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "udp_server_base.hpp"
#include <string>
class DnsServer : public UdpServerBase
{
public:
DnsServer(IoContext &io, unsigned short port, std::string answerIp);
protected:
void handleRequest(
const std::string_view data, const UdpEndpoint &sender
) override;
private:
std::string answerIp_;
};
+34
View File
@@ -0,0 +1,34 @@
#include "file_cli.hpp"
// helper for default values
constexpr auto withDefault = [](auto value) {
using T = decltype(value);
return boost::program_options::value<T>()->default_value(value);
};
FileCli::FileCli()
: CliBase("File Server Options")
, port_(9090)
, rootDir_("./data")
{
}
void FileCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help message")
("port,p", withDefault(9090), "Port to listen on")
("root,r", withDefault(std::string("./data")), "Root directory for files");
// clang-format on
}
int FileCli::getPort() const
{
return vm_["port"].as<int>();
}
std::string FileCli::getRootDir() const
{
return vm_["root"].as<std::string>();
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "cli_base.hpp"
class FileCli : public CliBase
{
public:
FileCli();
int getPort() const;
std::string getRootDir() const;
protected:
void setupOptions() override;
private:
int port_;
std::string rootDir_;
};
+13
View File
@@ -0,0 +1,13 @@
#include "file_server.hpp"
#include "file_session.hpp"
FileServer::FileServer(IoContext &io, unsigned short port, std::string rootDir)
: TcpServerBase(io, port)
, rootDir_(std::move(rootDir))
{
}
void FileServer::createSession(TcpSocket socket)
{
std::make_shared<FileSession>(std::move(socket), rootDir_)->start();
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "tcp_server_base.hpp"
#include <string>
class FileServer : public TcpServerBase
{
public:
FileServer(IoContext &io, unsigned short port, std::string rootDir);
protected:
void createSession(TcpSocket socket) override;
private:
std::string rootDir_;
};
+35
View File
@@ -0,0 +1,35 @@
#include "file_session.hpp"
#include "session_base.hpp"
#include "logger.hpp"
#include <fstream>
#include <sstream>
FileSession::FileSession(TcpSocket socket, std::string rootDir)
: SessionBase(std::move(socket))
, rootDir_(std::move(rootDir))
{
}
void FileSession::handleRequest(const std::string_view rawRequest)
{
// strip trailing newlines
std::string fileName(rawRequest);
while (!fileName.empty()
&& (fileName.back() == '\r' || fileName.back() == '\n')) {
fileName.pop_back();
}
std::string filePath = rootDir_ + "/" + fileName;
std::ifstream file(filePath, std::ios::binary);
std::ostringstream ss;
if (file) {
ss << file.rdbuf();
Logger::info("File served: " + filePath);
} else {
ss << "File not found: " << fileName;
Logger::warn("File not found: " + filePath);
}
doWrite(ss.str());
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "session_base.hpp"
class FileSession : public SessionBase
{
public:
FileSession(TcpSocket socket, std::string rootDir);
protected:
void handleRequest(const std::string_view rawRequest) override;
private:
std::string rootDir_;
};
+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_;
};
-16
View File
@@ -1,16 +0,0 @@
#include <boost/asio.hpp>
#include <iostream>
int main()
{
std::cout << "Hello, Boost + C++23 + xmake!\n";
boost::asio::io_context io;
boost::asio::steady_timer timer(io, std::chrono::seconds(1));
timer.async_wait([](const boost::system::error_code &) {
std::cout << "Timer fired after 1 second!\n";
});
io.run();
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
#include "dns_cli.hpp"
#include "dns_server.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
DnsCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true, "dns_server.log");
Logger::info(
"DNS server starting on port " + std::to_string(cli.getPort())
+ ", answering with IP " + cli.getIp()
);
IoContext io;
DnsServer server(io, cli.getPort(), cli.getIp());
server.start();
io.run();
}
+27
View File
@@ -0,0 +1,27 @@
#include "file_cli.hpp"
#include "file_server.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
FileCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true, "file_server.log");
IoContext io;
FileServer server(io, cli.getPort(), cli.getRootDir());
Logger::info(
"File server starting on port " + std::to_string(cli.getPort())
+ ", root dir: " + cli.getRootDir()
);
server.start();
io.run();
}
+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;
}
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
# Test DNS server on port 5300
dig @127.0.0.1 -p 5300 example.com
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Test File server on port 9090
# Send file name "test.txt" to server and print response
echo "test.txt" | nc 127.0.0.1 9090
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
# Test HTTP server on port 8080
curl -v http://127.0.0.1:8080/
+1
View File
@@ -0,0 +1 @@
Hello from file!
+29 -8
View File
@@ -2,16 +2,37 @@ set_languages "c++23"
add_rules("plugin.compile_commands.autoupdate", { outputdir = "." })
add_requires("boost", {
system = true, -- sudo pacman -S boost
add_requires("cmake::Boost", {
alias = "boost",
system = true,
configs = {
all = false,
system = true, -- asio
components = { "program_options", "log" },
},
})
target "app"
-- HTTP server target
target "http_server"
do
set_kind "binary"
add_files "src/*.cpp"
add_links "boost_system"
add_files("src/main/main_http.cpp", "src/http/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/http", "src/common", { public = true })
add_packages "boost"
end
-- File server target
target "file_server"
do
set_kind "binary"
add_files("src/main/main_file.cpp", "src/file/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/file", "src/common", { public = true })
add_packages "boost"
end
-- DNS server target
target "dns_server"
do
set_kind "binary"
add_files("src/main/main_dns.cpp", "src/dns/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/dns", "src/common", { public = true })
add_packages "boost"
end