feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name resolves to, in getaddrinfo's order, the way Python's socket.create_connection does. It used to ask for AF_INET only and keep the first answer, so `localhost` could not reach a server listening on ::1 alone, which is what a dev server on a recent Node does. The connect timeout bounds each address on its own; when none connects, what the last one failed with is thrown. - A failed resolve, socket() or connect throws SocketErrorException: the std::runtime_error it always was, with the same message, plus the errno (SocketErrorKind::System) or the getaddrinfo code (SocketErrorKind::Resolver). A caller that reports the cause in its own words, such as a Python-style "[Errno 111] Connection refused", no longer has to parse it back out of the text. - The move constructor carries the resolved addresses along. New: ShouldConnectDualStack, ShouldReportConnectErrors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a58564422
commit
78853033e8
6 changed files with 383 additions and 51 deletions
14
README.md
14
README.md
|
|
@ -53,6 +53,18 @@ client.Send("Hello World", 11);
|
|||
std::vector<char> data = client.RecieveSync();
|
||||
```
|
||||
|
||||
The host is resolved for IPv4 and IPv6 alike and every address it resolves to is tried in turn, as Python's `socket.create_connection` does, so `localhost` reaches a server that listens on `::1` only. The optional third argument is a connect timeout, applied to each address on its own. A failed resolve or connect throws `SocketErrorException`: still a `std::runtime_error` with the same message, but carrying a `kind` (`SocketErrorKind::System` for an errno such as `ECONNREFUSED`, `SocketErrorKind::Resolver` for a getaddrinfo result such as `EAI_NONAME`) and the `code`, so a caller can report the cause without parsing the text.
|
||||
|
||||
```cpp
|
||||
try {
|
||||
Crafter::ClientTCP client("localhost", 5432, std::chrono::seconds(5));
|
||||
} catch (const Crafter::SocketErrorException& e) {
|
||||
std::println("[Errno {}] {}", e.code, std::strerror(e.code)); // [Errno 111] Connection refused
|
||||
} catch (const Crafter::SocketTimeoutException&) {
|
||||
std::println("timed out");
|
||||
}
|
||||
```
|
||||
|
||||
#### ListenerTCP
|
||||
```cpp
|
||||
// Create a TCP listener
|
||||
|
|
@ -287,6 +299,8 @@ The library includes tests covering:
|
|||
- HTTPS round-trip (`ShouldSendRecieveHTTPS1`) — the plaintext round-trip replayed over TLS, so a transport regression surfaces as an HTTP failure, plus what only exists under TLS: ALPN, `scheme=https` reaching handlers, a body spanning many records, verification failing for an untrusted certificate *and* for a trusted certificate presented under the wrong name, and a plaintext peer on the TLS port being counted and shrugged off
|
||||
- HTTPS interop (`ShouldInteropCurlHTTPS1`) — `curl` verifying our certificate with `--cacert` (not `--insecure`): keep-alive reuse, POST, HEAD, negotiated version, and an h2-only client being refused rather than mis-served; then `ClientHTTP1` against python3's `http.server` behind `ssl.wrap_socket`, whose HTTP/1.0 `Connection: close` frames the body by `close_notify`
|
||||
- Mutual TLS and the raw stream (`ShouldRequireClientCertificateHTTPS1`) — a client certificate the listener's CA vouches for is served and one absent is refused; `TLSStream` is also driven directly with hand-written HTTP/1.1 to keep `:TLS` usable without `:ClientHTTP1` on top
|
||||
- Dual-stack connect (`ShouldConnectDualStack`) — `localhost` reaches a server on `::1` only and one on `127.0.0.1` only, an IPv6 literal connects over IPv6, and the last failure is what is thrown when no address answers; skips on a host without IPv6 loopback
|
||||
- Connect errors (`ShouldReportConnectErrors`) — a refused connect and an unresolvable name throw `SocketErrorException` with their errno or getaddrinfo code and the message ClientTCP always threw, and still reach a `std::runtime_error` handler
|
||||
|
||||
The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. `ShouldInteropCurlHTTP1` and `ShouldInteropCurlHTTPS1` skip whichever half is unavailable when `curl` or `python3` is not installed, and the mutual-TLS half of `ShouldRequireClientCertificateHTTPS1` skips without the `openssl` CLI (a *client* certificate needs the `clientAuth` extended key usage, which the built-in development certificate does not carry) — so all three pass on a bare machine. Install `curl`, `python3` and `openssl` to actually exercise them.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue