/** @file
 *  @brief Generic TCP/IP socket based server base class.
 */
/* Copyright 2006-2023 Olly Betts
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "tcpserver.h"

#include <xapian/error.h>

#include "resolver.h"
#include "socket_utils.h"

#include "safefcntl.h"
#include "safenetdb.h"
#include "safesysexits.h"
#include "safesysselect.h"
#include "safesyssocket.h"
#include "safeunistd.h"

#ifdef __WIN32__
# include <process.h>    /* _beginthreadex, _endthreadex */
#else
# include <netinet/in.h>
# include <netinet/tcp.h>
# include <signal.h>
# include <sys/wait.h>
#endif

#include <iostream>
#include <limits>

#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>

using namespace std;

// The parent process/main thread sits in a loop which calls accept() and
// then passes the connection off to a new process/threadm so we should accept
// connections promptly and shouldn't need to handle a large backlog.
//
// We've been using 5 for this since 2006 without anyone reporting a problem.
#define LISTEN_BACKLOG 5

/** Create a listening socket ready to accept connections.
 *
 *  @param host         hostname or address to listen on or an empty string to
 *                      accept connections on any interface.
 *  @param port         TCP port to listen on.
 *  @param tcp_nodelay  If true, enable TCP_NODELAY option.
 */
static int
create_listener(const std::string& host,
                int port,
                bool tcp_nodelay)
{
    int socketfd = -1;
    int bind_errno = 0;
    for (auto&& r : Resolver(host, port, AI_PASSIVE)) {
        int socktype = r.ai_socktype | SOCK_CLOEXEC;
        int fd = socket(r.ai_family, socktype, r.ai_protocol);
        if (fd == -1)
            continue;

#if !defined __WIN32__ && defined F_SETFD && defined FD_CLOEXEC
        // We can't use a preprocessor check on the *value* of SOCK_CLOEXEC as
        // on Linux SOCK_CLOEXEC is an enum, with '#define SOCK_CLOEXEC
        // SOCK_CLOEXEC' to allow '#ifdef SOCK_CLOEXEC' to work.
        if (SOCK_CLOEXEC == 0)
            (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
#endif

        int on = 1;
        if (tcp_nodelay) {
            // 4th argument might need to be void* or char* - cast it to char*
            // since C++ allows implicit conversion to void* but not from
            // void*.
            if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,
                           reinterpret_cast<char*>(&on),
                           sizeof(on)) < 0) {
                int setsockopt_errno = socket_errno();
                CLOSESOCKET(fd);
                throw Xapian::NetworkError("setsockopt TCP_NODELAY failed",
                                           setsockopt_errno);
            }
        }

#ifndef __WIN32__
        // On Unix, we use SO_REUSEADDR so that we can bind to a port which was
        // previously in use and still has old connections in the TIME_WAIT
        // state.  Without this it may not be possible to restart a server
        // without downtime.
        //
        // It seems that the default Microsoft behaviour is to allow bindings
        // to a port with old connections in the TIME_WAIT state, so we don't
        // need to do anything special here.
        //
        // We definitely want to avoid SO_REUSEADDR on Microsoft Windows as
        // it was implemented with stupid and dangerous semantics which allowed
        // binding to a port which was already bound and listening, and even
        // worse is that this affected *any* listening process, even if didn't
        // use SO_REUSEADDR itself.  This serious security problem existed for
        // many years but Microsoft finally addressed it and the situation
        // with Microsoft Windows versions that are still relevant is that we
        // don't need to worry about another process hijacking our listening
        // socket, but we still don't want to use SO_REUSEADDR.
        //
        // There's also the Microsoft-specific SO_EXCLUSIVEADDRUSE, but this
        // doesn't seem to affect the TIME_WAIT situation.
        //
        // We used to try to set it in combination with SO_REUSEADDR thinking
        // this tempered the problems, but you can't set both on the same
        // socket and the attempt to set SO_EXCLUSIVEADDRUSE was failing with
        // WSAEINVAL which we ignored (to support OS versions from before it
        // was added), so we've never actual set SO_EXCLUSIVEADDRUSE in any
        // released version of Xapian.
        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
                       reinterpret_cast<char*>(&on),
                       sizeof(on)) < 0) {
            int setsockopt_errno = socket_errno();
            CLOSESOCKET(fd);
            throw Xapian::NetworkError("setsockopt SO_REUSEADDR failed",
                                       setsockopt_errno);
        }
#endif

        if (::bind(fd, r.ai_addr, r.ai_addrlen) == 0) {
            socketfd = fd;
            break;
        }

        // Note down the error code for the first address we try, which seems
        // likely to be more helpful than the last in the case where they
        // differ.
        if (bind_errno == 0)
            bind_errno = socket_errno();

        CLOSESOCKET(fd);
    }

    if (socketfd < 0) {
#ifdef __WIN32__
        // On __WIN32__ we can get WSAEACCESS instead of EADDRINUSE in
        // some cases, but there are no privileged ports so we can just
        // treat it the same as EADDRINUSE.
        if (bind_errno == WSAEACCES) bind_errno = EADDRINUSE;
#endif
        if (bind_errno == EADDRINUSE) {
address_in_use:
            cerr << host << ':' << port << " already in use\n";
            // EX_UNAVAILABLE is 69.  Scripts can use this to detect that the
            // requested port was already in use.
            exit(EX_UNAVAILABLE);
        }
#ifndef __WIN32__
        // No privileged ports on __WIN32__.
        if (bind_errno == EACCES) {
            cerr << "Can't bind to privileged port " << port << '\n';
            // EX_NOPERM is 77.  Scripts can use this to detect if
            // xapian-tcpsrv failed to bind to the requested port.
            exit(EX_NOPERM);
        }
#endif
        throw Xapian::NetworkError("bind failed", bind_errno);
    }

    if (listen(socketfd, LISTEN_BACKLOG) < 0) {
        int listen_errno = socket_errno();
        CLOSESOCKET(socketfd);
        if (listen_errno == EADDRINUSE) {
            // The Linux listen(2) man page documents:
            //
            // EADDRINUSE  Another socket is already listening on the same port
            //
            // I'm not sure this is actually possible, but it's not hard to
            // handle it suitably in case it is.
            goto address_in_use;
        }
        throw Xapian::NetworkError("listen failed", listen_errno);
    }

    return socketfd;
}

TcpServer::TcpServer(const std::string& host,
                     int port,
                     bool tcp_nodelay,
                     bool verbose_)
    : listener(create_listener(host, port, tcp_nodelay)),
      verbose(verbose_)
{ }

int
TcpServer::accept_connection()
{
#ifndef __WIN32__
    // We want to be able to report when a client disconnects, but it's better
    // to do so in the main process to avoid problems with output getting
    // interleaved.  That means we need to be able to take action if either
    // a new connection is made or an existing connection goes away.
    //
    // To achieve this we use select() to wait until there's a connection to
    // accept(), or we're interrupted by a signal (we'll get SIGCHLD when a
    // child process servicing a client exits).
    //
    // We can rely on socketfd being small and so <= FD_SETSIZE so select()
    // should work fine here.
    fd_set fds;
    FD_ZERO(&fds);
    while (true) {
        FD_SET(listener, &fds);
        if (select(listener + 1, &fds, nullptr, nullptr, nullptr) > 0) {
            // There's a connection waiting to be accepted.
            break;
        }

        // Reap and report any zombie children.
        int status;
        while (waitpid(-1, &status, WNOHANG) > 0) {
            if (verbose) cout << "Connection closed.\n";
        }
    }
#endif

    struct sockaddr_storage client_address;
    SOCKLEN_T client_address_size = sizeof(client_address);
    int connection = accept(listener,
                            reinterpret_cast<sockaddr*>(&client_address),
                            &client_address_size);

    if (connection < 0) {
        int accept_errno = socket_errno();
#ifdef __WIN32__
        if (accept_errno == WSAEINTR) {
            // Our CtrlHandler function closed the socket.
            return -1;
        }
#endif
        throw Xapian::NetworkError("accept failed", accept_errno);
    }

    if (verbose) {
        char host[PRETTY_IP6_LEN];
        int port = pretty_ip6(&client_address, host);
        if (port >= 0) {
            cout << host << ':' << port << " connected\n";
        } else {
            cout << "Unknown host connected\n";
        }
    }

    return connection;
}

TcpServer::~TcpServer()
{
    CLOSESOCKET(listener);
}

#ifdef HAVE_FORK
// A fork() based implementation.

extern "C" {

static void
sigterm_handler(int)
{
    signal(SIGTERM, SIG_DFL);
    // Terminate all processes in the same process group.
    kill(0, SIGTERM);
}

}

void
TcpServer::run()
{
    // Handle connections until shutdown.

    // Put this process into its own process group so that we can easily kill
    // any child processes on SIGTERM.
    setpgid(0, 0);

    // Set up signal handler.
    signal(SIGTERM, sigterm_handler);

    while (true) {
        try {
            int connected_socket = accept_connection();
            pid_t pid = fork();
            if (pid == 0) {
                // Child process.
                close(listener);

                handle_one_connection(connected_socket);
                close(connected_socket);
                _exit(0);
            }

            // Parent process.

            if (pid < 0) {
                // fork() failed.
                int fork_errno = errno;
                close(connected_socket);
                throw Xapian::NetworkError("fork failed", fork_errno);
            }

            close(connected_socket);
        } catch (const Xapian::Error& e) {
            cerr << "Caught " << e.get_description() << '\n';
        } catch (...) {
            cerr << "Caught unknown exception\n";
        }
    }
}

#elif defined __WIN32__

// A threaded, Windows specific, implementation.

/** The socket which will be closed by CtrlHandler.
 *
 *  FIXME - is there any way to avoid using a global variable here?
 */
static const int* pShutdownSocket = NULL;

/// Console interrupt handler.
static BOOL
CtrlHandler(DWORD fdwCtrlType)
{
    switch (fdwCtrlType) {
        case CTRL_C_EVENT:
        case CTRL_CLOSE_EVENT:
            //  Console is about to die.
            // CTRL_CLOSE_EVENT gives us 5 seconds before displaying a
            // confirmation dialog asking if we really are sure.
        case CTRL_LOGOFF_EVENT:
        case CTRL_SHUTDOWN_EVENT:
            // These 2 will probably need to change when we get service
            // support - the service will prevent these being seen, so only
            // apply interactively.
            cout << "Shutting down...\n";
            break; // default behaviour
        case CTRL_BREAK_EVENT:
            // This (probably) means the developer is struggling to get
            // things to behave, and really wants to shutdown so let the OS
            // handle Ctrl+Break in the default way.
            cout << "Ctrl+Break: aborting process\n";
            return FALSE;
        default:
            cerr << "unexpected CtrlHandler: " << fdwCtrlType << '\n';
            return FALSE;
    }

    // Note: close() does not cause a blocking accept() call to terminate.
    // However, it appears closesocket() does.  This is much easier than trying
    // to setup a non-blocking accept().
    if (!pShutdownSocket || closesocket(*pShutdownSocket) == SOCKET_ERROR) {
        // We failed to close the socket, so just let the OS handle the
        // event in the default way.
        return FALSE;
    }

    pShutdownSocket = NULL;
    return TRUE; // Tell the OS that we've handled the event.
}

/// Structure which is used to pass parameters to the new threads.
struct thread_param
{
    thread_param(TcpServer* s, int c) : server(s), connected_socket(c) {}
    TcpServer* server;
    int connected_socket;
};

/// The thread entry-point.
static unsigned __stdcall
run_thread(void* param_)
{
    thread_param* param(reinterpret_cast<thread_param*>(param_));
    int socket = param->connected_socket;

    TcpServer* tcp_server = param->server;
    tcp_server->handle_one_connection(socket);
    closesocket(socket);

    if (tcp_server->get_verbose()) cout << "Connection closed.\n";

    delete param;

    _endthreadex(0);
    return 0;
}

void
TcpServer::run()
{
    // Handle connections until shutdown.

    // Set up the shutdown handler - this is a bit hacky, and sadly involves
    // a global variable.
    pShutdownSocket = &listener;
    if (!::SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE))
        throw Xapian::NetworkError("Failed to install shutdown handler");

    while (true) {
        try {
            int connected_socket = accept_connection();
            if (connected_socket == -1)
               return; // Shutdown has happened

            // Spawn a new thread to handle the connection.
            // (This seems like lots of hoops just to end up calling
            // this->handle_one_connection() on a new thread. There might be a
            // better way...)
            thread_param* param = new thread_param(this, connected_socket);
            HANDLE hthread = (HANDLE)_beginthreadex(NULL, 0, ::run_thread,
                                                    param, 0, NULL);
            if (hthread == 0) {
                // errno holds the error code from _beginthreadex, and
                // closesocket() doesn't set errno.
                closesocket(connected_socket);
                throw Xapian::NetworkError("_beginthreadex failed", errno);
            }

            // FIXME: keep track of open thread handles so we can gracefully
            // close each thread down.  OTOH, when we want to kill them all its
            // likely to mean the process is on its way down, so it doesn't
            // really matter...
            CloseHandle(hthread);
        } catch (const Xapian::Error& e) {
            cerr << "Caught " << e.get_description() << '\n';
        } catch (...) {
            cerr << "Caught unknown exception\n";
        }
    }
}

#else
# error Neither HAVE_FORK nor __WIN32__ are defined.
#endif

void
TcpServer::run_once()
{
    // Run a single request in the current process/thread.
    int fd = accept_connection();
    handle_one_connection(fd);
    CLOSESOCKET(fd);
}
