/*
 * Client-specific parts of the SSH-1 connection layer.
 */

#include <assert.h>

#include "putty.h"
#include "ssh.h"
#include "sshbpp.h"
#include "sshppl.h"
#include "sshchan.h"
#include "sshcr.h"
#include "ssh1connection.h"

void ssh1_connection_direction_specific_setup(
    struct ssh1_connection_state *s)
{
    if (!s->mainchan) {
        /*
         * Start up the main session, by telling mainchan.c to do it
         * all just as it would in SSH-2, and translating those
         * concepts to SSH-1's non-channel-shaped idea of the main
         * session.
         */
        s->mainchan = mainchan_new(
            &s->ppl, &s->cl, s->conf, s->term_width, s->term_height,
            false /* is_simple */, NULL);
    }
}

typedef void (*sf_handler_fn_t)(struct ssh1_connection_state *s,
                                bool success, void *ctx);

struct outstanding_succfail {
    sf_handler_fn_t handler;
    void *ctx;
    struct outstanding_succfail *next;

    /*
     * The 'trivial' flag is set if this handler is in response to a
     * request for which the SSH-1 protocol doesn't actually specify a
     * response packet. The client of this system (mainchan.c) will
     * expect to get an acknowledgment regardless, so we arrange to
     * send that ack immediately after the rest of the queue empties.
     */
    bool trivial;
};

static void ssh1_connection_process_trivial_succfails(void *vs);

static void ssh1_queue_succfail_handler(
    struct ssh1_connection_state *s, sf_handler_fn_t handler, void *ctx,
    bool trivial)
{
    struct outstanding_succfail *osf = snew(struct outstanding_succfail);
    osf->handler = handler;
    osf->ctx = ctx;
    osf->trivial = trivial;
    osf->next = NULL;
    if (s->succfail_tail)
        s->succfail_tail->next = osf;
    else
        s->succfail_head = osf;
    s->succfail_tail = osf;

    /* In case this one was trivial and the queue was already empty,
     * we should make sure we run the handler promptly, and the
     * easiest way is to queue it anyway and then run a trivials pass
     * by callback. */
    queue_toplevel_callback(ssh1_connection_process_trivial_succfails, s);
}

static void ssh1_connection_process_succfail(
    struct ssh1_connection_state *s, bool success)
{
    struct outstanding_succfail *prevhead = s->succfail_head;
    s->succfail_head = s->succfail_head->next;
    if (!s->succfail_head)
        s->succfail_tail = NULL;
    prevhead->handler(s, success, prevhead->ctx);
    sfree(prevhead);
}

static void ssh1_connection_process_trivial_succfails(void *vs)
{
    struct ssh1_connection_state *s = (struct ssh1_connection_state *)vs;
    while (s->succfail_head && s->succfail_head->trivial)
        ssh1_connection_process_succfail(s, true);
}

bool ssh1_handle_direction_specific_packet(
    struct ssh1_connection_state *s, PktIn *pktin)
{
    PacketProtocolLayer *ppl = &s->ppl; /* for ppl_logevent */

    PktOut *pktout;
    struct ssh1_channel *c;
    unsigned remid;
    struct ssh_rportfwd pf, *pfp;
    ptrlen host, data;
    int port;

    switch (pktin->type) {
      case SSH1_SMSG_SUCCESS:
      case SSH1_SMSG_FAILURE:
        if (!s->succfail_head) {
            ssh_remote_error(s->ppl.ssh,
                             "Received %s with no outstanding request",
                             ssh1_pkt_type(pktin->type));
            return true;
        }

        ssh1_connection_process_succfail(
            s, pktin->type == SSH1_SMSG_SUCCESS);
        queue_toplevel_callback(
            ssh1_connection_process_trivial_succfails, s);

        return true;

      case SSH1_SMSG_X11_OPEN:
        remid = get_uint32(pktin);

        /* Refuse if X11 forwarding is disabled. */
        if (!s->X11_fwd_enabled) {
            pktout = ssh_bpp_new_pktout(
                s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_FAILURE);
            put_uint32(pktout, remid);
            pq_push(s->ppl.out_pq, pktout);
            ppl_logevent("Rejected X11 connect request");
        } else {
            c = snew(struct ssh1_channel);
            c->connlayer = s;
            ssh1_channel_init(c);
            c->remoteid = remid;
            c->chan = x11_new_channel(s->x11authtree, &c->sc,
                                      NULL, -1, false);
            c->remoteid = remid;
            c->halfopen = false;

            pktout = ssh_bpp_new_pktout(
                s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_CONFIRMATION);
            put_uint32(pktout, c->remoteid);
            put_uint32(pktout, c->localid);
            pq_push(s->ppl.out_pq, pktout);
            ppl_logevent("Opened X11 forward channel");
        }

        return true;

      case SSH1_SMSG_AGENT_OPEN:
        remid = get_uint32(pktin);

        /* Refuse if agent forwarding is disabled. */
        if (!ssh_agent_forwarding_permitted(&s->cl)) {
            pktout = ssh_bpp_new_pktout(
                s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_FAILURE);
            put_uint32(pktout, remid);
            pq_push(s->ppl.out_pq, pktout);
        } else {
            c = snew(struct ssh1_channel);
            c->connlayer = s;
            ssh1_channel_init(c);
            c->remoteid = remid;
            c->halfopen = false;

            /*
             * If possible, make a stream-oriented connection to the
             * agent and set up an ordinary port-forwarding type
             * channel over it.
             */
            Plug *plug;
            Channel *ch = portfwd_raw_new(&s->cl, &plug, true);
            Socket *skt = agent_connect(plug);
            if (!sk_socket_error(skt)) {
                portfwd_raw_setup(ch, skt, &c->sc);
                c->chan = ch;
            } else {
                portfwd_raw_free(ch);

                /*
                 * Otherwise, fall back to the old-fashioned system of
                 * parsing the forwarded data stream ourselves for
                 * message boundaries, and passing each individual
                 * message to the one-off agent_query().
                 */
                c->chan = agentf_new(&c->sc);
            }

            pktout = ssh_bpp_new_pktout(
                s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_CONFIRMATION);
            put_uint32(pktout, c->remoteid);
            put_uint32(pktout, c->localid);
            pq_push(s->ppl.out_pq, pktout);
        }

        return true;

      case SSH1_MSG_PORT_OPEN:
        remid = get_uint32(pktin);
        host = get_string(pktin);
        port = toint(get_uint32(pktin));

        pf.dhost = mkstr(host);
        pf.dport = port;
        pfp = find234(s->rportfwds, &pf, NULL);

        if (!pfp) {
            ppl_logevent("Rejected remote port open request for %s:%d",
                         pf.dhost, port);
            pktout = ssh_bpp_new_pktout(
                s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_FAILURE);
            put_uint32(pktout, remid);
            pq_push(s->ppl.out_pq, pktout);
        } else {
            char *err;

            c = snew(struct ssh1_channel);
            c->connlayer = s;
            ppl_logevent("Received remote port open request for %s:%d",
                         pf.dhost, port);
            err = portfwdmgr_connect(
                s->portfwdmgr, &c->chan, pf.dhost, port,
                &c->sc, pfp->addressfamily);

            if (err) {
                ppl_logevent("Port open failed: %s", err);
                sfree(err);
                ssh1_channel_free(c);
                pktout = ssh_bpp_new_pktout(
                    s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_FAILURE);
                put_uint32(pktout, remid);
                pq_push(s->ppl.out_pq, pktout);
            } else {
                ssh1_channel_init(c);
                c->remoteid = remid;
                c->halfopen = false;
                pktout = ssh_bpp_new_pktout(
                    s->ppl.bpp, SSH1_MSG_CHANNEL_OPEN_CONFIRMATION);
                put_uint32(pktout, c->remoteid);
                put_uint32(pktout, c->localid);
                pq_push(s->ppl.out_pq, pktout);
                ppl_logevent("Forwarded port opened successfully");
            }
        }

        sfree(pf.dhost);

        return true;

      case SSH1_SMSG_STDOUT_DATA:
      case SSH1_SMSG_STDERR_DATA:
        data = get_string(pktin);
        if (!get_err(pktin)) {
            int bufsize = seat_output(
                s->ppl.seat, pktin->type == SSH1_SMSG_STDERR_DATA,
                data.ptr, data.len);
            if (!s->stdout_throttling && bufsize > SSH1_BUFFER_LIMIT) {
                s->stdout_throttling = true;
                ssh_throttle_conn(s->ppl.ssh, +1);
            }
        }

        return true;

      case SSH1_SMSG_EXIT_STATUS: {
        int exitcode = get_uint32(pktin);
        ppl_logevent("Server sent command exit status %d", exitcode);
        ssh_got_exitcode(s->ppl.ssh, exitcode);

        s->session_terminated = true;
        return true;
      }

      default:
        return false;
    }
}

static void ssh1mainchan_succfail_wantreply(struct ssh1_connection_state *s,
                                            bool success, void *ctx)
{
    chan_request_response(s->mainchan_chan, success);
}

static void ssh1mainchan_succfail_nowantreply(struct ssh1_connection_state *s,
                                              bool success, void *ctx)
{
}

static void ssh1mainchan_queue_response(struct ssh1_connection_state *s,
                                        bool want_reply, bool trivial)
{
    sf_handler_fn_t handler = (want_reply ? ssh1mainchan_succfail_wantreply :
                               ssh1mainchan_succfail_nowantreply);
    ssh1_queue_succfail_handler(s, handler, NULL, trivial);
}

static void ssh1mainchan_request_x11_forwarding(
    SshChannel *sc, bool want_reply, const char *authproto,
    const char *authdata, int screen_number, bool oneshot)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_X11_REQUEST_FORWARDING);
    put_stringz(pktout, authproto);
    put_stringz(pktout, authdata);
    if (s->local_protoflags & SSH1_PROTOFLAG_SCREEN_NUMBER)
        put_uint32(pktout, screen_number);
    pq_push(s->ppl.out_pq, pktout);

    ssh1mainchan_queue_response(s, want_reply, false);
}

static void ssh1mainchan_request_agent_forwarding(
    SshChannel *sc, bool want_reply)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(
        s->ppl.bpp, SSH1_CMSG_AGENT_REQUEST_FORWARDING);
    pq_push(s->ppl.out_pq, pktout);

    ssh1mainchan_queue_response(s, want_reply, false);
}

static void ssh1mainchan_request_pty(
    SshChannel *sc, bool want_reply, Conf *conf, int w, int h)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_REQUEST_PTY);
    put_stringz(pktout, conf_get_str(s->conf, CONF_termtype));
    put_uint32(pktout, h);
    put_uint32(pktout, w);
    put_uint32(pktout, 0); /* width in pixels */
    put_uint32(pktout, 0); /* height in pixels */
    write_ttymodes_to_packet(
        BinarySink_UPCAST(pktout), 1,
        get_ttymodes_from_conf(s->ppl.seat, conf));
    pq_push(s->ppl.out_pq, pktout);

    ssh1mainchan_queue_response(s, want_reply, false);
}

static bool ssh1mainchan_send_env_var(
    SshChannel *sc, bool want_reply, const char *var, const char *value)
{
    return false;              /* SSH-1 doesn't support this at all */
}

static void ssh1mainchan_start_shell(SshChannel *sc, bool want_reply)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_EXEC_SHELL);
    pq_push(s->ppl.out_pq, pktout);

    ssh1mainchan_queue_response(s, want_reply, true);
}

static void ssh1mainchan_start_command(
    SshChannel *sc, bool want_reply, const char *command)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_EXEC_CMD);
    put_stringz(pktout, command);
    pq_push(s->ppl.out_pq, pktout);

    ssh1mainchan_queue_response(s, want_reply, true);
}

static bool ssh1mainchan_start_subsystem(
    SshChannel *sc, bool want_reply, const char *subsystem)
{
    return false;              /* SSH-1 doesn't support this at all */
}

static bool ssh1mainchan_send_serial_break(
    SshChannel *sc, bool want_reply, int length)
{
    return false;              /* SSH-1 doesn't support this at all */
}

static bool ssh1mainchan_send_signal(
    SshChannel *sc, bool want_reply, const char *signame)
{
    return false;              /* SSH-1 doesn't support this at all */
}

static void ssh1mainchan_send_terminal_size_change(
    SshChannel *sc, int w, int h)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_WINDOW_SIZE);
    put_uint32(pktout, h);
    put_uint32(pktout, w);
    put_uint32(pktout, 0); /* width in pixels */
    put_uint32(pktout, 0); /* height in pixels */
    pq_push(s->ppl.out_pq, pktout);
}

static void ssh1mainchan_hint_channel_is_simple(SshChannel *sc)
{
}

static size_t ssh1mainchan_write(
    SshChannel *sc, bool is_stderr, const void *data, size_t len)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_STDIN_DATA);
    put_string(pktout, data, len);
    pq_push(s->ppl.out_pq, pktout);

    return 0;
}

static void ssh1mainchan_write_eof(SshChannel *sc)
{
    struct ssh1_connection_state *s =
        container_of(sc, struct ssh1_connection_state, mainchan_sc);
    PktOut *pktout;

    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH1_CMSG_EOF);
    pq_push(s->ppl.out_pq, pktout);
}

static const SshChannelVtable ssh1mainchan_vtable = {
    .write = ssh1mainchan_write,
    .write_eof = ssh1mainchan_write_eof,
    .request_x11_forwarding = ssh1mainchan_request_x11_forwarding,
    .request_agent_forwarding = ssh1mainchan_request_agent_forwarding,
    .request_pty = ssh1mainchan_request_pty,
    .send_env_var = ssh1mainchan_send_env_var,
    .start_shell = ssh1mainchan_start_shell,
    .start_command = ssh1mainchan_start_command,
    .start_subsystem = ssh1mainchan_start_subsystem,
    .send_serial_break = ssh1mainchan_send_serial_break,
    .send_signal = ssh1mainchan_send_signal,
    .send_terminal_size_change = ssh1mainchan_send_terminal_size_change,
    .hint_channel_is_simple = ssh1mainchan_hint_channel_is_simple,
    /* other methods are NULL */
};

static void ssh1_session_confirm_callback(void *vctx)
{
    struct ssh1_connection_state *s = (struct ssh1_connection_state *)vctx;
    chan_open_confirmation(s->mainchan_chan);
}

SshChannel *ssh1_session_open(ConnectionLayer *cl, Channel *chan)
{
    struct ssh1_connection_state *s =
        container_of(cl, struct ssh1_connection_state, cl);
    s->mainchan_sc.vt = &ssh1mainchan_vtable;
    s->mainchan_sc.cl = &s->cl;
    s->mainchan_chan = chan;
    queue_toplevel_callback(ssh1_session_confirm_callback, s);
    return &s->mainchan_sc;
}

static void ssh1_rportfwd_response(struct ssh1_connection_state *s,
                                   bool success, void *ctx)
{
    PacketProtocolLayer *ppl = &s->ppl; /* for ppl_logevent */
    struct ssh_rportfwd *rpf = (struct ssh_rportfwd *)ctx;

    if (success) {
        ppl_logevent("Remote port forwarding from %s enabled",
                     rpf->log_description);
    } else {
        ppl_logevent("Remote port forwarding from %s refused",
                     rpf->log_description);

        struct ssh_rportfwd *realpf = del234(s->rportfwds, rpf);
        assert(realpf == rpf);
        portfwdmgr_close(s->portfwdmgr, rpf->pfr);
        free_rportfwd(rpf);
    }
}

struct ssh_rportfwd *ssh1_rportfwd_alloc(
    ConnectionLayer *cl,
    const char *shost, int sport, const char *dhost, int dport,
    int addressfamily, const char *log_description, PortFwdRecord *pfr,
    ssh_sharing_connstate *share_ctx)
{
    struct ssh1_connection_state *s =
        container_of(cl, struct ssh1_connection_state, cl);
    struct ssh_rportfwd *rpf = snew(struct ssh_rportfwd);

    rpf->shost = dupstr(shost);
    rpf->sport = sport;
    rpf->dhost = dupstr(dhost);
    rpf->dport = dport;
    rpf->addressfamily = addressfamily;
    rpf->log_description = dupstr(log_description);
    rpf->pfr = pfr;

    if (add234(s->rportfwds, rpf) != rpf) {
        free_rportfwd(rpf);
        return NULL;
    }

    PktOut *pktout = ssh_bpp_new_pktout(
        s->ppl.bpp, SSH1_CMSG_PORT_FORWARD_REQUEST);
    put_uint32(pktout, rpf->sport);
    put_stringz(pktout, rpf->dhost);
    put_uint32(pktout, rpf->dport);
    pq_push(s->ppl.out_pq, pktout);

    ssh1_queue_succfail_handler(s, ssh1_rportfwd_response, rpf, false);

    return rpf;
}

SshChannel *ssh1_serverside_x11_open(
    ConnectionLayer *cl, Channel *chan, const SocketPeerInfo *pi)
{
    unreachable("Should never be called in the client");
}

SshChannel *ssh1_serverside_agent_open(ConnectionLayer *cl, Channel *chan)
{
    unreachable("Should never be called in the client");
}

bool ssh1_connection_need_antispoof_prompt(struct ssh1_connection_state *s)
{
    return !seat_set_trust_status(s->ppl.seat, false);
}
