//
// File: hzTcpClient.cpp
//
// Legal Notice: This file is part of the HadronZoo C++ Class Library. Copyright 2025 HadronZoo Project (http://www.hadronzoo.com)
//
// The HadronZoo C++ Class Library is free software: You can redistribute it, and/or modify it under the terms of the GNU Lesser General Public License, as published by the Free
// Software Foundation, either version 3 of the License, or any later version.
//
// The HadronZoo C++ Class Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with the HadronZoo C++ Class Library. If not, see http://www.gnu.org/licenses.
//
//
// Implimentation of the hzTcpClient class
//
#include <cstdio>
#include <string>
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <errno.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include "hzProcess.h"
#include "hzTcpClient.h"
hzEcode hzTcpClient::ConnectIP (const hzIpaddr& ipa, uint32_t nPort, uint32_t nTimeoutR, uint32_t nTimeoutS)
{
// Establish a standard, non-local, non-SSL TCP connection to a server at a known IP address.
//
// Arguments: 1) hostname The server name or IP address
// 2) nPort The port number
// 3) nTimoutR Socket option read timeout (default 30 seconds)
// 4) nTimoutW Socket option write timeout (default 30 seconds)
//
// Returns: E_NOSOCKET If a socket could not be obtained
// E_HOSTFAIL If no connection could be established or if socket options were not set.
// E_OK If a connection to the host was established
_hzfunc("hzTcpClient::ConnectIP") ;
hzEcode rc = E_OK ; // Return code
// Check we are not already connected
if (m_nSock)
{
//if (m_Hostname == hostname && m_nPort == nPort)
if (m_nPort == nPort)
return E_OK ;
m_Hostname.Clear() ;
m_pHost = 0 ;
Close() ;
}
// Create the socket
m_nPort = nPort ;
memset(&m_SvrAddr, 0, sizeof(m_SvrAddr)) ;
m_SvrAddr.sin_family = AF_INET ;
memcpy(&m_SvrAddr.sin_addr, m_pHost->h_addr, m_pHost->h_length) ;
m_SvrAddr.sin_port = htons(nPort) ;
if ((m_nSock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return hzerr(E_NOSOCKET, "Could not create socket (errno=%d)", errno) ;
// Connect as client to host
if (connect(m_nSock, (SOCKADDR*) &m_SvrAddr, sizeof(m_SvrAddr)) < 0)
return hzerr(E_HOSTFAIL, "Could not connect to host [%s] on port %d (errno=%d)", *m_Hostname, m_nPort, errno) ;
// Apply timeouts
rc = SetRecvTimeout(nTimeoutR) ;
if (rc == E_OK)
rc = SetSendTimeout(nTimeoutR) ;
return E_OK ;
}
hzEcode hzTcpClient::ConnectStd (const char* hostname, uint32_t nPort, uint32_t nTimeoutR, uint32_t nTimeoutS)
{
// Establish a standard, non-local, non-SSL TCP connection to a server. Note that to re-connect in the event of a drop-out, just call this function again.
// A subsequent call will not repeat the DNS query unless it is to a different hostname. Note also that this function will do nothing if called with the
// same hostname, no error flag has been set and the socket is non-zero. The error flag is set by send or recv errors.
//
// Arguments: 1) hostname The server name or IP address
// 2) nPort The port number
// 3) nTimoutR Socket option read timeout (default 30 seconds)
// 4) nTimoutW Socket option write timeout (default 30 seconds)
//
// Returns: E_DNS_NOHOST If the domain does not exist
// E_DNS_FAILED If the domain settings were invalid
// E_DNS_NODATA If the domain exists but no server found
// E_DNS_RETRY If the DNS was busy
// E_NOSOCKET If a socket could not be obtained
// E_HOSTFAIL If no connection could be established or if socket options were not set.
// E_OK If a connection to the host was established
_hzfunc("hzTcpClient::ConnectStd") ;
hzEcode rc = E_OK ; // Return code
// Check we are not already connected
if (m_nSock)
{
if (m_Hostname == hostname && m_nPort == nPort)
return E_OK ;
m_Hostname.Clear() ;
m_pHost = 0 ;
Close() ;
}
if (m_Hostname && m_Hostname != hostname)
{
// At the point of call there was no socket but this hzTcpClient instance has a hostname from a previous connection. If this differs from the host now
// being sought then the m_pHost value will be invalid and a fresh call to gethostbyname is required.
m_Hostname = hostname ;
m_pHost = 0 ;
}
if (!m_Hostname)
m_Hostname = hostname ;
// If we have not got the hostname from a previous connect, get the hostname now
if (!m_pHost)
{
m_pHost = gethostbyname(hostname) ;
if (!m_pHost)
{
if (h_errno == TRY_AGAIN) return E_DNS_RETRY ;
if (h_errno == HOST_NOT_FOUND) return E_DNS_NOHOST ;
if (h_errno == NO_RECOVERY) return E_DNS_FAILED ;
if (h_errno == NO_DATA || h_errno == NO_ADDRESS)
return E_DNS_NODATA ;
m_Hostname.Clear() ;
return hzerr(E_DNS_NOHOST, "Unknown Host [%s]\n", hostname) ;
}
}
// Create the socket
m_nPort = nPort ;
memset(&m_SvrAddr, 0, sizeof(m_SvrAddr)) ;
m_SvrAddr.sin_family = AF_INET ;
memcpy(&m_SvrAddr.sin_addr, m_pHost->h_addr, m_pHost->h_length) ;
m_SvrAddr.sin_port = htons(nPort) ;
if ((m_nSock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return hzerr(E_NOSOCKET, "Could not create socket (errno=%d)", errno) ;
// Connect stage
if (connect(m_nSock, (SOCKADDR*) &m_SvrAddr, sizeof(m_SvrAddr)) < 0)
return hzerr(E_HOSTFAIL, "Could not connect to host [%s] on port %d (errno=%d)", *m_Hostname, m_nPort, errno) ;
// Apply timeouts
rc = SetRecvTimeout(nTimeoutR) ;
if (rc == E_OK)
rc = SetSendTimeout(nTimeoutR) ;
return E_OK ;
}
hzEcode hzTcpClient::ConnectSSL (const char* hostname, uint32_t nPort, uint32_t nTimeoutR, uint32_t nTimeoutS)
{
// Purpose: Establish an SSL TCP connection to a server
//
// Arguments: 1) hostname The server name or IP address
// 2) nPort The port number
// 3) nTimeoutR Timeout (Recv)
// 4) nTimeoutS Timeout (Send)
//
// Returns: E_DNS_NOHOST If the domain does not exist
// E_DNS_FAILED If the domain settings were invalid
// E_DNS_NODATA If the domain exists but no server found
// E_DNS_RETRY If the DNS was busy
// E_INITFAIL If the SSL client side settings fail
// E_NOSOCKET If a socket could not be obtained
// E_HOSTFAIL If no connection could be established or if socket options were not set.
// E_OK If a connection to the host was established
_hzfunc("hzTcpClient::ConnectSSL") ;
static bool bBeenHere = false ; // OPENSSL init state
const SSL_METHOD* sslMethod = 0 ; // SSL client method
SSL_CTX* sslCtx = 0 ; // SSL client CTX structure
int32_t sys_rc ; // Return from connect call
hzEcode rc = E_OK ; // Return code
// Ensure SSL/TLS is initialized
if (!bBeenHere)
{
#if OPENSSL_VERSION_NUMBER < 0x10100000L
SSL_library_init();
#else
OPENSSL_init_ssl(0, NULL);
#endif
bBeenHere = true ;
}
// Check we are not already connected
if (m_nSock)
{
if (m_Hostname == hostname && m_nPort == nPort)
return E_OK ;
m_Hostname.Clear() ;
m_pHost = 0 ;
Close() ;
}
if (m_Hostname && m_Hostname != hostname)
{
// At the point of call there was no socket but this hzTcpClient instance has a hostname from a previous connection. If this differs from the host now
// being sought then the m_pHost value will be invalid and a fresh call to gethostbyname is required.
m_Hostname = hostname ;
m_pHost = 0 ;
}
// Get the host IP
m_pHost = gethostbyname(hostname) ;
if (!m_pHost)
{
threadLog("No Host found\n") ;
if (h_errno == TRY_AGAIN) return E_DNS_RETRY ;
if (h_errno == HOST_NOT_FOUND) return E_DNS_NOHOST ;
if (h_errno == NO_RECOVERY) return E_DNS_FAILED ;
if (h_errno == NO_DATA || h_errno == NO_ADDRESS)
return E_DNS_NODATA ;
m_Hostname.Clear() ;
return hzerr(E_DNS_NOHOST, "Unknown Host [%s]\n", hostname) ;
}
m_Hostname = hostname ;
m_nPort = nPort ;
// Create the socket
memset(&m_SvrAddr, 0, sizeof(m_SvrAddr)) ;
m_SvrAddr.sin_family = AF_INET ;
memcpy(&m_SvrAddr.sin_addr, m_pHost->h_addr, m_pHost->h_length) ;
m_SvrAddr.sin_port = htons(nPort) ;
if ((m_nSock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return hzerr(E_INITFAIL, "Could not create socket (returns %d, errno=%d)", m_nSock, errno) ;
threadLog("Using socket %d\n", m_nSock) ;
// Establish a standard, non-SSL TCP/IP connection
sys_rc = connect(m_nSock, (SOCKADDR*) &m_SvrAddr, sizeof(m_SvrAddr)) ;
if (sys_rc < 0)
{
threadLog("Could not connect to host (returns %d)", sys_rc) ;
return hzerr(E_HOSTFAIL, "Could not connect to host [%s] on port %d (errno=%d)", *m_Hostname, m_nPort, errno) ;
}
threadLog("Connected\n") ;
// Apply timeouts
if (!nTimeoutR) nTimeoutR = 90 ;
if (!nTimeoutS) nTimeoutS = 90 ;
if (rc == E_OK) rc = SetRecvTimeout(nTimeoutR) ;
if (rc == E_OK) rc = SetSendTimeout(nTimeoutS) ;
if (rc != E_OK)
return hzerr(rc, "Could not set connection timeouts") ;
threadLog("Socket options set\n") ;
/*
** SSL/TLS part
*/
sslMethod = TLS_client_method() ;
if (!sslMethod)
{
Close() ;
return hzerr(E_INITFAIL, "No SSL Client Method issued") ;
}
threadLog("Method set\n") ;
sslCtx = SSL_CTX_new(sslMethod) ;
if (!sslCtx)
{
Close() ;
hzerr(E_INITFAIL, "No SSL Structure issued") ;
}
threadLog("CTX Created\n") ;
m_pSSL = SSL_new(sslCtx) ;
if (!m_pSSL)
{
threadLog("Could not allocate SSL structure\n") ;
return E_HOSTFAIL ;
}
threadLog("SSL allocated\n") ;
/*
if (SSL_CTX_set_cipher_list(sslCtx, "ECDHE-RSA-AES128-GCM-SHA256") <= 0)
{
threadLog("Error setting the cipher list.\n");
return E_HOSTFAIL ;
}
threadLog("Cipers Listed\n") ;
*/
sys_rc = SSL_set_fd(m_pSSL, m_nSock);
if (sys_rc != 1)
{
threadLog("Could not set SSL file descriptor\n") ;
return E_HOSTFAIL ;
}
threadLog("SSL fd set\n") ;
SSL_set_tlsext_host_name(m_pSSL, *m_Hostname) ;
SSL_set_connect_state(m_pSSL) ;
threadLog("SSL set as client\n") ;
sys_rc = SSL_connect(m_pSSL);
//sys_rc = SSL_do_handshake(m_pSSL);
threadLog("Handshake done\n") ;
if (sys_rc != 1)
{
threadLog("Could not connect: %s\n", ShowErrorSSL(SSL_get_error(m_pSSL, sys_rc))) ;
return E_HOSTFAIL ;
}
threadLog("Connected Secure\n") ;
/*
Move to separate function or scrap
if (bCheckCert)
{
if (SSL_get_peer_certificate(m_pSSL) != NULL)
{
if (SSL_get_verify_result(m_pSSL) == X509_V_OK)
threadLog("Client verification with SSL_get_verify_result() succeeded.\n") ;
else
threadLog("Client verification with SSL_get_verify_result() failed.\n") ;
}
}
*/
return E_OK ;
}
hzEcode hzTcpClient::ConnectLoc (uint32_t nPort)
{
// Purpose: Establish a UNIX domain TCP connection to a server
//
// Arguments: 1) nPort The port number
//
// Returns: E_NOSOCKET If the socket could not be created
// E_HOSTFAIL If not connection can be established
// E_OK If operation successfull
_hzfunc("hzTcpClient::ConnectLoc") ;
// Check we are not already connected
if (m_nSock)
{
if (m_nPort == nPort)
return E_OK ;
Close() ;
}
m_Hostname = "127.0.0.1" ;
m_nPort = nPort ;
// Get the hostname
if (!(m_pHost = gethostbyname(*m_Hostname)))
{
threadLog("Unknown Host [%s]\n", *m_Hostname) ;
return E_HOSTFAIL ;
}
// Create the socket
memset(&m_SvrAddr, 0, sizeof(m_SvrAddr)) ;
m_SvrAddr.sin_family = AF_UNIX ;
memcpy(&m_SvrAddr.sin_addr, m_pHost->h_addr, m_pHost->h_length) ;
m_SvrAddr.sin_port = htons(nPort) ;
if ((m_nSock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return hzerr(E_NOSOCKET, "Could not create socket (returns %d, errno %d)", m_nSock, errno) ;
// Connect stage
if (connect(m_nSock, (struct sockaddr *) &m_SvrAddr, sizeof(m_SvrAddr)) < 0)
return hzerr(E_HOSTFAIL, "Could not connect to host (errno=%d)", errno) ;
if (m_nSock <= 0)
return hzerr(E_HOSTFAIL, "Unspecified error. Socket is %d\n", m_nSock) ;
return E_OK ;
}
hzEcode hzTcpClient::SetSendTimeout (uint32_t nInterval)
{
// Set the socket options so that the timeout for outgoing packets is set to the supplied interval
//
// Arguments: 1) nInterval Timeout interval in seconds
//
// Returns: E_HOSTFAIL If the timeout could not be set
// E_OK If the timeout was successfully set
timeval tv ; // Timeout structure
tv.tv_sec = nInterval > 0 ? nInterval : 30 ;
tv.tv_usec = 0 ;
if (setsockopt(m_nSock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0)
return E_HOSTFAIL ;
return E_OK ;
}
hzEcode hzTcpClient::SetRecvTimeout (uint32_t nInterval)
{
// Set the socket options so that the timeout for receiving packets is set to the supplied interval
//
// Arguments: 1) nInterval Timeout interval in seconds
//
// Returns: E_HOSTFAIL If the timeout could not be set
// E_OK If the timeout was successfully set
struct timeval tv ; // Timeout structure
tv.tv_sec = nInterval > 0 ? nInterval : 30 ;
tv.tv_usec = 0 ;
if (setsockopt(m_nSock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
return E_HOSTFAIL ;
return E_OK ;
}
void hzTcpClient::Show (hzChain& Z)
{
// Diagnostic purposes. Show details for host if currently connected
//
// Arguments: 1) Z Chain to aggregate report to
//
// Returns: None
if (!m_pHost)
Z << "hzTcpClient::Show: Not Connected\n" ;
else
{
Z.Printf("hzTcpClient::Show: Connected to: %u.%u.%u.%u %s (type %d, len %d)\n",
m_pHost->h_addr[0], m_pHost->h_addr[1], m_pHost->h_addr[2], m_pHost->h_addr[3],
m_pHost->h_name, m_pHost->h_addrtype, m_pHost->h_length) ;
Z.Printf(" (alias) %s\n", m_pHost->h_aliases[0]) ;
Z.Printf(" (addr) %s\n", m_pHost->h_addr_list[0] + 4) ;
Z << "------\n" ;
}
}
hzEcode hzTcpClient::Send (const void* pIn, uint32_t nLen)
{
// Purpose: Write buffer content to a socket
//
// Arguments: 1) pIn The buffer (void*)
// 2) nLen Number of bytes to send
//
// Returns: E_NOSOCKET If the connection is not open
// E_WRITEFAIL If the number of bytes written falls short of the intended number. In this event the connection is closed.
// E_OK If the operation is successfull.
_hzfunc("hzTcpClient::Send(void*,uint32_t)") ;
uint32_t nSent ; // Bytes actually sent
if (!m_nSock) return E_NOSOCKET ;
if (nLen == 0) return E_OK ;
if (m_pSSL)
//nSent = SSL_write(m_pSSL, (const char*) pIn, nLen) ;
nSent = SSL_write(m_pSSL, pIn, nLen) ;
else
//nSent = write(m_nSock, (const char*) pIn, nLen) ;
nSent = write(m_nSock, pIn, nLen) ;
if (nSent == 0)
{
threadLog("Socket timed out while writing to server %s port %d socket %d", *m_Hostname, m_nPort, m_nSock) ;
return E_TIMEOUT ;
}
if (nSent != nLen)
{
Close() ;
threadLog("Socket write error to server %s port %d socket %d", *m_Hostname, m_nPort, m_nSock) ;
return E_WRITEFAIL ;
}
return E_OK ;
}
hzEcode hzTcpClient::Send (const hzChain& C)
{
// Purpose: Write chain content to a socket
//
// Arguments: 1) C The chain to send. No size indicator is needed as whole chain is sent.
//
// Returns: E_NOSOCKET If the connection is not open
// E_WRITEFAIL If the number of bytes written falls short of the intended number. In this event the connection is closed.
// E_TIMEOUT If the operation timed out.
// E_OK If the operation is successfull.
_hzfunc("hzTcpClient::Send(hzChain&)") ;
chIter ci ; // To iterate input chain
char* i ; // To populate output buffer
uint32_t nSend ; // Bytes to send in current packet
uint32_t nSent ; // Bytes actually sent according to write operation
uint32_t nTotal = 0 ; // Total sent so far
hzEcode rc = E_OK ; // Return code
if (!m_nSock)
return E_NOSOCKET ;
ci = C ;
for (; rc == E_OK && nTotal < C.Size() ;)
{
for (i = m_Buf, nSend = 0 ; !ci.eof() && nSend < HZ_MAXPACKET ; nSend++, ci++)
*i++ = *ci ;
if (!nSend)
break ;
// Do the send
if (m_pSSL)
nSent = SSL_write(m_pSSL, m_Buf, nSend) ;
else
nSent = write(m_nSock, m_Buf, nSend) ;
if (nSent < 0)
rc = errno == ETIMEDOUT ? E_TIMEOUT : E_SENDFAIL ;
else
nTotal += nSent ;
}
return rc ;
}
hzEcode hzTcpClient::Recv (void* vpOut, uint32_t& nRecv, uint32_t nMax)
{
// Purpose: Read from a socket into a buffer.
//
// Arguments: 1) vpOut The buffer to populate
// 2) nRecv A reference to number of bytes received
// 3) nMax The maximum number of bytes to receive
//
// Returns: E_NOSOCKET If the connection has been closed
// E_RECVFAIL If the socket read operation fails
// E_OK If operation successfull
_hzfunc("hzTcpClient::Recv(1)") ;
char* cpOut ; // Buffer recast to char*
int32_t nBytes ; // Bytes recieved in socket read
cpOut = (char*) vpOut ;
cpOut[0] = 0 ;
nRecv = 0 ;
if (!m_nSock)
return E_NOSOCKET ;
if (nMax == 0)
return E_OK ;
if (m_pSSL)
nBytes = SSL_read(m_pSSL, cpOut, nMax) ;
else
nBytes = recv(m_nSock, cpOut, nMax, 0) ;
if (nBytes < 0)
{
if (errno == EAGAIN) return E_OK ;
if (errno == ETIMEDOUT) return E_TIMEOUT ;
//Close() ;
return E_RECVFAIL ;
}
nRecv = nBytes ;
return E_OK ;
}
hzEcode hzTcpClient::Recv (hzChain& Z)
{
// Purpose: Read from a socket into the supplied chain. The function terminates when no more bytes can be read from the socket.
//
// Argument: Z The chain to populate
//
// Returns: E_NOSOCKET If the connection has been closed
// E_RECVFAIL If the socket read operation fails
// E_MEMORY If there was insufficent memory to complete the operation.
// E_OK If operation successfull
_hzfunc("hzTcpClient::Recv(2)") ;
int32_t nRecv ; // Bytes read by recv() or SSL_read()
if (!m_nSock)
return E_NOSOCKET ;
for (;;)
{
if (m_pSSL)
nRecv = SSL_read(m_pSSL, m_Buf, HZ_MAXPACKET) ;
else
nRecv = recv(m_nSock, m_Buf, HZ_MAXPACKET, 0) ;
if (!nRecv)
break ;
if (nRecv < 0)
{
if (errno == EAGAIN)
return E_TIMEOUT ;
if (errno == ETIMEDOUT)
return E_TIMEOUT ;
Close() ;
return E_RECVFAIL ;
}
Z.Append(m_Buf, nRecv) ;
}
return E_OK ;
}
void hzTcpClient::Close (void)
{
// Closes the client TCP connection. If there is an SSL connection, this is shutdown and then the handle for the SSL is freed.
//
// Arguments: None
// Returns: None
if (m_pSSL)
{
SSL_shutdown(m_pSSL) ;
SSL_free(m_pSSL) ;
m_pSSL = 0 ;
}
if (m_nSock)
close(m_nSock) ;
m_nSock = 0 ;
}