//
//  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 ;
}