// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// SecLink proxy  -- bridges a DOS serial connection to a telnet BBS
//
// Architecture:
//   DOS terminal <-> TCP <-> proxy <-> TCP <-> BBS
//                   secLink protocol       plain telnet
//
// The proxy runs on Linux and sits between two TCP connections:
//   1. Left side: the DOS side connects via a serial-over-TCP feature.
//      The proxy speaks the full secLink protocol (packet framing, DH
//      handshake, XTEA encryption) over this connection.
//   2. Right side: plain telnet to a BBS. The proxy handles telnet IAC
//      negotiation and strips control sequences before forwarding clean
//      data to the DOS side.
//
// This design keeps all crypto complexity on the proxy side (Linux has
// /dev/urandom for good entropy) and inside the DOS app. The BBS sees
// a normal telnet client and doesn't know encryption is involved.
//
// The socket shim (sockShim.c) makes the TCP socket look like a COM port
// to the secLink stack, allowing full code reuse of the packet and
// security layers without modification.
//
// Usage: proxy [listen_port] [bbs_host] [bbs_port]
// Defaults: 2323 10.1.0.244 2023

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <poll.h>
#include <fcntl.h>

#include "sockShim.h"
#include "../../libs/kpunch/serial/seclink/secLink.h"
#include "../../libs/kpunch/serial/security/security.h"


// ========================================================================
// Defines
// ========================================================================

#define DEFAULT_LISTEN_PORT     2323
#define DEFAULT_BBS_HOST        "10.1.0.244"
#define DEFAULT_BBS_PORT        2023

#define CHANNEL_TERMINAL        0
#define POLL_TIMEOUT_MS         10

// Telnet protocol bytes (RFC 854). The proxy must handle these because BBS
// servers send telnet negotiation sequences that would corrupt the terminal
// display if passed through raw.
#define TEL_IAC                 255
#define TEL_DONT                254
#define TEL_DO                  253
#define TEL_WONT                252
#define TEL_WILL                251
#define TEL_SB                  250
#define TEL_SE                  240

// Telnet options we accept
#define TELOPT_ECHO             1
#define TELOPT_SGA              3
#define TELOPT_TTYPE            24
#define TELOPT_NAWS             31

// Telnet parser states. The parser is a simple state machine that strips
// IAC sequences from the BBS data stream. It persists across read() calls
// (sTelState is static) because a sequence can span TCP segment boundaries.
#define TS_DATA                 0
#define TS_IAC                  1
#define TS_WILL                 2
#define TS_WONT                 3
#define TS_DO                   4
#define TS_DONT                 5
#define TS_SB                   6
#define TS_SB_IAC               7


// ========================================================================
// Static globals
// ========================================================================

static volatile bool sRunning  = true;
static volatile bool sGotEnter = false;
static int           sTelState = TS_DATA;
static int           sClientFd = -1;


// ========================================================================
// Static prototypes (alphabetical)
// ========================================================================

static int  connectToBbs(const char *host, int port);
static int  createListenSocket(int port);
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel);
static void seedRng(void);
static void sigHandler(int sig);
static int  telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out);
static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt);


// ========================================================================
// Static functions (alphabetical)
// ========================================================================

static int connectToBbs(const char *host, int port) {
    struct addrinfo  hints;
    struct addrinfo *res;
    char             portStr[16];
    int              fd;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    snprintf(portStr, sizeof(portStr), "%d", port);

    if (getaddrinfo(host, portStr, &hints, &res) != 0) {
        return -1;
    }

    fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) {
        freeaddrinfo(res);
        return -1;
    }

    if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
        close(fd);
        freeaddrinfo(res);
        return -1;
    }

    freeaddrinfo(res);
    return fd;
}


static int createListenSocket(int port) {
    struct sockaddr_in addr;
    int                fd;
    int                opt = 1;

    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        return -1;
    }

    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port        = htons(port);

    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        close(fd);
        return -1;
    }

    if (listen(fd, 1) < 0) {
        close(fd);
        return -1;
    }

    return fd;
}


// secLink receive callback  -- called when decrypted data arrives from the DOS
// terminal. Before the BBS is connected, we watch for an ENTER keypress as
// a "ready" signal so the user can see the terminal is working before we
// connect to the BBS (which would immediately start sending ANSI art).
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
    int bbsFd = *(int *)ctx;

    (void)channel;
    if (!sGotEnter) {
        for (int i = 0; i < len; i++) {
            if (data[i] == '\r' || data[i] == '\n') {
                sGotEnter = true;
                printf("Got ENTER from terminal.\n");
                break;
            }
        }
        return;
    }

    // The BBS socket may not be connected yet (bbsFd still -1); drop these
    // bytes rather than writing to an invalid fd.
    if (bbsFd < 0) {
        return;
    }

    int sent = 0;
    while (sent < len) {
        ssize_t n = write(bbsFd, data + sent, len - sent);
        if (n <= 0) {
            break;
        }
        sent += (int)n;
    }
}


// On Linux, /dev/urandom provides high-quality entropy without blocking.
// This is vastly better than the PIT-based entropy on DOS  -- one of the
// advantages of running the proxy on the host OS.
static void seedRng(void) {
    uint8_t entropy[32];
    FILE   *f;

    f = fopen("/dev/urandom", "rb");
    if (f) {
        if (fread(entropy, 1, sizeof(entropy), f) < sizeof(entropy)) {
            fprintf(stderr, "Warning: short read from /dev/urandom\n");
        }
        fclose(f);
    }

    secRngSeed(entropy, sizeof(entropy));
}


static void sigHandler(int sig) {
    (void)sig;
    sRunning = false;
    // Shutdown the client socket to break any blocking reads in the packet layer
    if (sClientFd >= 0) {
        shutdown(sClientFd, SHUT_RDWR);
    }
}


// Filter telnet IAC sequences from BBS data and handle negotiation.
// We accept ECHO and SGA (Suppress Go Ahead) because most BBSes require them
// for character-at-a-time mode. We accept TTYPE and NAWS for terminal type
// and window size negotiation. Everything else is refused. Subnegotiations
// (SB...SE) are consumed silently  -- we don't actually respond with terminal
// type or window size data, but accepting the option prevents the BBS from
// falling back to line mode.
// Returns the number of clean data bytes written to 'out'.
static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
    int outLen = 0;

    for (int i = 0; i < inLen; i++) {
        uint8_t b = in[i];

        switch (sTelState) {
            case TS_DATA:
                if (b == TEL_IAC) {
                    sTelState = TS_IAC;
                } else {
                    out[outLen++] = b;
                }
                break;

            case TS_IAC:
                switch (b) {
                    case TEL_IAC:
                        // Escaped 0xFF  -- emit literal
                        out[outLen++] = 0xFF;
                        sTelState = TS_DATA;
                        break;
                    case TEL_WILL:
                        sTelState = TS_WILL;
                        break;
                    case TEL_WONT:
                        sTelState = TS_WONT;
                        break;
                    case TEL_DO:
                        sTelState = TS_DO;
                        break;
                    case TEL_DONT:
                        sTelState = TS_DONT;
                        break;
                    case TEL_SB:
                        sTelState = TS_SB;
                        break;
                    default:
                        // Unknown command, skip
                        sTelState = TS_DATA;
                        break;
                }
                break;

            case TS_WILL:
                // Server offers to do something  -- accept ECHO and SGA, refuse others
                printf("  TEL RX: WILL %d\n", b);
                if (b == TELOPT_ECHO || b == TELOPT_SGA) {
                    telnetRespond(bbsFd, TEL_DO, b);
                } else {
                    telnetRespond(bbsFd, TEL_DONT, b);
                }
                sTelState = TS_DATA;
                break;

            case TS_WONT:
                printf("  TEL RX: WONT %d\n", b);
                telnetRespond(bbsFd, TEL_DONT, b);
                sTelState = TS_DATA;
                break;

            case TS_DO:
                // Server asks us to do something  -- accept TTYPE and NAWS, refuse others
                printf("  TEL RX: DO %d\n", b);
                if (b == TELOPT_TTYPE || b == TELOPT_NAWS) {
                    telnetRespond(bbsFd, TEL_WILL, b);
                } else {
                    telnetRespond(bbsFd, TEL_WONT, b);
                }
                sTelState = TS_DATA;
                break;

            case TS_DONT:
                printf("  TEL RX: DONT %d\n", b);
                telnetRespond(bbsFd, TEL_WONT, b);
                sTelState = TS_DATA;
                break;

            case TS_SB:
                // Inside subnegotiation  -- skip until IAC SE
                if (b == TEL_IAC) {
                    sTelState = TS_SB_IAC;
                }
                break;

            case TS_SB_IAC:
                if (b == TEL_SE) {
                    sTelState = TS_DATA;
                } else {
                    sTelState = TS_SB;
                }
                break;
        }
    }

    return outLen;
}


// Send a telnet negotiation response
static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) {
    uint8_t resp[3] = {TEL_IAC, cmd, opt};
    ssize_t n = write(bbsFd, resp, 3);
    (void)n;
    printf("  TEL TX: %s %d\n",
           cmd == TEL_WILL ? "WILL" : cmd == TEL_WONT ? "WONT" :
           cmd == TEL_DO ? "DO" : "DONT", opt);
}


// ========================================================================
// Main
// ========================================================================

int main(int argc, char *argv[]) {
    int              listenPort = DEFAULT_LISTEN_PORT;
    const char      *bbsHost   = DEFAULT_BBS_HOST;
    int              bbsPort   = DEFAULT_BBS_PORT;
    int              listenFd;
    int              clientFd;
    int              bbsFd = -1;
    SecLinkT        *link;
    struct sockaddr_in clientAddr;
    socklen_t        clientLen;
    struct pollfd    fds[2];
    int              rc;

    if (argc > 1 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
        printf("Usage: %s [listen_port] [bbs_host] [bbs_port]\n", argv[0]);
        printf("Defaults: %d %s %d\n", DEFAULT_LISTEN_PORT, DEFAULT_BBS_HOST, DEFAULT_BBS_PORT);
        return 0;
    }

    if (argc > 1) {
        listenPort = atoi(argv[1]);
    }
    if (argc > 2) {
        bbsHost = argv[2];
    }
    if (argc > 3) {
        bbsPort = atoi(argv[3]);
    }

    // Ignore SIGPIPE so write() to a closed socket returns EPIPE instead of
    // killing the process. Essential for any TCP server/proxy.
    signal(SIGPIPE, SIG_IGN);
    struct sigaction sa;
    sa.sa_handler = sigHandler;
    sa.sa_flags   = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    // Listen for incoming DOS-side connection
    listenFd = createListenSocket(listenPort);
    if (listenFd < 0) {
        fprintf(stderr, "Failed to listen on port %d: %s\n", listenPort, strerror(errno));
        return 1;
    }
    printf("Listening on port %d...\n", listenPort);

    // Accept connection from the DOS side (poll so Ctrl+C works)
    struct pollfd listenPoll = {listenFd, POLLIN, 0};
    while (sRunning) {
        int pr = poll(&listenPoll, 1, 500);
        if (pr > 0) {
            break;
        }
    }

    // A Ctrl+C during the wait clears sRunning; don't fall into a blocking
    // accept() that would then require a second signal to break out of.
    if (!sRunning) {
        close(listenFd);
        return 0;
    }

    clientLen = sizeof(clientAddr);
    clientFd  = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen);
    close(listenFd);
    if (!sRunning || clientFd < 0) {
        if (sRunning) {
            fprintf(stderr, "Accept failed: %s\n", strerror(errno));
        }
        return sRunning ? 1 : 0;
    }
    printf("DOS client connected.\n");
    sClientFd = clientFd;

    // Map the TCP socket to "COM0" so the secLink stack (which calls rs232Read/
    // rs232Write) transparently operates over the TCP connection instead of
    // a real UART. The sockShim intercepts rs232 calls and redirects to socket I/O.
    sockShimSetFd(0, clientFd);

    // Seed RNG from /dev/urandom and open secLink
    seedRng();
    link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecvFromDos, &bbsFd);
    if (!link) {
        fprintf(stderr, "Failed to open secLink.\n");
        close(clientFd);
        return 1;
    }

    // DH key exchange (blocks until both sides complete)
    printf("Waiting for secLink handshake...\n");
    rc = secLinkHandshake(link);
    if (rc != SECLINK_SUCCESS) {
        fprintf(stderr, "Handshake failed: %d\n", rc);
        secLinkClose(link);
        return 1;
    }
    printf("Handshake complete.\n");

    // Wait for ENTER from terminal before connecting to BBS
    printf("Waiting for terminal to send ENTER...\n");
    while (sRunning) {
        // Bail if the DOS client dropped, otherwise this loop polls a dead
        // socket forever waiting for an ENTER that can never arrive.
        if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) {
            printf("DOS client disconnected.\n");
            sRunning = false;
            break;
        }
        if (sGotEnter) {
            break;
        }
        usleep(10000);
    }
    if (!sRunning) {
        secLinkClose(link);
        return 0;
    }

    // Send test string to verify data path
    const char *testMsg = "SecLink proxy ready.\r\n";
    secLinkSend(link, (const uint8_t *)testMsg, (int)strlen(testMsg), CHANNEL_TERMINAL, true, false);
    printf("Sent test message to terminal.\n");

    // Now connect to BBS
    printf("Connecting to %s:%d...\n", bbsHost, bbsPort);
    bbsFd = connectToBbs(bbsHost, bbsPort);
    if (bbsFd < 0) {
        fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno));
        secLinkClose(link);
        return 1;
    }
    printf("BBS connected. Proxying traffic.\n");

    // Keep the BBS socket blocking. Reads are gated by poll(POLLIN), so a
    // blocking fd never stalls the loop, and blocking writes guarantee the
    // accumulating write loop in onRecvFromDos delivers the full payload
    // under send-buffer back-pressure instead of silently truncating it.

    // Main proxy loop: poll both sockets with a short timeout so secLinkPoll
    // runs frequently enough for ACK processing and retransmits. Data flows:
    //   DOS side -> secLink (decrypt) -> callback -> write to BBS
    //   BBS -> read -> telnet filter -> secLink (encrypt) -> DOS side
    fds[0].fd     = clientFd;
    fds[0].events = POLLIN;
    fds[1].fd     = bbsFd;
    fds[1].events = POLLIN;

    while (sRunning) {
        poll(fds, 2, POLL_TIMEOUT_MS);

        // Process incoming secLink packets from the DOS side (callback
        // forwards decrypted data to BBS). A client FIN leaves the fd
        // level-triggered readable without POLLERR/POLLHUP, so the revents
        // check below never fires -- the disconnect must be caught here or
        // the loop spins at full CPU forever.
        if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) {
            printf("DOS client disconnected.\n");
            break;
        }

        // Read from BBS, filter telnet, send clean data to the DOS side
        if (fds[1].revents & POLLIN) {
            uint8_t raw[SECLINK_MAX_PAYLOAD];
            uint8_t clean[SECLINK_MAX_PAYLOAD];
            ssize_t n = read(bbsFd, raw, sizeof(raw));
            if (n <= 0) {
                printf("BBS disconnected.\n");
                break;
            }

            int cleanLen = telnetFilter(bbsFd, raw, (int)n, clean);
            if (cleanLen > 0) {
                // Retry with ACK processing if the send window is full
                int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false);
                while (rc != SECLINK_SUCCESS && sRunning) {
                    // Bail if the DOS client dropped, otherwise this spins
                    // forever waiting for a window that never opens.
                    if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) {
                        printf("DOS client disconnected.\n");
                        sRunning = false;
                        break;
                    }

                    usleep(10000);
                    rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false);
                }
            }
        }

        // Check for disconnects
        if (fds[0].revents & (POLLERR | POLLHUP)) {
            printf("DOS client disconnected.\n");
            break;
        }
        if (fds[1].revents & (POLLERR | POLLHUP)) {
            printf("BBS disconnected.\n");
            break;
        }
    }

    printf("Shutting down.\n");
    secLinkClose(link);
    close(bbsFd);
    close(clientFd);

    return 0;
}
