8 Commits

Author SHA1 Message Date
Ваше Имя 526b27deaf chore(aliases): remove SteadyTimer alias 2025-10-07 18:37:31 +04:00
Ваше Имя 7c19123da9 chore(.gitignore): add ignore file 2025-10-07 18:36:54 +04:00
Ваше Имя c9e1b0a149 feat(portscanner): implement port scanning CLI and core logic
Implement command-line interface for port scanning with IP validation and option
parsing. Add core scanning logic using Boost.Asio for asynchronous socket
checks, including timeout handling and port probing. Update xmake.lua to
configure build target and include necessary files.
2025-10-06 00:05:11 +04:00
Ваше Имя 0d96c21199 feat(common): add Boost Asio aliases
Introduce aliases for Asio IP, TCP, UDP, and timer types to enhance code
readability and structure.
2025-10-06 00:04:34 +04:00
Ваше Имя 679d752177 refactor(common): remove session and tcp server base classes 2025-10-06 00:03:10 +04:00
Ваше Имя 9f88390b1a refactor(option_utils): enhance option handling with default values and names
Removed server.hpp as part of refactoring the option utilities to use Boost's
program_options more effectively, allowing for named default values.
2025-10-05 14:20:21 +04:00
Ваше Имя a4c84379ff chore: remove UDP server implementation and main example 2025-10-04 17:37:58 +04:00
Ваше Имя 827cc07175 feat(option_utils): add withDefault utility for Boost program options 2025-10-04 17:09:41 +04:00
46 changed files with 291 additions and 1079 deletions
+1
View File
@@ -2,3 +2,4 @@
.xmake .xmake
build build
compile_commands.json compile_commands.json
ignore
+26
View File
@@ -0,0 +1,26 @@
#include "portscan_cli.hpp"
#include <boost/program_options/value_semantic.hpp>
#include <cassert>
PortScanCli::PortScanCli()
: CliBase("Port Scanner Options")
, ip_("")
{
}
void PortScanCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("ip,H", withDefault(ip_, "<IP>"), "Local IPv4 to scan");
// clang-format on
}
std::string PortScanCli::getIp() const
{
if (vm_["ip"].empty() == 0) {
return "127.0.0.1";
}
return vm_.at("ip").as<std::string>();
}
@@ -1,20 +1,19 @@
#pragma once #pragma once
#include "cli_base.hpp" #include "cli_base.hpp"
#include "option_utils.hpp"
#include <string> #include <string>
class DnsCli : public CliBase class PortScanCli : public CliBase, protected OptionUtils
{ {
public: public:
DnsCli(); PortScanCli();
int getPort() const;
std::string getIp() const; std::string getIp() const;
protected: protected:
void setupOptions() override; void setupOptions() override;
private: private:
int port_;
std::string ip_; std::string ip_;
}; };
+8 -6
View File
@@ -1,27 +1,29 @@
#pragma once #pragma once
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/program_options.hpp> #include <boost/program_options.hpp>
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
// --- Error handling --- // --- Error handling ---
using ErrorCode = boost::system::error_code; using ErrorCode = boost::system::error_code;
// --- IO Context --- // --- Asio types ---
using IoContext = boost::asio::io_context; using IoContext = boost::asio::io_context;
using SteadyTimer = boost::asio::steady_timer;
// --- TCP --- // --- Ip types ---
using IpAddress = boost::asio::ip::address;
// --- TCP types ---
using TcpSocket = boost::asio::ip::tcp::socket; using TcpSocket = boost::asio::ip::tcp::socket;
using TcpAcceptor = boost::asio::ip::tcp::acceptor; using TcpAcceptor = boost::asio::ip::tcp::acceptor;
using TcpEndpoint = boost::asio::ip::tcp::endpoint; using TcpEndpoint = boost::asio::ip::tcp::endpoint;
// --- UDP (для DNS) --- // --- UDP types ---
using UdpSocket = boost::asio::ip::udp::socket; using UdpSocket = boost::asio::ip::udp::socket;
using UdpEndpoint = boost::asio::ip::udp::endpoint; using UdpEndpoint = boost::asio::ip::udp::endpoint;
// --- Таймеры ---
using SteadyTimer = boost::asio::steady_timer;
// --- Program options --- // --- Program options ---
using OptionsDescription = boost::program_options::options_description; using OptionsDescription = boost::program_options::options_description;
using VariablesMap = boost::program_options::variables_map; using VariablesMap = boost::program_options::variables_map;
-1
View File
@@ -1,5 +1,4 @@
#include "logger.hpp" #include "logger.hpp"
#include <boost/date_time/posix_time/posix_time_io.hpp> #include <boost/date_time/posix_time/posix_time_io.hpp>
void Logger::init(bool toConsole, const std::string_view filePath) void Logger::init(bool toConsole, const std::string_view filePath)
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <boost/program_options.hpp>
class OptionUtils
{
public:
template <typename T>
using OptionValue = boost::program_options::typed_value<T> *;
template <typename T>
static OptionValue<T>
withDefault(const T &value, const char *name = nullptr);
protected:
OptionUtils() = default;
~OptionUtils() = default;
};
#include "option_utils.tpp"
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "option_utils.hpp"
template <typename T>
OptionUtils::OptionValue<T>
OptionUtils::withDefault(const T &value, const char *name)
{
auto v = boost::program_options::value<T>()->default_value(value);
if (name) {
v->value_name(name); // Optional
}
return v;
}
-9
View File
@@ -1,9 +0,0 @@
#pragma once
class Server
{
public:
virtual ~Server() = default;
virtual void start() = 0;
virtual void stop() = 0;
};
-70
View File
@@ -1,70 +0,0 @@
#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
@@ -1,25 +0,0 @@
#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
@@ -1,23 +0,0 @@
#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
@@ -1,17 +0,0 @@
#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
@@ -1,46 +0,0 @@
#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
@@ -1,23 +0,0 @@
#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_;
};
+142
View File
@@ -0,0 +1,142 @@
#include "port_scanner.hpp"
#include "logger.hpp"
#include <boost/asio.hpp>
#include <string>
PortScanner::PortScanner(
const std::string &ip, const std::vector<uint16_t> &ports
)
: ports_(ports)
{
ErrorCode ec;
addr_ = boost::asio::ip::make_address(ip, ec);
ip_ok_ = !ec;
if (!ip_ok_) {
Logger::error(std::string("PortScanner: invalid IP: ") + ip);
} else {
Logger::debug(std::string("PortScanner: using IP: ") + ip);
}
}
void PortScanner::setTimeoutMs(int ms)
{
if (ms > 0) {
timeout_ms_ = ms;
Logger::debug(
std::string("PortScanner: timeout set to ")
+ std::to_string(timeout_ms_) + " ms"
);
}
}
void PortScanner::run()
{
if (!ip_ok_) {
Logger::error("PortScanner::run - aborting: ip not ok");
return;
}
for (uint16_t p : ports_) {
if (probe(p)) {
Logger::info(std::to_string(p));
}
}
}
bool PortScanner::probe(uint16_t port)
{
TcpSocket s(io_);
SteadyTimer t(io_);
auto ep = endpoint(port);
Logger::debug(
std::string("PortScanner::probe - probing port ") + std::to_string(port)
);
armTimer(t, s);
armConnect(s, ep);
runUntilDone();
cancelTimer(t);
drainLeftovers();
close(s);
if (ok_) {
Logger::debug(std::string("Port ") + std::to_string(port) + " is OPEN");
} else {
Logger::debug(
std::string("Port ") + std::to_string(port) + " is CLOSED/FILTERED"
);
}
return ok_;
}
TcpEndpoint PortScanner::endpoint(uint16_t port) const
{
return TcpEndpoint(addr_, port);
}
void PortScanner::armTimer(SteadyTimer &t, TcpSocket &s)
{
t.expires_after(std::chrono::milliseconds(timeout_ms_));
auto onTimeout = [&s](const ErrorCode &ec) {
if (!ec) {
Logger::debug("Timer expired -> cancelling socket");
s.cancel();
}
};
t.async_wait(onTimeout);
}
void PortScanner::armConnect(TcpSocket &s, const TcpEndpoint &ep)
{
ok_ = false;
done_ = false;
auto onConnect = [this](const ErrorCode &ec) {
if (!ec) {
ok_ = true;
Logger::debug("async_connect succeeded");
} else {
ok_ = false;
Logger::debug(std::string("async_connect failed: ") + ec.message());
}
done_ = true;
};
s.async_connect(ep, onConnect);
}
void PortScanner::runUntilDone()
{
io_.restart();
while (!done_ && io_.run_one()) {
}
}
void PortScanner::cancelTimer(SteadyTimer &t)
{
ErrorCode ec;
t.cancel();
if (ec) {
Logger::warn(std::string("cancelTimer: ") + ec.message());
}
}
void PortScanner::drainLeftovers()
{
io_.restart();
while (io_.run_one()) {
continue;
}
}
void PortScanner::close(TcpSocket &s)
{
ErrorCode ig;
if (s.close(ig)) {
Logger::warn(std::string("close socket failed: ") + ig.message());
}
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "aliases.hpp"
#include <boost/asio.hpp>
#include <cstdint>
#include <string>
#include <vector>
class PortScanner
{
public:
PortScanner(const std::string &ip, const std::vector<uint16_t> &ports);
void setTimeoutMs(int ms);
void run();
private:
bool probe(uint16_t port);
TcpEndpoint endpoint(uint16_t port) const;
void armTimer(SteadyTimer &t, TcpSocket &s);
void armConnect(TcpSocket &s, const TcpEndpoint &ep);
void runUntilDone();
void cancelTimer(SteadyTimer &t);
void drainLeftovers();
void close(TcpSocket &s);
private:
IoContext io_;
IpAddress addr_;
std::vector<uint16_t> ports_;
int timeout_ms_{300};
bool ip_ok_{false};
bool ok_{false};
bool done_{false};
};
-34
View File
@@ -1,34 +0,0 @@
#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>();
}
-57
View File
@@ -1,57 +0,0 @@
#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
@@ -1,18 +0,0 @@
#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
@@ -1,34 +0,0 @@
#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
@@ -1,19 +0,0 @@
#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
@@ -1,13 +0,0 @@
#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
@@ -1,16 +0,0 @@
#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
@@ -1,35 +0,0 @@
#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
@@ -1,15 +0,0 @@
#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
@@ -1,34 +0,0 @@
#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
@@ -1,19 +0,0 @@
#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
@@ -1,70 +0,0 @@
#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
@@ -1,34 +0,0 @@
#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
@@ -1,89 +0,0 @@
#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
@@ -1,16 +0,0 @@
#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
@@ -1,95 +0,0 @@
#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
@@ -1,36 +0,0 @@
#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
@@ -1,15 +0,0 @@
#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
@@ -1,15 +0,0 @@
#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
@@ -1,61 +0,0 @@
#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
@@ -1,15 +0,0 @@
#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_;
};
+38
View File
@@ -0,0 +1,38 @@
#include "cli_base.hpp"
#include "logger.hpp"
#include "port_scanner.hpp"
#include "portscan_cli.hpp"
#include <string>
#include <vector>
static std::vector<uint16_t> allPorts()
{
std::vector<uint16_t> v;
v.reserve(65535);
for (uint32_t p = 1; p <= 65535; ++p) {
v.push_back(static_cast<uint16_t>(p));
}
return v;
}
int main(int argc, char **argv)
{
Logger::init(true);
PortScanCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
const std::string ip = cli.getIp();
auto ports = allPorts();
Logger::info("Starting full port scan (1..65535)...");
PortScanner scanner(ip, ports);
scanner.run();
Logger::info("Scan completed.");
return 0;
}
-26
View File
@@ -1,26 +0,0 @@
#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
@@ -1,27 +0,0 @@
#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
@@ -1,29 +0,0 @@
#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;
}
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
# Test DNS server on port 5300
dig @127.0.0.1 -p 5300 example.com
-4
View File
@@ -1,4 +0,0 @@
#!/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
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
# Test HTTP server on port 8080
curl -v http://127.0.0.1:8080/
-1
View File
@@ -1 +0,0 @@
Hello from file!
+3 -22
View File
@@ -10,29 +10,10 @@ add_requires("cmake::Boost", {
}, },
}) })
-- HTTP server target target "port_scanner"
target "http_server"
do do
set_kind "binary" set_kind "binary"
add_files("src/main/main_http.cpp", "src/http/*.cpp", "src/common/*.cpp") add_files("src/main.cpp", "src/cli/*.cpp", "src/common/*.cpp", "src/core/*.cpp")
add_includedirs("src", "src/http", "src/common", { public = true }) add_includedirs("src", "src/cli", "src/common", "src/core", { 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" add_packages "boost"
end end