diff --git a/src/paping.cpp b/src/paping.cpp index eae378f..fba4128 100644 --- a/src/paping.cpp +++ b/src/paping.cpp @@ -65,6 +65,32 @@ static std::string colored(std::string_view text, std::string_view color) { return std::string(color) + std::string(text) + std::string(col::rst); } +static std::string json_escape(std::string_view s) { + std::string out; + out.reserve(s.size()); + for (char c : s) { + if (c == '"') out += "\\\""; + else if (c == '\\') out += "\\\\"; + else if (c == '\n') out += "\\n"; + else if (c == '\r') out += "\\r"; + else if (c == '\t') out += "\\t"; + else out += c; + } + return out; +} + +static std::string csv_escape(std::string_view s) { + if (s.find_first_of(",\"\n\r") == std::string_view::npos) + return std::string(s); + std::string out = "\""; + for (char c : s) { + if (c == '"') out += "\"\""; + else out += c; + } + out += '"'; + return out; +} + static void platform_init() { #ifdef _WIN32 WSADATA wsa{}; @@ -82,11 +108,16 @@ static void platform_cleanup() { #endif } +enum class OutputFormat { Human, Json, Csv }; + struct Config { - std::string host; - int port = 0; - int count = -1; - int timeout = 1000; + std::string host; + int port = 0; + int count = -1; + int timeout = 1000; + OutputFormat format = OutputFormat::Human; + bool silent = false; + bool stats_only = false; }; struct HostInfo { @@ -139,7 +170,7 @@ static std::optional resolve(const std::string& hostname) { return info; } -enum class Probe { Connected, TimedOut, Failed }; +enum class Probe { Connected, TimedOut, Failed }; static Probe tcp_probe(const std::string& ip, int port, int timeout_ms, double& elapsed_ms) { elapsed_ms = 0.0; @@ -207,66 +238,135 @@ static Probe tcp_probe(const std::string& ip, int port, int timeout_ms, double& return result; } -static void print_banner() { +static void print_banner(const Config& cfg) { + if (cfg.stats_only || cfg.format != OutputFormat::Human) return; std::cout << std::format("Paping-NG v{} - Copyright (c) 2026 Oliver (arch3r.eu)\n\n", VERSION); } static void print_usage() { - std::cout << + std::cout << std::format("Paping-NG v{} - Copyright (c) 2026 Oliver (arch3r.eu)\n\n", VERSION) << "Usage: paping -p [options]\n\n" "Options:\n" - " -p, --port N TCP port to probe (required)\n" - " -c, --count N stop after N probes (default: run forever)\n" - " -t, --timeout N connection timeout in ms (default: 1000)\n" - " -h, --help show this help\n"; + " -p, --port N TCP port to probe (required)\n" + " -c, --count N stop after N probes (default: run forever)\n" + " -t, --timeout N connection timeout in ms (default: 1000)\n" + " --format output format: human (default), json, csv\n" + " --silent suppress per-probe lines; print stats at end\n" + " --stats-only suppress banner, target, and per-probe lines\n" + " -h, --help show this help\n"; } -static void print_target(const HostInfo& host, int port) { +static void print_target(const Config& cfg, const HostInfo& host) { + if (cfg.stats_only || cfg.format != OutputFormat::Human) return; if (host.is_ip) { std::cout << std::format("Connecting to {} on TCP port {}:\n\n", colored(host.ip, col::yellow), - colored(std::to_string(port), col::yellow)); + colored(std::to_string(cfg.port), col::yellow)); } else { std::cout << std::format("Connecting to {} [{}] on TCP port {}:\n\n", colored(host.canonical, col::yellow), colored(host.ip, col::yellow), - colored(std::to_string(port), col::yellow)); + colored(std::to_string(cfg.port), col::yellow)); } } -static void print_probe(const HostInfo& host, int port, Probe result, double ms) { - switch (result) { - case Probe::Connected: - std::cout << std::format("Connected to {}: time={} protocol={} port={}\n", - colored(host.ip, col::green), - colored(std::format("{:.2f}ms", ms), col::green), - colored("TCP", col::green), - colored(std::to_string(port), col::green)); +static void print_csv_header() { + std::cout << "seq,host,ip,port,status,time_ms\n"; +} + +static void print_probe(const Config& cfg, const HostInfo& host, int seq, Probe result, double ms) { + if (cfg.silent) return; + + switch (cfg.format) { + case OutputFormat::Human: + switch (result) { + case Probe::Connected: + std::cout << std::format("Connected to {}: time={} protocol={} port={}\n", + colored(host.ip, col::green), + colored(std::format("{:.2f}ms", ms), col::green), + colored("TCP", col::green), + colored(std::to_string(cfg.port), col::green)); + break; + case Probe::TimedOut: + std::cout << colored("Connection timed out\n", col::red); + break; + case Probe::Failed: + std::cout << colored("Connection failed\n", col::red); + break; + } break; - case Probe::TimedOut: - std::cout << colored("Connection timed out\n", col::red); + + case OutputFormat::Json: { + const char* status = + result == Probe::Connected ? "connected" : + result == Probe::TimedOut ? "timed_out" : "failed"; + if (result == Probe::Connected) { + std::cout << std::format( + "{{\"seq\":{},\"host\":\"{}\",\"ip\":\"{}\",\"port\":{}," + "\"status\":\"{}\",\"time_ms\":{:.3f}}}\n", + seq, json_escape(host.canonical), host.ip, cfg.port, status, ms); + } else { + std::cout << std::format( + "{{\"seq\":{},\"host\":\"{}\",\"ip\":\"{}\",\"port\":{}," + "\"status\":\"{}\",\"time_ms\":null}}\n", + seq, json_escape(host.canonical), host.ip, cfg.port, status); + } break; - case Probe::Failed: - std::cout << colored("Connection failed\n", col::red); + } + + case OutputFormat::Csv: { + const char* status = + result == Probe::Connected ? "connected" : + result == Probe::TimedOut ? "timed_out" : "failed"; + std::cout << std::format("{},{},{},{},{},{}\n", + seq, + csv_escape(host.canonical), + host.ip, + cfg.port, + status, + result == Probe::Connected ? std::format("{:.3f}", ms) : ""); break; } + } } -static void print_stats(const Stats& s) { +static void print_stats(const Config& cfg, const Stats& s) { if (s.attempts == 0) return; const double fail_pct = 100.0 * s.failures / s.attempts; - const double min_v = s.connects > 0 ? s.min_ms : 0.0; - const double max_v = s.connects > 0 ? s.max_ms : 0.0; - std::cout << std::format( - "\n{} Probed = {}, Connected = {}, Failed = {} ({:.2f}%)\n" - "Approximate connection times:\n" - " Minimum = {:.2f}ms, Maximum = {:.2f}ms, Average = {:.2f}ms\n", - colored("Connection statistics:\n", col::blue), - colored(std::to_string(s.attempts), col::cyan), - colored(std::to_string(s.connects), col::green), - colored(std::to_string(s.failures), col::red), - fail_pct, min_v, max_v, s.avg()); + const double min_v = s.connects > 0 ? s.min_ms : 0.0; + const double max_v = s.connects > 0 ? s.max_ms : 0.0; + + switch (cfg.format) { + case OutputFormat::Human: + std::cout << std::format( + "\n{} Probed = {}, Connected = {}, Failed = {} ({:.2f}%)\n" + "Approximate connection times:\n" + " Minimum = {:.2f}ms, Maximum = {:.2f}ms, Average = {:.2f}ms\n", + colored("Connection statistics:\n", col::blue), + colored(std::to_string(s.attempts), col::cyan), + colored(std::to_string(s.connects), col::green), + colored(std::to_string(s.failures), col::red), + fail_pct, min_v, max_v, s.avg()); + break; + + case OutputFormat::Json: + std::cout << std::format( + "{{\"stats\":{{\"attempts\":{},\"connected\":{},\"failed\":{}," + "\"failure_pct\":{:.2f},\"min_ms\":{:.3f},\"max_ms\":{:.3f}," + "\"avg_ms\":{:.3f}}}}}\n", + s.attempts, s.connects, s.failures, + fail_pct, min_v, max_v, s.avg()); + break; + + case OutputFormat::Csv: + std::cout << std::format( + "\n#stats,attempts,connected,failed,failure_pct,min_ms,max_ms,avg_ms\n" + "#stats,{},{},{},{:.2f},{:.3f},{:.3f},{:.3f}\n", + s.attempts, s.connects, s.failures, + fail_pct, min_v, max_v, s.avg()); + break; + } } static std::optional parse_args(int argc, char* argv[]) { @@ -281,11 +381,26 @@ static std::optional parse_args(int argc, char* argv[]) { const char* p = argv[i]; auto [end, ec] = std::from_chars(p, p + std::strlen(p), out); return ec == std::errc{}; - }; + }; + + auto next_str = [&](std::string& out) -> bool { + if (++i >= argc) return false; + out = argv[i]; + return true; + }; if (arg == "-p" || arg == "--port") { if (!next_int(cfg.port)) return std::nullopt; } - else if (arg == "-c" || arg == "--count") { if (!next_int(cfg.count)) return std::nullopt; } + else if (arg == "-c" || arg == "--count") { if (!next_int(cfg.count)) return std::nullopt; } else if (arg == "-t" || arg == "--timeout") { if (!next_int(cfg.timeout)) return std::nullopt; } + else if (arg == "--format") { + std::string fmt; + if (!next_str(fmt)) return std::nullopt; + if (fmt == "json") cfg.format = OutputFormat::Json; + else if (fmt == "csv") cfg.format = OutputFormat::Csv; + else return std::nullopt; + } + else if (arg == "--silent") { cfg.silent = true; } + else if (arg == "--stats-only") { cfg.stats_only = true; cfg.silent = true; } else if (arg == "-h" || arg == "--help" || arg == "-?") { return std::nullopt; } else if (!got_host) { cfg.host = argv[i]; got_host = true; } else { return std::nullopt; } @@ -301,7 +416,6 @@ static void sig_handler(int) { g_stop = true; } int main(int argc, char* argv[]) { platform_init(); - print_banner(); auto cfg_opt = parse_args(argc, argv); if (!cfg_opt) { @@ -311,6 +425,8 @@ int main(int argc, char* argv[]) { } const Config& cfg = *cfg_opt; + print_banner(cfg); + auto host_opt = resolve(cfg.host); if (!host_opt) { std::cerr << colored("Cannot resolve host: ", col::red) << cfg.host << '\n'; @@ -319,16 +435,20 @@ int main(int argc, char* argv[]) { } const HostInfo& host = *host_opt; - print_target(host, cfg.port); + print_target(cfg, host); + + if (cfg.format == OutputFormat::Csv && !cfg.silent) + print_csv_header(); + std::signal(SIGINT, sig_handler); Stats stats; int exit_code = 0; - bool infinite = (cfg.count < 0); + bool infinite = (cfg.count < 0); for (int i = 0; (infinite || i < cfg.count) && !g_stop; ++i) { double elapsed = 0.0; - Probe result = tcp_probe(host.ip, cfg.port, cfg.timeout, elapsed); + Probe result = tcp_probe(host.ip, cfg.port, cfg.timeout, elapsed); ++stats.attempts; if (result == Probe::Connected) { @@ -340,15 +460,15 @@ int main(int argc, char* argv[]) { exit_code = 1; } - print_probe(host, cfg.port, result, elapsed); + print_probe(cfg, host, i + 1, result, elapsed); const int sleep_ms = 1000 - static_cast(std::min(elapsed, 1000.0)); - const bool more = infinite || (i + 1 < cfg.count); + const bool more = infinite || (i + 1 < cfg.count); if (sleep_ms > 0 && more && !g_stop) std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); } - print_stats(stats); + print_stats(cfg, stats); platform_cleanup(); return exit_code; } \ No newline at end of file