/*
 * Platform-independent parts of a standalone SOCKS server program
 * based on the PuTTY SOCKS code.
 */

#include <string.h>
#include <errno.h>

#include "putty.h"
#include "storage.h"
#include "misc.h"
#include "ssh.h"
#include "ssh/channel.h"
#include "psocks.h"

/*
 * Possible later TODOs:
 *
 *  - verbosity setting for log messages
 *
 *  - could import proxy.c and use name_lookup rather than
 *    sk_namelookup, to allow forwarding via some other proxy type
 */

#define BUFLIMIT 16384

#define LOGBITS(X)                              \
    X(CONNSTATUS)                               \
    X(DIALOGUE)                                 \
    /* end of list */

#define BITINDEX_ENUM(x) LOG_##x##_bitindex,
enum { LOGBITS(BITINDEX_ENUM) };
#define BITFLAG_ENUM(x) LOG_##x = 1 << LOG_##x##_bitindex,
enum { LOGBITS(BITFLAG_ENUM) };

typedef struct psocks_connection psocks_connection;

typedef enum RecordDestination {
    REC_NONE, REC_FILE, REC_PIPE
} RecordDestination;

struct psocks_state {
    const PsocksPlatform *platform;
    int listen_port;
    bool acceptall;
    PortFwdManager *portfwdmgr;
    uint64_t next_conn_index;
    FILE *logging_fp;
    unsigned log_flags;
    RecordDestination rec_dest;
    char *rec_cmd;
    bool got_subcmd;

    ConnectionLayer cl;
};

struct psocks_connection {
    psocks_state *ps;
    Channel *chan;
    char *host, *realhost;
    int port;
    SockAddr *addr;
    Socket *socket;
    bool connecting, eof_pfmgr_to_socket, eof_socket_to_pfmgr;
    uint64_t index;
    PsocksDataSink *rec_sink;

    Plug plug;
    SshChannel sc;
};

static SshChannel *psocks_lportfwd_open(
    ConnectionLayer *cl, const char *hostname, int port,
    const char *description, const SocketEndpointInfo *pi, Channel *chan);

static const ConnectionLayerVtable psocks_clvt = {
    .lportfwd_open = psocks_lportfwd_open,
    /* everything else is NULL */
};

static size_t psocks_sc_write(SshChannel *sc, bool is_stderr, const void *,
                              size_t);
static void psocks_sc_write_eof(SshChannel *sc);
static void psocks_sc_initiate_close(SshChannel *sc, const char *err);
static void psocks_sc_unthrottle(SshChannel *sc, size_t bufsize);

static const SshChannelVtable psocks_scvt = {
    .write = psocks_sc_write,
    .write_eof = psocks_sc_write_eof,
    .initiate_close = psocks_sc_initiate_close,
    .unthrottle = psocks_sc_unthrottle,
    /* all the rest are NULL */
};

static void psocks_plug_log(Plug *p, Socket *s, PlugLogType type,
                            SockAddr *addr, int port,
                            const char *error_msg, int error_code);
static void psocks_plug_closing(Plug *p, PlugCloseType, const char *error_msg);
static void psocks_plug_receive(Plug *p, int urgent,
                                const char *data, size_t len);
static void psocks_plug_sent(Plug *p, size_t bufsize);

static const PlugVtable psocks_plugvt = {
    .log = psocks_plug_log,
    .closing = psocks_plug_closing,
    .receive = psocks_plug_receive,
    .sent = psocks_plug_sent,
};

static void psocks_conn_log(psocks_connection *conn, const char *fmt, ...)
{
    if (!conn->ps->logging_fp)
        return;

    va_list ap;
    va_start(ap, fmt);
    char *msg = dupvprintf(fmt, ap);
    va_end(ap);
    fprintf(conn->ps->logging_fp, "c#%"PRIu64": %s\n", conn->index, msg);
    sfree(msg);
    fflush(conn->ps->logging_fp);
}

static void psocks_conn_log_data(psocks_connection *conn, PsocksDirection dir,
                                 const void *vdata, size_t len)
{
    if ((conn->ps->log_flags & LOG_DIALOGUE) && conn->ps->logging_fp) {
        const char *data = vdata;
        while (len > 0) {
            const char *nl = memchr(data, '\n', len);
            size_t thislen = nl ? (nl+1) - data : len;
            const char *thisdata = data;
            data += thislen;
            len -= thislen;

            static const char *const direction_names[2] = {
                [UP] = "send", [DN] = "recv" };

            fprintf(conn->ps->logging_fp, "c#%"PRIu64": %s \"", conn->index,
                    direction_names[dir]);
            write_c_string_literal(conn->ps->logging_fp,
                                   make_ptrlen(thisdata, thislen));
            fprintf(conn->ps->logging_fp, "\"\n");
        }

        fflush(conn->ps->logging_fp);
    }

    if (conn->rec_sink)
        put_data(conn->rec_sink->s[dir], vdata, len);
}

static void psocks_connection_establish(void *vctx);

static SshChannel *psocks_lportfwd_open(
    ConnectionLayer *cl, const char *hostname, int port,
    const char *description, const SocketEndpointInfo *pi, Channel *chan)
{
    psocks_state *ps = container_of(cl, psocks_state, cl);
    psocks_connection *conn = snew(psocks_connection);
    memset(conn, 0, sizeof(*conn));
    conn->ps = ps;
    conn->sc.vt = &psocks_scvt;
    conn->plug.vt = &psocks_plugvt;
    conn->chan = chan;
    conn->host = dupstr(hostname);
    conn->port = port;
    conn->index = ps->next_conn_index++;
    if (conn->ps->log_flags & LOG_CONNSTATUS)
        psocks_conn_log(conn, "request from %s for %s port %d",
                        pi->log_text, hostname, port);
    switch (conn->ps->rec_dest) {
      case REC_FILE:
        {
            char *fnames[2];
            FILE *fp[2];
            bool ok = true;

            static const char *const direction_names[2] = {
                [UP] = "sockout", [DN] = "sockin" };

            for (size_t i = 0; i < 2; i++) {
                fnames[i] = dupprintf("%s.%"PRIu64, direction_names[i],
                                      conn->index);
                fp[i] = fopen(fnames[i], "wb");
                if (!fp[i]) {
                    psocks_conn_log(conn, "cannot log this connection: "
                                    "creating file '%s': %s",
                                    fnames[i], strerror(errno));
                    ok = false;
                }
            }
            if (ok) {
                if (conn->ps->log_flags & LOG_CONNSTATUS)
                    psocks_conn_log(conn, "logging to '%s' / '%s'",
                                    fnames[0], fnames[1]);
                conn->rec_sink = pds_stdio(fp);
            } else {
                for (size_t i = 0; i < 2; i++) {
                    if (fp[i]) {
                        remove(fnames[i]);
                        fclose(fp[i]);
                    }
                }
            }
            for (size_t i = 0; i < 2; i++)
                sfree(fnames[i]);
        }
        break;
      case REC_PIPE:
        {
            static const char *const direction_args[2] = {
                [UP] = "out", [DN] = "in" };
            char *index_arg = dupprintf("%"PRIu64, conn->index);
            char *err;
            conn->rec_sink = conn->ps->platform->open_pipes(
                conn->ps->rec_cmd, direction_args, index_arg, &err);
            if (!conn->rec_sink) {
                psocks_conn_log(conn, "cannot log this connection: "
                                "creating pipes: %s", err);
                sfree(err);
            }
            sfree(index_arg);
        }
        break;
      default:
        break;
    }
    queue_toplevel_callback(psocks_connection_establish, conn);
    return &conn->sc;
}

static void psocks_conn_free(psocks_connection *conn)
{
    if (conn->ps->log_flags & LOG_CONNSTATUS)
        psocks_conn_log(conn, "closed");

    sfree(conn->host);
    sfree(conn->realhost);
    if (conn->socket)
        sk_close(conn->socket);
    if (conn->chan)
        chan_free(conn->chan);
    if (conn->rec_sink)
        pds_free(conn->rec_sink);
    delete_callbacks_for_context(conn);
    sfree(conn);
}

static void psocks_connection_establish(void *vctx)
{
    psocks_connection *conn = (psocks_connection *)vctx;

    /*
     * Look up destination host name.
     */
    conn->addr = sk_namelookup(conn->host, &conn->realhost, ADDRTYPE_UNSPEC);

    const char *err = sk_addr_error(conn->addr);
    if (err) {
        char *msg = dupprintf("name lookup failed: %s", err);
        chan_open_failed(conn->chan, msg);
        sfree(msg);

        psocks_conn_free(conn);
        return;
    }

    /*
     * Make the connection.
     */
    conn->connecting = true;
    conn->socket = sk_new(conn->addr, conn->port, false, false, false, false,
                          &conn->plug);
}

static size_t psocks_sc_write(SshChannel *sc, bool is_stderr,
                              const void *data, size_t len)
{
    psocks_connection *conn = container_of(sc, psocks_connection, sc);
    if (!conn->socket) return 0;

    psocks_conn_log_data(conn, UP, data, len);

    return sk_write(conn->socket, data, len);
}

static void psocks_check_close(void *vctx)
{
    psocks_connection *conn = (psocks_connection *)vctx;
    if (chan_want_close(conn->chan, conn->eof_pfmgr_to_socket,
                        conn->eof_socket_to_pfmgr))
        psocks_conn_free(conn);
}

static void psocks_sc_write_eof(SshChannel *sc)
{
    psocks_connection *conn = container_of(sc, psocks_connection, sc);
    if (!conn->socket) return;
    sk_write_eof(conn->socket);
    conn->eof_pfmgr_to_socket = true;

    if (conn->ps->log_flags & LOG_DIALOGUE)
        psocks_conn_log(conn, "send eof");

    queue_toplevel_callback(psocks_check_close, conn);
}

static void psocks_sc_initiate_close(SshChannel *sc, const char *err)
{
    psocks_connection *conn = container_of(sc, psocks_connection, sc);
    sk_close(conn->socket);
    conn->socket = NULL;
}

static void psocks_sc_unthrottle(SshChannel *sc, size_t bufsize)
{
    psocks_connection *conn = container_of(sc, psocks_connection, sc);
    if (bufsize < BUFLIMIT)
	sk_set_frozen(conn->socket, false);
}

static void psocks_plug_log(Plug *plug, Socket *s, PlugLogType type,
                            SockAddr *addr, int port,
                            const char *error_msg, int error_code)
{
    psocks_connection *conn = container_of(plug, psocks_connection, plug);
    char addrbuf[256];

    if (!(conn->ps->log_flags & LOG_CONNSTATUS))
        return;

    switch (type) {
      case PLUGLOG_CONNECT_TRYING:
        sk_getaddr(addr, addrbuf, sizeof(addrbuf));
        if (sk_addr_needs_port(addr))
            psocks_conn_log(conn, "trying to connect to %s port %d",
                            addrbuf, port);
        else
            psocks_conn_log(conn, "trying to connect to %s", addrbuf);
        break;
      case PLUGLOG_CONNECT_FAILED:
        psocks_conn_log(conn, "connection attempt failed: %s", error_msg);
        break;
      case PLUGLOG_CONNECT_SUCCESS:
        psocks_conn_log(conn, "connection established", error_msg);
        if (conn->connecting) {
            chan_open_confirmation(conn->chan);
            conn->connecting = false;
        }
        break;
      case PLUGLOG_PROXY_MSG:
        psocks_conn_log(conn, "connection setup: %s", error_msg);
        break;
    };
}

static void psocks_plug_closing(Plug *plug, PlugCloseType type,
                                const char *error_msg)
{
    psocks_connection *conn = container_of(plug, psocks_connection, plug);
    if (conn->connecting) {
        if (conn->ps->log_flags & LOG_CONNSTATUS)
            psocks_conn_log(conn, "unable to connect: %s", error_msg);

        chan_open_failed(conn->chan, error_msg);
        conn->eof_socket_to_pfmgr = true;
        conn->eof_pfmgr_to_socket = true;
        conn->connecting = false;
    } else {
        if (conn->ps->log_flags & LOG_DIALOGUE)
            psocks_conn_log(conn, "recv eof");

        chan_send_eof(conn->chan);
        conn->eof_socket_to_pfmgr = true;
    }
    queue_toplevel_callback(psocks_check_close, conn);
}

static void psocks_plug_receive(Plug *plug, int urgent,
                                const char *data, size_t len)
{
    psocks_connection *conn = container_of(plug, psocks_connection, plug);
    size_t bufsize = chan_send(conn->chan, false, data, len);
    sk_set_frozen(conn->socket, bufsize > BUFLIMIT);

    psocks_conn_log_data(conn, DN, data, len);
}

static void psocks_plug_sent(Plug *plug, size_t bufsize)
{
    psocks_connection *conn = container_of(plug, psocks_connection, plug);
    sk_set_frozen(conn->socket, bufsize > BUFLIMIT);
}

psocks_state *psocks_new(const PsocksPlatform *platform)
{
    psocks_state *ps = snew(psocks_state);
    memset(ps, 0, sizeof(*ps));

    ps->listen_port = 1080;
    ps->acceptall = false;

    ps->cl.vt = &psocks_clvt;
    ps->portfwdmgr = portfwdmgr_new(&ps->cl);

    ps->logging_fp = stderr; /* could make this configurable later */
    ps->log_flags = LOG_CONNSTATUS;
    ps->rec_dest = REC_NONE;
    ps->platform = platform;

    return ps;
}

void psocks_free(psocks_state *ps)
{
    portfwdmgr_free(ps->portfwdmgr);
    sfree(ps->rec_cmd);
    sfree(ps);
}

void psocks_cmdline(psocks_state *ps, CmdlineArgList *arglist)
{
    bool doing_opts = true;
    size_t arglistpos = 0;
    size_t args_seen = 0;

    while (arglist->args[arglistpos]) {
	CmdlineArg *arg = arglist->args[arglistpos++];
        const char *p = cmdline_arg_to_str(arg);

	if (doing_opts && p[0] == '-' && p[1]) {
            if (!strcmp(p, "--")) {
                doing_opts = false;
            } else if (!strcmp(p, "-g")) {
		ps->acceptall = true;
            } else if (!strcmp(p, "-d")) {
		ps->log_flags |= LOG_DIALOGUE;
            } else if (!strcmp(p, "-f")) {
		ps->rec_dest = REC_FILE;
            } else if (!strcmp(p, "-p")) {
                if (!ps->platform->open_pipes) {
		    fprintf(stderr, "psocks: '-p' is not supported on this "
                            "platform\n");
		    exit(1);
                }
		if (arglist->args[arglistpos] > 0) {
		    ps->rec_cmd = dupstr(
                        cmdline_arg_to_str(arglist->args[arglistpos++]));
		} else {
		    fprintf(stderr, "psocks: expected an argument to '-p'\n");
		    exit(1);
		}
		ps->rec_dest = REC_PIPE;
	    } else if (!strcmp(p, "--exec")) {
                if (!ps->platform->start_subcommand) {
		    fprintf(stderr, "psocks: running a subcommand is not "
                            "supported on this platform\n");
		    exit(1);
                }
                if (!arglist->args[arglistpos]) {
		    fprintf(stderr, "psocks: --exec requires a command\n");
		    exit(1);
                }
                /* Now consume all further argv words for the
                 * subcommand, even if they look like options */
                ps->platform->found_subcommand(arglist->args[arglistpos]);
                ps->got_subcmd = true;
                break;
	    } else if (!strcmp(p, "--help")) {
                printf("usage: psocks [ -d ] [ -f");
                if (ps->platform->open_pipes)
                    printf(" | -p pipe-cmd");
                printf(" ] [ -g ] port-number");
                printf("\n");
                printf("where: -d           log all connection contents to"
                       " standard output\n");
                printf("       -f           record each half-connection to "
                       "a file sockin.N/sockout.N\n");
                if (ps->platform->open_pipes)
                    printf("       -p pipe-cmd  pipe each half-connection"
                           " to 'pipe-cmd [in|out] N'\n");
                printf("       -g           accept connections from anywhere,"
                       " not just localhost\n");
                if (ps->platform->start_subcommand)
                    printf("       --exec subcmd [args...]   run command, and "
                           "terminate when it exits\n");
                printf("       port-number  listen on this port"
                       " (default 1080)\n");
                printf("also: psocks --help      display this help text\n");
                exit(0);
            } else {
                fprintf(stderr, "psocks: unrecognised option '%s'\n", p);
                exit(1);
            }
	} else {
            switch (args_seen++) {
              case 0:
                ps->listen_port = atoi(p);
                break;
              default:
                fprintf(stderr, "psocks: unexpected extra argument '%s'\n", p);
                exit(1);
                break;
            }
	}
    }
}

void psocks_start(psocks_state *ps)
{
    Conf *conf = conf_new();
    conf_set_bool(conf, CONF_lport_acceptall, ps->acceptall);
    char *key = dupprintf("AL%d", ps->listen_port);
    conf_set_str_str(conf, CONF_portfwd, key, "D");
    sfree(key);

    portfwdmgr_config(ps->portfwdmgr, conf);

    if (ps->got_subcmd)
        ps->platform->start_subcommand();

    conf_free(conf);
}

/*
 * Some stubs that are needed to link against PuTTY modules.
 */

int check_stored_host_key(const char *hostname, int port,
                          const char *keytype, const char *key)
{
    unreachable("host keys not handled in this tool");
}

void store_host_key(Seat *seat, const char *hostname, int port,
                    const char *keytype, const char *key)
{
    unreachable("host keys not handled in this tool");
}

/*
 * stdio-targeted PsocksDataSink.
 */
typedef struct PsocksDataSinkStdio {
    stdio_sink sink[2];
    PsocksDataSink pds;
} PsocksDataSinkStdio;

static void stdio_free(PsocksDataSink *pds)
{
    PsocksDataSinkStdio *pdss = container_of(pds, PsocksDataSinkStdio, pds);

    for (size_t i = 0; i < 2; i++)
        fclose(pdss->sink[i].fp);

    sfree(pdss);
}

PsocksDataSink *pds_stdio(FILE *fp[2])
{
    PsocksDataSinkStdio *pdss = snew(PsocksDataSinkStdio);

    for (size_t i = 0; i < 2; i++) {
        setvbuf(fp[i], NULL, _IONBF, 0);
        stdio_sink_init(&pdss->sink[i], fp[i]);
        pdss->pds.s[i] = BinarySink_UPCAST(&pdss->sink[i]);
    }

    pdss->pds.free = stdio_free;

    return &pdss->pds;
}
