#3 - DNS support + IPv6

This commit is contained in:
Martin Slachta
2026-08-05 17:14:04 +02:00
parent 0e639abcd2
commit b71f740b9a
13 changed files with 291 additions and 92 deletions
+25 -19
View File
@@ -1,6 +1,5 @@
#include "ClientArgs.hpp"
#include <arpa/inet.h>
#include <charconv>
#include <algorithm>
#include <cctype>
@@ -25,12 +24,6 @@ std::string_view trim(std::string_view str) {
return str.substr(start, end - start);
}
bool is_valid_ipv4(std::string_view ip_str) {
// Use inet_pton to validate IPv4 format
struct in_addr addr;
return inet_pton(AF_INET, std::string(ip_str).c_str(), &addr) == 1;
}
tl::expected<int, std::string> parse_port(std::string_view port_str) {
if(port_str.empty()) {
return 8080; // Default port
@@ -63,15 +56,27 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
return tl::make_unexpected("address cannot be empty");
}
// Find the colon to split host and port
size_t colon_pos = text.rfind(':');
std::string_view host;
std::string_view port_str;
if(colon_pos == std::string_view::npos) {
// No colon found: treat entire string as port or host
// If it's all digits, treat as port; otherwise as host (will fail validation)
// An IPv6 literal carries colons of its own, so the brackets it is written
// in are what says where the host ends. This is the form to_string() emits.
if(text.front() == '[') {
size_t closing = text.find(']');
if(closing == std::string_view::npos) {
return tl::make_unexpected("address is missing a closing bracket");
}
host = text.substr(1, closing - 1);
port_str = text.substr(closing + 1);
if(!port_str.empty()) {
if(port_str.front() != ':') {
return tl::make_unexpected("expected a port after the closing bracket");
}
port_str.remove_prefix(1);
}
} else if(size_t colon_pos = text.rfind(':'); colon_pos == std::string_view::npos) {
bool all_digits = !text.empty() && std::all_of(text.begin(), text.end(),
[](unsigned char c) { return std::isdigit(c); });
@@ -79,7 +84,6 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
host = "127.0.0.1";
port_str = text;
} else {
// Treat as host with no port
host = text;
port_str = "";
}
@@ -93,10 +97,6 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
return tl::make_unexpected("host cannot be empty");
}
if(!is_valid_ipv4(host)) {
return tl::make_unexpected("not a valid IPv4 address");
}
// Parse port
auto port_result = parse_port(port_str);
if(!port_result) {
@@ -104,7 +104,13 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
}
int port = port_result.value();
return net::Address(std::optional<std::string>(std::string(host)), port);
auto address_r = net::Address::resolve(std::string(host), port);
if(!address_r) {
return tl::make_unexpected(address_r.error().message());
}
return address_r.value();
}
std::optional<std::string> server_arg(int argc, char** argv) {
+7 -4
View File
@@ -12,10 +12,13 @@ namespace tw::app {
/**
* Parse an address string into a network address.
*
* Accepts "host:port" or a bare port number. Bare port uses 127.0.0.1.
* Missing port defaults to 8080. Trims surrounding whitespace.
* Validates the host with inet_pton and returns an error string
* for non-IPv4 addresses or invalid ports.
* Accepts "host:port" or a bare port number, where the host may be a name as
* well as an address literal; an IPv6 literal has to be bracketed, as
* "[::1]:8080". Bare port uses 127.0.0.1. Missing port defaults to 8080.
* Trims surrounding whitespace.
*
* Looks the host up, so it blocks for as long as that takes, and returns an
* error string for a host that does not resolve or an invalid port.
*/
tl::expected<net::Address, std::string> parse_address(std::string_view text);