Files
2025-12-10 14:38:26 -08:00

287 lines
12 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#ifndef SOCKET_H_23498325972583947678456437
#define SOCKET_H_23498325972583947678456437
#include "sys_error.h"
#include <idn2.h>
#include <unistd.h> //close
#include <sys/socket.h>
#include <netinet/tcp.h> //TCP_NODELAY
#include <netdb.h> //getaddrinfo
namespace zen
{
#define THROW_LAST_SYS_ERROR_WSA(functionName) \
do { const ErrorCode ecInternal = getLastError(); throw SysError(formatSystemError(functionName, ecInternal)); } while (false)
#define THROW_LAST_SYS_ERROR_GAI(rcGai) \
do { \
if (rcGai == EAI_SYSTEM) /*"check errno for details"*/ \
THROW_LAST_SYS_ERROR("getaddrinfo"); \
\
throw SysError(formatSystemError("getaddrinfo", formatGaiErrorCode(rcGai), utfTo<std::wstring>(::gai_strerror(rcGai)))); \
} while (false)
inline
std::wstring formatGaiErrorCode(int ec)
{
switch (ec) //codes used on both Linux and macOS
{
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ADDRFAMILY);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_AGAIN);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_BADFLAGS);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAIL);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAMILY);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_MEMORY);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NODATA);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NONAME);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SERVICE);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SOCKTYPE);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SYSTEM);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_OVERFLOW);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INPROGRESS);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_CANCELED);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NOTCANCELED);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ALLDONE);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INTR);
ZEN_CHECK_CASE_FOR_CONSTANT(EAI_IDN_ENCODE);
default:
return replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(ec));
}
}
//patch up socket portability:
using SocketType = int;
const SocketType invalidSocket = -1;
inline void closeSocket(SocketType s) { ::close(s); }
void setNonBlocking(SocketType socket, bool value); //throw SysError
//Winsock needs to be initialized before calling any of these functions! (WSAStartup/WSACleanup)
class Socket //throw SysError
{
public:
Socket(const Zstring& server, const Zstring& serviceName, int timeoutSec) //throw SysError
{
//GetAddrInfo(): "If the pNodeName parameter contains an empty string, all registered addresses on the local computer are returned."
// "If the pNodeName parameter points to a string equal to "localhost", all loopback addresses on the local computer are returned."
if (trimCpy(server).empty())
throw SysError(_("Server name must not be empty."));
Zstring nodeName = server; //macOS supports IDN out of the box, it seems :) - unlike Linux:
if (!isAsciiString(server))
{
char* punyEncoded = nullptr;
int rc = idn2_lookup_ul(server.c_str(), &punyEncoded, IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL); //follow libcurl/src/lib/idn.c
if (rc != IDN2_OK)
rc = idn2_lookup_ul(server.c_str(), &punyEncoded, IDN2_TRANSITIONAL); //fallback to TR46 Transitional mode for better IDNA2003 compatibility
if (rc != IDN2_OK)
throw SysError(formatSystemError("idn2_lookup_ul", replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(rc)), L""));
ZEN_ON_SCOPE_EXIT(idn2_free(punyEncoded));
nodeName = punyEncoded;
}
const addrinfo hints
{
.ai_flags = AI_ADDRCONFIG, //return IPv4 address iff system supports it; dito for IPv6
.ai_family = AF_UNSPEC, //don't care if AF_INET or AF_INET6
.ai_socktype = SOCK_STREAM, //we *do* care about this one!
};
addrinfo* servinfo = nullptr;
ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo));
const int rcGai = ::getaddrinfo(nodeName.c_str(), serviceName.c_str(), &hints, &servinfo);
if (rcGai != 0)
THROW_LAST_SYS_ERROR_GAI(rcGai);
if (!servinfo)
throw SysError(formatSystemError("getaddrinfo", L"", L"Empty server info."));
const auto getConnectedSocket = [timeoutSec](const auto& /*addrinfo*/ ai)
{
SocketType testSocket = ::socket(ai.ai_family, //int socket_family
SOCK_CLOEXEC | SOCK_NONBLOCK |
ai.ai_socktype, //int socket_type
ai.ai_protocol); //int protocol
if (testSocket == invalidSocket)
THROW_LAST_SYS_ERROR_WSA("socket");
ZEN_ON_SCOPE_FAIL(closeSocket(testSocket));
if (::connect(testSocket, ai.ai_addr, static_cast<int>(ai.ai_addrlen)) != 0) //0 or SOCKET_ERROR(-1)
{
if (errno != EINPROGRESS)
THROW_LAST_SYS_ERROR_WSA("connect");
fd_set writefds{};
fd_set exceptfds{}; //mostly only relevant for connect()
FD_SET(testSocket, &writefds);
FD_SET(testSocket, &exceptfds);
/*const*/ timeval tv{.tv_sec = timeoutSec};
const int rv = ::select(
testSocket + 1, //int nfds = "highest-numbered file descriptor in any of the three sets, plus 1"
nullptr, //fd_set* readfds
&writefds, //fd_set* writefds
&exceptfds, //fd_set* exceptfds
&tv); //const timeval* timeout
if (rv < 0)
THROW_LAST_SYS_ERROR_WSA("select");
if (rv == 0) //time-out!
throw SysError(formatSystemError("select, " + utfTo<std::string>(_P("1 sec", "%x sec", timeoutSec)), ETIMEDOUT));
int error = 0;
socklen_t optLen = sizeof(error);
if (::getsockopt(testSocket, //[in] SOCKET s
SOL_SOCKET, //[in] int level
SO_ERROR, //[in] int optname
reinterpret_cast<char*>(&error), //[out] char* optval
&optLen) //[in, out] socklen_t* optlen
!= 0)
THROW_LAST_SYS_ERROR_WSA("getsockopt(SO_ERROR)");
if (error != 0)
throw SysError(formatSystemError("connect, SO_ERROR", static_cast<ErrorCode>(error))/*== system error code, apparently!?*/);
}
setNonBlocking(testSocket, false); //throw SysError
return testSocket;
};
/* getAddrInfo() often returns only one ai_family == AF_INET address, but more items are possible:
facebook.com: 1 x AF_INET6, 3 x AF_INET
microsoft.com: 5 x AF_INET => server not allowing connection: hanging for 5x timeoutSec :( */
std::optional<SysError> firstError;
for (const auto* /*::addrinfo*/ si = servinfo; si; si = si->ai_next)
try
{
socket_ = getConnectedSocket(*si); //throw SysError; pass ownership
firstError = std::nullopt;
break;
}
catch (const SysError& e) { if (!firstError) firstError = e; }
if (firstError)
throw* firstError;
assert(socket_ != invalidSocket); //list was non-empty, so there's either an error, or a valid socket
ZEN_ON_SCOPE_FAIL(closeSocket(socket_));
//-----------------------------------------------------------
//configure *after* selecting appropriate socket: cfg-failure should not discard otherwise fine connection!
int noDelay = 1; //disable Nagle algorithm: https://brooker.co.za/blog/2024/05/09/nagle.html
//e.g. test case "website sync": 23% shorter comparison time!
if (::setsockopt(socket_, //_In_ SOCKET s
IPPROTO_TCP, //_In_ int level
TCP_NODELAY, //_In_ int optname
reinterpret_cast<const char*>(&noDelay), //_In_ const char* optval
sizeof(noDelay)) != 0) //_In_ int optlen
THROW_LAST_SYS_ERROR_WSA("setsockopt(TCP_NODELAY)");
}
~Socket() { closeSocket(socket_); }
SocketType get() const { return socket_; }
private:
Socket (const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
SocketType socket_ = invalidSocket;
};
//more socket helper functions:
namespace
{
size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF!
{
if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check!
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
int bytesReceived = 0;
for (;;)
{
bytesReceived = ::recv(socket, //_In_ SOCKET s
static_cast<char*>(buffer), //_Out_ char* buf
static_cast<int>(bytesToRead), //_In_ int len
0); //_In_ int flags
if (bytesReceived >= 0 || errno != EINTR)
break;
}
if (bytesReceived < 0)
THROW_LAST_SYS_ERROR_WSA("recv");
ASSERT_SYSERROR(makeUnsigned(bytesReceived) <= bytesToRead); //better safe than sorry
return bytesReceived; //"zero indicates end of file"
}
size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite) //throw SysError; may return short! CONTRACT: bytesToWrite > 0
{
if (bytesToWrite == 0)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
int bytesWritten = 0;
for (;;)
{
bytesWritten = ::send(socket, //_In_ SOCKET s
static_cast<const char*>(buffer), //_In_ const char* buf
static_cast<int>(bytesToWrite), //_In_ int len
0); //_In_ int flags
if (bytesWritten >= 0 || errno != EINTR)
break;
}
if (bytesWritten < 0)
THROW_LAST_SYS_ERROR_WSA("send");
if (bytesWritten == 0)
throw SysError(formatSystemError("send", L"", L"Zero bytes processed."));
ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry
return bytesWritten;
}
}
//initiate termination of connection by sending TCP FIN package
inline
void shutdownSocketSend(SocketType socket) //throw SysError
{
if (::shutdown(socket, SHUT_WR) != 0)
THROW_LAST_SYS_ERROR_WSA("shutdown");
}
inline
void setNonBlocking(SocketType socket, bool nonBlocking) //throw SysError
{
int flags = ::fcntl(socket, F_GETFL);
if (flags == -1)
THROW_LAST_SYS_ERROR("fcntl(F_GETFL)");
if (nonBlocking)
flags |= O_NONBLOCK;
else
flags &= ~O_NONBLOCK;
if (::fcntl(socket, F_SETFL, flags) != 0)
THROW_LAST_SYS_ERROR(nonBlocking ? "fcntl(F_SETFL, O_NONBLOCK)" : "fcntl(F_SETFL, ~O_NONBLOCK)");
}
}
#endif //SOCKET_H_23498325972583947678456437