/*
 * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
 *
 * Licensed under the Apache License 2.0 (the "License").  You may not use
 * this file except in compliance with the License.  You can obtain a copy
 * in the file LICENSE in the source distribution or at
 * https://www.openssl.org/source/license.html
 */

#include "internal/sockets.h"
#include <openssl/bio.h>
#include <openssl/err.h>
#include "internal/thread_once.h"
#include "internal/rio_notifier.h"

/*
 * Sets a socket as close-on-exec, except that this is a no-op if we are certain
 * we do not need to do this or the OS does not support the concept.
 */
static int set_cloexec(int fd)
{
#if !defined(SOCK_CLOEXEC) && defined(FD_CLOEXEC)
    return fcntl(fd, F_SETFD, FD_CLOEXEC) >= 0;
#else
    return 1;
#endif
}

#if defined(OPENSSL_SYS_WINDOWS)

static CRYPTO_ONCE ensure_wsa_startup_once = CRYPTO_ONCE_STATIC_INIT;
static int wsa_started;

static void ossl_wsa_cleanup(void)
{
    if (wsa_started) {
        wsa_started = 0;
        WSACleanup();
    }
}

DEFINE_RUN_ONCE_STATIC(do_wsa_startup)
{
    WORD versionreq = 0x0202; /* Version 2.2 */
    WSADATA wsadata;

    if (WSAStartup(versionreq, &wsadata) != 0)
        return 0;
    wsa_started = 1;
    OPENSSL_atexit(ossl_wsa_cleanup);
    return 1;
}

static ossl_inline int ensure_wsa_startup(void)
{
    return RUN_ONCE(&ensure_wsa_startup_once, do_wsa_startup);
}

#endif

#if RIO_NOTIFIER_METHOD == RIO_NOTIFIER_METHOD_SOCKET

/* Create a close-on-exec socket. */
static int create_socket(int domain, int socktype, int protocol)
{
    int fd;
#if defined(OPENSSL_SYS_WINDOWS)
    static const int on = 1;

    /*
     * Use WSASocketA to create a socket which is immediately marked as
     * non-inheritable, avoiding race conditions if another thread is about to
     * call CreateProcess.
     * NOTE: windows xp (0x501) doesn't support the non-inheritance flag here
     * but preventing inheritance isn't mandatory, just a safety precaution
     * so we can get away with not including it for older platforms
     */

#ifdef WSA_FLAG_NO_HANDLE_INHERIT
    fd = (int)WSASocketA(domain, socktype, protocol, NULL, 0,
        WSA_FLAG_NO_HANDLE_INHERIT);

    /*
     * Its also possible that someone is building a binary on a newer windows
     * SDK, but running it on a runtime that doesn't support inheritance
     * suppression.  In that case the above will return INVALID_SOCKET, and
     * our response for those older platforms is to try the call again
     * without the flag
     */
    if (fd == INVALID_SOCKET)
        fd = (int)WSASocketA(domain, socktype, protocol, NULL, 0, 0);
#else
    fd = (int)WSASocketA(domain, socktype, protocol, NULL, 0, 0);
#endif
    if (fd == INVALID_SOCKET) {
        int err = get_last_socket_error();

        ERR_raise_data(ERR_LIB_SYS, err,
            "calling WSASocketA() = %d", err);
        return INVALID_SOCKET;
    }

    /* Prevent interference with the socket from other processes on Windows. */
    if (setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (void *)&on, sizeof(on)) < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_socket_error(),
            "calling setsockopt()");
        BIO_closesocket(fd);
        return INVALID_SOCKET;
    }

#else
#if defined(SOCK_CLOEXEC)
    socktype |= SOCK_CLOEXEC;
#endif

    fd = BIO_socket(domain, socktype, protocol, 0);
    if (fd == INVALID_SOCKET) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_socket()");
        return INVALID_SOCKET;
    }

    /*
     * Make socket close-on-exec unless this was already done above at socket
     * creation time.
     */
    if (!set_cloexec(fd)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling set_cloexec()");
        BIO_closesocket(fd);
        return INVALID_SOCKET;
    }
#endif

    return fd;
}

/*
 * The SOCKET notifier method manually creates a connected TCP socket pair by
 * temporarily creating a TCP listener on a random port and connecting back to
 * it.
 *
 * Win32 does not support socketpair(2), and Win32 pipes are not compatible with
 * Winsock select(2). This means our only means of making select(2) wakeable is
 * to artificially create a loopback TCP connection and send bytes to it.
 */
int ossl_rio_notifier_init(RIO_NOTIFIER *nfy)
{
    int rc, lfd = -1, rfd = -1, wfd = -1;
    struct sockaddr_in sa = { 0 }, accept_sa;
    socklen_t sa_len = sizeof(sa), accept_sa_len = sizeof(accept_sa);

#if defined(OPENSSL_SYS_WINDOWS)
    if (!ensure_wsa_startup()) {
        ERR_raise_data(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR,
            "Cannot start Windows sockets");
        return 0;
    }
#endif
    /* Create a close-on-exec socket. */
    lfd = create_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (lfd == INVALID_SOCKET) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling create_socket()");
        return 0;
    }

    /* Bind the socket to a random loopback port. */
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    rc = bind(lfd, (const struct sockaddr *)&sa, sizeof(sa));
    if (rc < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling bind()");
        goto err;
    }

    /* Determine what random port was allocated. */
    rc = getsockname(lfd, (struct sockaddr *)&sa, &sa_len);
    if (rc < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling getsockname()");
        goto err;
    }

    /* Start listening. */
    rc = listen(lfd, 1);
    if (rc < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling listen()");
        goto err;
    }

    /* Create another socket to connect to the listener. */
    wfd = create_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (wfd == INVALID_SOCKET) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling create_socket()");
        goto err;
    }

    /*
     * Disable Nagle's algorithm on the writer so that wakeups happen
     * immediately.
     */
    if (!BIO_set_tcp_ndelay(wfd, 1)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_set_tcp_ndelay()");
        goto err;
    }

    /*
     * Connect the writer to the listener.
     */
    rc = connect(wfd, (struct sockaddr *)&sa, sizeof(sa));
    if (rc < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling connect()");
        goto err;
    }

    /*
     * The connection accepted from the listener is the read side.
     */
    rfd = accept(lfd, (struct sockaddr *)&accept_sa, &accept_sa_len);
    if (rfd == INVALID_SOCKET) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling accept()");
        goto err;
    }

    rc = getsockname(wfd, (struct sockaddr *)&sa, &sa_len);
    if (rc < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling getsockname()");
        goto err;
    }

    /* Close the listener, which we don't need anymore. */
    BIO_closesocket(lfd);
    lfd = -1;

    /*
     * Sanity check - ensure someone else didn't connect to our listener during
     * the brief window of possibility above.
     */
    if (accept_sa.sin_family != AF_INET || accept_sa.sin_port != sa.sin_port) {
        ERR_raise_data(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR,
            "connected address differs from accepted address");
        goto err;
    }

    /* Make both sides of the connection non-blocking. */
    if (!BIO_socket_nbio(rfd, 1)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_socket_nbio()");
        goto err;
    }

    if (!BIO_socket_nbio(wfd, 1)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_socket_nbio()");
        goto err;
    }

    nfy->rfd = rfd;
    nfy->wfd = wfd;
    return 1;

err:
    if (lfd != INVALID_SOCKET)
        BIO_closesocket(lfd);
    if (wfd != INVALID_SOCKET)
        BIO_closesocket(wfd);
    if (rfd != INVALID_SOCKET)
        BIO_closesocket(rfd);
    return 0;
}

#elif RIO_NOTIFIER_METHOD == RIO_NOTIFIER_METHOD_SOCKETPAIR

int ossl_rio_notifier_init(RIO_NOTIFIER *nfy)
{
    int fds[2], domain = AF_INET, type = SOCK_STREAM;

#if defined(SOCK_CLOEXEC)
    type |= SOCK_CLOEXEC;
#endif
#if defined(SOCK_NONBLOCK)
    type |= SOCK_NONBLOCK;
#endif

#if defined(OPENSSL_SYS_UNIX) && defined(AF_UNIX)
    domain = AF_UNIX;
#endif

    if (socketpair(domain, type, 0, fds) < 0) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling socketpair()");
        return 0;
    }

    if (!set_cloexec(fds[0]) || !set_cloexec(fds[1])) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling set_cloexec()");
        goto err;
    }

#if !defined(SOCK_NONBLOCK)
    if (!BIO_socket_nbio(fds[0], 1) || !BIO_socket_nbio(fds[1], 1)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_socket_nbio()");
        goto err;
    }
#endif

    if (domain == AF_INET && !BIO_set_tcp_ndelay(fds[1], 1)) {
        ERR_raise_data(ERR_LIB_SYS, get_last_sys_error(),
            "calling BIO_set_tcp_ndelay()");
        goto err;
    }

    nfy->rfd = fds[0];
    nfy->wfd = fds[1];
    return 1;

err:
    BIO_closesocket(fds[1]);
    BIO_closesocket(fds[0]);
    return 0;
}

#endif

void ossl_rio_notifier_cleanup(RIO_NOTIFIER *nfy)
{
    if (nfy->rfd < 0)
        return;

    BIO_closesocket(nfy->wfd);
    BIO_closesocket(nfy->rfd);
    nfy->rfd = nfy->wfd = -1;
}

int ossl_rio_notifier_signal(RIO_NOTIFIER *nfy)
{
    static const unsigned char ch = 0;
    ossl_ssize_t wr;

    do
        /*
         * Note: If wr returns 0 the buffer is already full so we don't need to
         * do anything.
         */
        wr = writesocket(nfy->wfd, (void *)&ch, sizeof(ch));
    while (wr < 0 && get_last_socket_error_is_eintr());

    return 1;
}

int ossl_rio_notifier_unsignal(RIO_NOTIFIER *nfy)
{
    unsigned char buf[16];
    ossl_ssize_t rd;

    /*
     * signal() might have been called multiple times. Drain the buffer until
     * it's empty.
     */
    do
        rd = readsocket(nfy->rfd, (void *)buf, sizeof(buf));
    while (rd == sizeof(buf)
        || (rd < 0 && get_last_socket_error_is_eintr()));

    if (rd < 0 && !BIO_fd_non_fatal_error(get_last_socket_error()))
        return 0;

    return 1;
}
