2026-07-22 17:55:45 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
2025-11-02 15:00:53 +01:00
|
|
|
|
|
|
|
|
module;
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <sys/uio.h>
|
|
|
|
|
#include <sys/time.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <netdb.h>
|
|
|
|
|
#include <strings.h>
|
2025-11-03 14:25:51 +01:00
|
|
|
#include <cerrno>
|
2025-11-02 15:00:53 +01:00
|
|
|
|
|
|
|
|
module Crafter.Network:ClientTCP_impl;
|
|
|
|
|
import :ClientTCP;
|
|
|
|
|
import Crafter.Thread;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
ClientTCP::ClientTCP(int socketid) : socketid(socketid)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientTCP::ClientTCP(const char* hostName, std::uint16_t port)
|
|
|
|
|
{
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
// getaddrinfo rather than gethostbyname: the latter is not thread safe,
|
|
|
|
|
// and it signals failure by returning null — which was then dereferenced
|
|
|
|
|
// straight into a crash on any unresolvable host.
|
|
|
|
|
addrinfo hints{};
|
|
|
|
|
hints.ai_family = AF_INET;
|
|
|
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
|
addrinfo* resolved = nullptr;
|
|
|
|
|
const std::string service = std::to_string(port);
|
|
|
|
|
const int status = getaddrinfo(hostName, service.c_str(), &hints, &resolved);
|
|
|
|
|
if (status != 0 || resolved == nullptr) {
|
|
|
|
|
throw std::runtime_error(std::string("Could not resolve host '") + hostName + "': "
|
|
|
|
|
+ gai_strerror(status));
|
|
|
|
|
}
|
|
|
|
|
host = nullptr;
|
|
|
|
|
serv_addr = *reinterpret_cast<sockaddr_in*>(resolved->ai_addr);
|
|
|
|
|
freeaddrinfo(resolved);
|
2025-11-02 15:00:53 +01:00
|
|
|
|
2025-11-03 15:51:13 +01:00
|
|
|
Connect();
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientTCP::ClientTCP(std::string hostName, std::uint16_t port): ClientTCP(hostName.c_str(), port)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
// The moved-from object gives up ownership; the socket itself stays open —
|
|
|
|
|
// closing it here (as this used to) meant moving a ClientTCP silently
|
|
|
|
|
// dropped the connection it was carrying.
|
2025-11-03 15:51:13 +01:00
|
|
|
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid) {
|
|
|
|
|
other.socketid = -1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 15:00:53 +01:00
|
|
|
ClientTCP::~ClientTCP()
|
|
|
|
|
{
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
if(socketid != -1) {
|
2025-11-03 14:25:51 +01:00
|
|
|
shutdown(socketid, SHUT_RDWR);
|
|
|
|
|
close(socketid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 15:51:13 +01:00
|
|
|
void ClientTCP::Connect() {
|
|
|
|
|
if((socketid = socket(AF_INET, SOCK_STREAM, 0)) == -1){
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
throw std::runtime_error(std::string("Could not open socket: ") + std::strerror(errno));
|
2025-11-03 15:51:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(connect(socketid,(sockaddr*)&serv_addr, sizeof(sockaddr)) == -1){
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
// Report the failure instead of handing back a socket that is not
|
|
|
|
|
// connected to anything — every later send/recv on it would fail
|
|
|
|
|
// with a far less obvious error.
|
|
|
|
|
const std::string reason = std::strerror(errno);
|
|
|
|
|
close(socketid);
|
|
|
|
|
socketid = -1;
|
|
|
|
|
throw std::runtime_error("Could not connect to server: " + reason);
|
2025-11-03 15:51:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 14:25:51 +01:00
|
|
|
void ClientTCP::Stop() {
|
|
|
|
|
shutdown(socketid, SHUT_RDWR);
|
2025-11-02 15:00:53 +01:00
|
|
|
close(socketid);
|
2025-11-03 14:25:51 +01:00
|
|
|
socketid = -1;
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::Send(const void* buffer, std::uint32_t size) const {
|
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these
bit it:
- gethostbyname() returning null on an unresolvable host was dereferenced
straight into a crash, and it is not thread safe; use getaddrinfo.
- A failed socket()/connect() only printed to stderr and handed back an
unusable ClientTCP, so the real error surfaced much later as an
unrelated errno from send().
- send() was assumed to accept everything it was offered. It does not
once a buffer outgrows the socket's send buffer, which silently
truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a
vanished peer raises EPIPE instead of killing the process.
- ClientTCP's move constructor closed the socket it had just taken
ownership of, and both it and the destructor tested `socketid != 1`
where they meant `!= -1`.
- ListenerTCP ignored bind()'s result, leaving a listener that accepted
nothing with no explanation, and did not set SO_REUSEADDR, so a
restart hit EADDRINUSE for the length of TIME_WAIT.
accept() failing during Stop() is expected and no longer logged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:44:56 +00:00
|
|
|
// send() is free to accept less than it was offered, and does so
|
|
|
|
|
// routinely once a buffer outgrows the socket's send buffer — a
|
|
|
|
|
// multi-megabyte HTTP body, say. Loop until it is all handed over.
|
|
|
|
|
const char* data = reinterpret_cast<const char*>(buffer);
|
|
|
|
|
std::uint32_t sent = 0;
|
|
|
|
|
while (sent < size) {
|
|
|
|
|
const auto status = send(socketid, data + sent, size - sent, MSG_NOSIGNAL);
|
|
|
|
|
if (status == 0) {
|
|
|
|
|
throw SocketClosedException();
|
|
|
|
|
} else if (status < 0) {
|
|
|
|
|
if (errno == EINTR) continue;
|
|
|
|
|
throw std::runtime_error(std::strerror(errno));
|
|
|
|
|
}
|
|
|
|
|
sent += static_cast<std::uint32_t>(status);
|
2025-11-03 15:51:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-02 15:00:53 +01:00
|
|
|
std::vector<char> ClientTCP::RecieveSync(std::uint32_t bufferSize) const {
|
|
|
|
|
std::vector<char> totalBuffer(bufferSize);
|
|
|
|
|
int read = recv(socketid, totalBuffer.data(), bufferSize, 0);
|
2025-11-03 14:25:51 +01:00
|
|
|
if(read < 0) {
|
|
|
|
|
throw std::runtime_error(std::strerror(errno));
|
|
|
|
|
} else if(read == 0) {
|
|
|
|
|
throw SocketClosedException();
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
return totalBuffer;
|
|
|
|
|
}
|
|
|
|
|
int ClientTCP::RecieveSync(std::uint32_t bufferSize, void* buffer) const {
|
|
|
|
|
return recv(socketid, reinterpret_cast<char*>(buffer), bufferSize, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveSync() const {
|
|
|
|
|
int count;
|
|
|
|
|
ioctl(socketid, FIONREAD, &count);
|
2025-11-03 14:25:51 +01:00
|
|
|
if(count == 0){
|
|
|
|
|
count = 1024;
|
|
|
|
|
}
|
2025-11-02 15:00:53 +01:00
|
|
|
std::vector<char> buffer(count);
|
2025-11-03 14:25:51 +01:00
|
|
|
int read = recv(socketid, buffer.data(), count, 0);
|
|
|
|
|
if(read < 0) {
|
|
|
|
|
throw std::runtime_error(std::strerror(errno));
|
|
|
|
|
} else if(read == 0) {
|
|
|
|
|
throw SocketClosedException();
|
|
|
|
|
} else if(count != read) {
|
|
|
|
|
buffer.resize(read);
|
|
|
|
|
}
|
2025-11-02 15:00:53 +01:00
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveUntilCloseSync() const {
|
|
|
|
|
int count;
|
|
|
|
|
ioctl(socketid, FIONREAD, &count);
|
2025-11-03 14:25:51 +01:00
|
|
|
if(count == 0){
|
|
|
|
|
count = 1024;
|
|
|
|
|
}
|
2025-11-02 15:00:53 +01:00
|
|
|
std::vector<char> buffer(count);
|
2025-11-03 14:25:51 +01:00
|
|
|
int read = recv(socketid, buffer.data(), count, 0);
|
|
|
|
|
if(read < 0) {
|
|
|
|
|
throw std::runtime_error(std::strerror(errno));
|
|
|
|
|
} else if(read == 0) {
|
|
|
|
|
throw SocketClosedException();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 15:00:53 +01:00
|
|
|
while(true) {
|
|
|
|
|
ioctl(socketid, FIONREAD, &count);
|
2025-11-03 14:25:51 +01:00
|
|
|
if(count == 0){
|
|
|
|
|
count = 1024;
|
|
|
|
|
}
|
2025-11-02 15:00:53 +01:00
|
|
|
unsigned int oldSize = buffer.size();
|
|
|
|
|
buffer.resize(buffer.size()+count);
|
2025-11-03 14:25:51 +01:00
|
|
|
int read = recv(socketid, buffer.data()+oldSize, count, 0);
|
|
|
|
|
if(read < 0) {
|
|
|
|
|
throw std::runtime_error(std::strerror(errno));
|
|
|
|
|
} else if(read == 0) {
|
|
|
|
|
return buffer;
|
|
|
|
|
} else if(count != read) {
|
|
|
|
|
buffer.resize(read);
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveUntilFullSync(std::uint32_t bufferSize) const {
|
|
|
|
|
std::vector<char> buffer(bufferSize);
|
|
|
|
|
int read = 0;
|
|
|
|
|
while(read < bufferSize) {
|
|
|
|
|
int newRead = recv(socketid, buffer.data()+read, bufferSize-read, 0);
|
2025-11-03 15:51:13 +01:00
|
|
|
if(newRead < 0) {
|
2025-11-03 14:25:51 +01:00
|
|
|
throw std::runtime_error(std::strerror(errno));
|
2025-11-03 15:51:13 +01:00
|
|
|
} else if(newRead == 0) {
|
2025-11-03 14:25:51 +01:00
|
|
|
throw SocketClosedException();
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
2025-11-03 14:25:51 +01:00
|
|
|
read+=newRead;
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
buffer.resize(read);
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback) const {
|
|
|
|
|
ThreadPool::Enqueue([recieveCallback, this, bufferSize](){
|
|
|
|
|
recieveCallback(this->RecieveSync(bufferSize));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(int)> recieveCallback, char* buffer) const {
|
|
|
|
|
ThreadPool::Enqueue([recieveCallback, this, bufferSize, buffer](){
|
|
|
|
|
recieveCallback(this->RecieveSync(bufferSize, buffer));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback) const {
|
|
|
|
|
ThreadPool::Enqueue([recieveCallback, this, bufferSize](){
|
|
|
|
|
recieveCallback(this->RecieveUntilFullSync(bufferSize));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::function<void(std::vector<char>)> recieveCallback) const {
|
|
|
|
|
ThreadPool::Enqueue([this, recieveCallback](){
|
|
|
|
|
recieveCallback(this->RecieveSync());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback) const {
|
|
|
|
|
ThreadPool::Enqueue([this, recieveCallback](){
|
|
|
|
|
recieveCallback(this->RecieveUntilCloseSync());
|
|
|
|
|
});
|
|
|
|
|
}
|