2 Commits

Author SHA1 Message Date
Ваше Имя 20640e766e refactor(cli): improve option handling with defaults and protected inheritance
Update OptionUtils to include name placeholders for default values, switch cli
classes to protected inheritance, and use at() for safer option retrieval.
2025-10-05 14:13:52 +04:00
Ваше Имя 75563091c1 feat(echo): add echo client and server with CLI support 2025-10-04 20:28:19 +04:00
24 changed files with 446 additions and 274 deletions
-1
View File
@@ -2,4 +2,3 @@
.xmake .xmake
build build
compile_commands.json compile_commands.json
ignore
-26
View File
@@ -1,26 +0,0 @@
#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>();
}
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class PortScanCli : public CliBase, protected OptionUtils
{
public:
PortScanCli();
std::string getIp() const;
protected:
void setupOptions() override;
private:
std::string ip_;
};
+59
View File
@@ -0,0 +1,59 @@
#include "echo_client.hpp"
#include <boost/asio/connect.hpp>
EchoClient::EchoClient(IoContext &io, std::string host, unsigned short port)
: io_(io)
, host_(std::move(host))
, port_(port)
, socket_(io_)
{
}
bool EchoClient::run(const std::string &message)
{
if (!connect()) {
Logger::error("EchoClient: connect failed");
return false;
}
const bool ok = send(message);
close();
return ok;
}
bool EchoClient::connect()
{
try {
boost::asio::ip::tcp::resolver resolver(io_);
auto results = resolver.resolve(host_, std::to_string(port_));
boost::asio::connect(socket_, results);
Logger::info(
"EchoClient: connected to " + host_ + ":" + std::to_string(port_)
);
return true;
} catch (const std::exception &ex) {
Logger::error(
std::string("EchoClient: connect exception: ") + ex.what()
);
return false;
}
}
bool EchoClient::send(const std::string &message)
{
try {
auto bytes = boost::asio::write(socket_, boost::asio::buffer(message));
Logger::info("EchoClient: sent " + std::to_string(bytes) + " bytes");
return true;
} catch (const std::exception &ex) {
Logger::error(std::string("EchoClient: send exception: ") + ex.what());
return false;
}
}
void EchoClient::close()
{
ErrorCode ec;
socket_.shutdown(TcpSocket::shutdown_both, ec);
socket_.close(ec);
Logger::info("EchoClient: connection closed");
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "aliases.hpp"
#include "logger.hpp"
#include <string>
class EchoClient final
{
public:
EchoClient(IoContext &io, std::string host, unsigned short port);
bool run(const std::string &message);
private:
bool connect();
bool send(const std::string &message);
void close();
private:
IoContext &io_;
std::string host_;
unsigned short port_;
TcpSocket socket_{io_};
};
+34
View File
@@ -0,0 +1,34 @@
#include "echo_client_cli.hpp"
#include <boost/program_options/value_semantic.hpp>
EchoClientCli::EchoClientCli()
: CliBase("Echo Client Options")
, host_("127.0.0.1")
, port_(9000)
, message_("Hello from client")
{
}
void EchoClientCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("host,H", withDefault(host_, "<HOST>"), "Server host (IP or DNS)")
("port,p", withDefault(port_, "<PORT>"), "Server port")
("message,m", withDefault(message_, "<TEXT>"), "Message to send");
// clang-format on
}
std::string EchoClientCli::getHost() const
{
return vm_.at("host").as<std::string>();
}
int EchoClientCli::getPort() const
{
return vm_.at("port").as<int>();
}
std::string EchoClientCli::getMessage() const
{
return vm_.at("message").as<std::string>();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class EchoClientCli : public CliBase, protected OptionUtils
{
public:
EchoClientCli();
std::string getHost() const;
int getPort() const;
std::string getMessage() const;
protected:
void setupOptions() override;
private:
// defaults (used only to show defaults in help via withDefault)
std::string host_;
int port_;
std::string message_;
};
+6 -8
View File
@@ -1,29 +1,27 @@
#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;
// --- Asio types --- // --- IO Context ---
using IoContext = boost::asio::io_context; using IoContext = boost::asio::io_context;
using SteadyTimer = boost::asio::steady_timer;
// --- Ip types --- // --- TCP ---
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 types --- // --- UDP (для DNS) ---
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;
+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_;
};
-142
View File
@@ -1,142 +0,0 @@
#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
@@ -1,37 +0,0 @@
#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};
};
-38
View File
@@ -1,38 +0,0 @@
#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
@@ -0,0 +1,26 @@
#include "aliases.hpp"
#include "echo_client.hpp"
#include "echo_client_cli.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
EchoClientCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true);
IoContext io;
const std::string host = cli.getHost();
const auto port = static_cast<unsigned short>(cli.getPort());
const std::string message = cli.getMessage();
EchoClient client(io, host, port);
const bool ok = client.run(message);
return ok ? 0 : 1;
}
+27
View File
@@ -0,0 +1,27 @@
#include "aliases.hpp"
#include "echo_server.hpp"
#include "echo_server_cli.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
EchoServerCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true);
IoContext io;
const auto port = static_cast<unsigned short>(cli.getPort());
EchoServer server(io, port);
Logger::info("EchoServer starting on port " + std::to_string(port));
server.start();
io.run();
return 0;
}
+8
View File
@@ -0,0 +1,8 @@
#include "echo_server.hpp"
#include "echo_session.hpp"
void EchoServer::createSession(TcpSocket socket)
{
auto session = std::make_shared<EchoSession>(std::move(socket));
session->start();
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "tcp_server_base.hpp"
class EchoServer final : public TcpServerBase
{
public:
EchoServer(IoContext &io, unsigned short port)
: TcpServerBase(io, port)
{
}
protected:
void createSession(TcpSocket socket) override;
};
+27
View File
@@ -0,0 +1,27 @@
#include "echo_server_cli.hpp"
EchoServerCli::EchoServerCli()
: CliBase("Echo Server Options")
, port_(9000)
, address_("0.0.0.0")
{
}
void EchoServerCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("port,p", withDefault(port_, "<PORT>"), "Port to listen on")
("address,a", withDefault(address_, "<ADDRESS>"), "Bind address (e.g. 0.0.0.0)");
// clang-format on
}
int EchoServerCli::getPort() const
{
return vm_.at("port").as<int>();
}
std::string EchoServerCli::getAddress() const
{
return vm_.at("address").as<std::string>();
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class EchoServerCli : public CliBase, protected OptionUtils
{
public:
EchoServerCli();
int getPort() const;
std::string getAddress() const;
protected:
void setupOptions() override;
private:
int port_;
std::string address_;
};
+15
View File
@@ -0,0 +1,15 @@
#include "echo_session.hpp"
#include "logger.hpp"
#include <utility> // std::move
EchoSession::EchoSession(TcpSocket socket)
: SessionBase(std::move(socket))
{
}
void EchoSession::handleRequest(const std::string_view rawRequest)
{
Logger::info(
std::string("EchoSession received: ") + std::string(rawRequest)
);
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "session_base.hpp"
class EchoSession final : public SessionBase
{
public:
explicit EchoSession(TcpSocket socket);
protected:
void handleRequest(const std::string_view rawRequest) override;
};
+13 -3
View File
@@ -10,10 +10,20 @@ add_requires("cmake::Boost", {
}, },
}) })
target "port_scanner" -- Echo server target
target "echo_server"
do do
set_kind "binary" set_kind "binary"
add_files("src/main.cpp", "src/cli/*.cpp", "src/common/*.cpp", "src/core/*.cpp") add_files("src/main/echo_server_main.cpp", "src/server/*.cpp", "src/session/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/cli", "src/common", "src/core", { public = true }) add_includedirs("src", "src/server", "src/session", "src/common", { public = true })
add_packages "boost"
end
-- Echo client target
target "echo_client"
do
set_kind "binary"
add_files("src/main/echo_client_main.cpp", "src/client/*.cpp", "src/session/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/client", "src/session", "src/common", { public = true })
add_packages "boost" add_packages "boost"
end end