/*
 * Copyright (C) 2004-2024 ZNC, see the NOTICE file for details.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef ZNC_SOCKET_H
#define ZNC_SOCKET_H

#include <znc/zncconfig.h>
#include <znc/Csocket.h>
#include <znc/Threads.h>
#include <znc/Translation.h>

class CModule;

class CZNCSock : public Csock, protected CCoreTranslationMixin {
  public:
    CZNCSock(int timeout = 60);
    CZNCSock(const CString& sHost, u_short port, int timeout = 60);
    ~CZNCSock() {}

    int ConvertAddress(const struct sockaddr_storage* pAddr, socklen_t iAddrLen,
                       CString& sIP, u_short* piPort) const override;
#ifdef HAVE_LIBSSL
    int VerifyPeerCertificate(int iPreVerify,
                              X509_STORE_CTX* pStoreCTX) override;
    void SSLHandShakeFinished() override;
    bool CheckSSLCert(X509* pCert);
    virtual void SSLCertError(X509* pCert) {}
    bool SNIConfigureClient(CString& sHostname) override;
    CString GetSSLPeerFingerprint(X509* pCert = nullptr) const;
#else
    CString GetSSLPeerFingerprint() const { return ""; }
#endif
    void SetHostToVerifySSL(const CString& sHost) {
        m_sHostToVerifySSL = sHost;
    }
    void SetSSLTrustedPeerFingerprints(const SCString& ssFPs) {
        m_ssTrustedFingerprints = ssFPs;
    }

    void SetTrustAllCerts(bool bTrustAll) { m_bTrustAllCerts = bTrustAll; }
    bool GetTrustAllCerts() const { return m_bTrustAllCerts; }

    void SetTrustPKI(bool bTrustPKI) { m_bTrustPKI = bTrustPKI; }
    bool GetTrustPKI() const { return m_bTrustPKI; }

    void SetEncoding(const CString&);

    virtual CString GetRemoteIP() const { return Csock::GetRemoteIP(); }

  protected:
    // All existing errno codes seem to be in range 1-300
    enum {
        errnoBadSSLCert = 12569,
    };

  private:
    CString m_sHostToVerifySSL;
    SCString m_ssTrustedFingerprints;
    SCString m_ssCertVerificationErrors;
    bool m_bTrustAllCerts = false;
    bool m_bTrustPKI = true;
};

enum EAddrType { ADDR_IPV4ONLY, ADDR_IPV6ONLY, ADDR_ALL };

class CSockManager : public TSocketManager<CZNCSock>,
                     private CCoreTranslationMixin {
  public:
    CSockManager();
    virtual ~CSockManager();

    bool ListenHost(u_short iPort, const CString& sSockName,
                    const CString& sBindHost, bool bSSL = false,
                    int iMaxConns = SOMAXCONN, CZNCSock* pcSock = nullptr,
                    u_int iTimeout = 0, EAddrType eAddr = ADDR_ALL) {
        CSListener L(iPort, sBindHost);

        L.SetSockName(sSockName);
        L.SetIsSSL(bSSL);
        L.SetTimeout(iTimeout);
        L.SetMaxConns(iMaxConns);

#ifdef HAVE_IPV6
        switch (eAddr) {
            case ADDR_IPV4ONLY:
                L.SetAFRequire(CSSockAddr::RAF_INET);
                break;
            case ADDR_IPV6ONLY:
                L.SetAFRequire(CSSockAddr::RAF_INET6);
                break;
            case ADDR_ALL:
                L.SetAFRequire(CSSockAddr::RAF_ANY);
                break;
        }
#endif

        return Listen(L, pcSock);
    }

    bool ListenAll(u_short iPort, const CString& sSockName, bool bSSL = false,
                   int iMaxConns = SOMAXCONN, CZNCSock* pcSock = nullptr,
                   u_int iTimeout = 0, EAddrType eAddr = ADDR_ALL) {
        return ListenHost(iPort, sSockName, "", bSSL, iMaxConns, pcSock,
                          iTimeout, eAddr);
    }

    u_short ListenRand(const CString& sSockName, const CString& sBindHost,
                       bool bSSL = false, int iMaxConns = SOMAXCONN,
                       CZNCSock* pcSock = nullptr, u_int iTimeout = 0,
                       EAddrType eAddr = ADDR_ALL) {
        unsigned short uPort = 0;
        CSListener L(0, sBindHost);

        L.SetSockName(sSockName);
        L.SetIsSSL(bSSL);
        L.SetTimeout(iTimeout);
        L.SetMaxConns(iMaxConns);

#ifdef HAVE_IPV6
        switch (eAddr) {
            case ADDR_IPV4ONLY:
                L.SetAFRequire(CSSockAddr::RAF_INET);
                break;
            case ADDR_IPV6ONLY:
                L.SetAFRequire(CSSockAddr::RAF_INET6);
                break;
            case ADDR_ALL:
                L.SetAFRequire(CSSockAddr::RAF_ANY);
                break;
        }
#endif

        Listen(L, pcSock, &uPort);

        return uPort;
    }

    u_short ListenAllRand(const CString& sSockName, bool bSSL = false,
                          int iMaxConns = SOMAXCONN, CZNCSock* pcSock = nullptr,
                          u_int iTimeout = 0, EAddrType eAddr = ADDR_ALL) {
        return (ListenRand(sSockName, "", bSSL, iMaxConns, pcSock, iTimeout,
                           eAddr));
    }

    void Connect(const CString& sHostname, u_short iPort,
                 const CString& sSockName, int iTimeout = 60, bool bSSL = false,
                 const CString& sBindHost = "", CZNCSock* pcSock = nullptr);

    unsigned int GetAnonConnectionCount(const CString& sIP) const;
    void DelSockByAddr(Csock* pcSock) override;

  private:
    void FinishConnect(const CString& sHostname, u_short iPort,
                       const CString& sSockName, int iTimeout, bool bSSL,
                       const CString& sBindHost, CZNCSock* pcSock);

    std::map<Csock*, bool /* deleted */> m_InFlightDnsSockets;

#ifdef HAVE_PTHREAD
    class CThreadMonitorFD;
    friend class CThreadMonitorFD;
#endif
#ifdef HAVE_THREADED_DNS
    struct TDNSTask {
        TDNSTask()
            : sHostname(""),
              iPort(0),
              sSockName(""),
              iTimeout(0),
              bSSL(false),
              sBindhost(""),
              pcSock(nullptr),
              bDoneTarget(false),
              bDoneBind(false),
              aiTarget(nullptr),
              aiBind(nullptr) {}

        TDNSTask(const TDNSTask&) = delete;
        TDNSTask& operator=(const TDNSTask&) = delete;

        CString sHostname;
        u_short iPort;
        CString sSockName;
        int iTimeout;
        bool bSSL;
        CString sBindhost;
        CZNCSock* pcSock;

        bool bDoneTarget;
        bool bDoneBind;
        addrinfo* aiTarget;
        addrinfo* aiBind;
    };
    class CDNSJob : public CJob {
      public:
        CDNSJob()
            : sHostname(""),
              task(nullptr),
              pManager(nullptr),
              bBind(false),
              iRes(0),
              aiResult(nullptr) {}

        CDNSJob(const CDNSJob&) = delete;
        CDNSJob& operator=(const CDNSJob&) = delete;

        CString sHostname;
        TDNSTask* task;
        CSockManager* pManager;
        bool bBind;

        int iRes;
        addrinfo* aiResult;

        void runThread() override;
        void runMain() override;
    };
    void StartTDNSThread(TDNSTask* task, bool bBind);
    void SetTDNSThreadFinished(TDNSTask* task, bool bBind, addrinfo* aiResult);
    static void* TDNSThread(void* argument);
#endif
  protected:
};

/**
 * @class CSocket
 * @brief Base Csock implementation to be used by modules
 *
 * By all means, this class should be used as a base for sockets originating from modules. It handles removing instances of itself
 * from the module as it unloads, and simplifies use in general.
 * - EnableReadLine is default to true in this class
 * - MaxBuffer for readline is set to 10240, in the event this is reached the socket is closed (@see ReachedMaxBuffer)
 */
class CSocket : public CZNCSock {
  public:
    /**
     * @brief ctor
     * @param pModule the module this sock instance is associated to
     */
    CSocket(CModule* pModule);
    /**
     * @brief ctor
     * @param pModule the module this sock instance is associated to
     * @param sHostname the hostname being connected to
     * @param uPort the port being connected to
     * @param iTimeout the timeout period for this specific sock
     */
    CSocket(CModule* pModule, const CString& sHostname, unsigned short uPort,
            int iTimeout = 60);
    virtual ~CSocket();

    CSocket(const CSocket&) = delete;
    CSocket& operator=(const CSocket&) = delete;

    using Csock::Connect;
    using Csock::Listen;

    //! This defaults to closing the socket, feel free to override
    void ReachedMaxBuffer() override;
    void SockError(int iErrno, const CString& sDescription) override;

    //! This limits the global connections from this IP to defeat DoS attacks, feel free to override. The ACL used is provided by the main interface @see CZNC::AllowConnectionFrom
    bool ConnectionFrom(const CString& sHost, unsigned short uPort) override;

    //! Ease of use Connect, assigns to the manager and is subsequently tracked
    bool Connect(const CString& sHostname, unsigned short uPort,
                 bool bSSL = false, unsigned int uTimeout = 60);
    //! Ease of use Listen, assigned to the manager and is subsequently tracked
    bool Listen(unsigned short uPort, bool bSSL, unsigned int uTimeout = 0);

    // Getters
    CModule* GetModule() const;
    // !Getters

#ifndef SWIG
    // Translation. As opposed to CCoreTranslationMixin, this one uses module.mo
    CString t_s(const CString& sEnglish, const CString& sContext = "") const;
    CInlineFormatMessage t_f(const CString& sEnglish,
                             const CString& sContext = "") const;
    CInlineFormatMessage t_p(const CString& sEnglish, const CString& sEnglishes,
                             int iNum, const CString& sContext) const;
    CDelayedTranslation t_d(const CString& sEnglish,
                            const CString& sContext = "") const;
#endif

  private:
  protected:
    CModule*
        m_pModule;  //!< pointer to the module that this sock instance belongs to
};

/**
 * @class CIRCSocket
 * @brief Base IRC socket for client<->ZNC, and ZNC<->server
 */
class CIRCSocket : public CZNCSock {
  public:
#ifdef HAVE_ICU
    /**
     * @brief Allow IRC control characters to appear even if protocol encoding explicitly disallows them.
     *
     * E.g. ISO-2022-JP disallows 0x0F, which in IRC means "reset format",
     * so by default it gets replaced with U+FFFD ("replacement character").
     * https://code.google.com/p/chromium/issues/detail?id=277062#c3
     *
     * In case if protocol encoding uses these code points for something else, the encoding takes preference,
     * and they are not IRC control characters anymore.
     */
    void IcuExtToUCallback(UConverterToUnicodeArgs* toArgs,
                           const char* codeUnits, int32_t length,
                           UConverterCallbackReason reason,
                           UErrorCode* err) override;
    void IcuExtFromUCallback(UConverterFromUnicodeArgs* fromArgs,
                             const UChar* codeUnits, int32_t length,
                             UChar32 codePoint, UConverterCallbackReason reason,
                             UErrorCode* err) override;
#endif
};

#endif /* ZNC_SOCKET_H */
