/*
 * "Telnet" proxy negotiation.
 *
 * (This is for ad-hoc proxies where you connect to the proxy's
 * telnet port and send a command such as `connect host port'. The
 * command is configurable, since this proxy type is typically not
 * standardised or at all well-defined.)
 */

#include "putty.h"
#include "network.h"
#include "proxy.h"
#include "sshcr.h"

char *format_telnet_command(SockAddr *addr, int port, Conf *conf,
                            unsigned *flags_out)
{
    char *fmt = conf_get_str(conf, CONF_proxy_telnet_command);
    int so = 0, eo = 0;
    strbuf *buf = strbuf_new();
    unsigned flags = 0;

    /* we need to escape \\, \%, \r, \n, \t, \x??, \0???,
     * %%, %host, %port, %user, and %pass
     */

    while (fmt[eo] != 0) {

        /* scan forward until we hit end-of-line,
         * or an escape character (\ or %) */
        while (fmt[eo] != 0 && fmt[eo] != '%' && fmt[eo] != '\\')
            eo++;

        /* if we hit eol, break out of our escaping loop */
        if (fmt[eo] == 0) break;

        /* if there was any unescaped text before the escape
         * character, send that now */
        if (eo != so)
            put_data(buf, fmt + so, eo - so);

        so = eo++;

        /* if the escape character was the last character of
         * the line, we'll just stop and send it. */
        if (fmt[eo] == 0) break;

        if (fmt[so] == '\\') {

            /* we recognize \\, \%, \r, \n, \t, \x??.
             * anything else, we just send unescaped (including the \).
             */

            switch (fmt[eo]) {

              case '\\':
                put_byte(buf, '\\');
                eo++;
                break;

              case '%':
                put_byte(buf, '%');
                eo++;
                break;

              case 'r':
                put_byte(buf, '\r');
                eo++;
                break;

              case 'n':
                put_byte(buf, '\n');
                eo++;
                break;

              case 't':
                put_byte(buf, '\t');
                eo++;
                break;

              case 'x':
              case 'X': {
                /* escaped hexadecimal value (ie. \xff) */
                unsigned char v = 0;
                int i = 0;

                for (;;) {
                    eo++;
                    if (fmt[eo] >= '0' && fmt[eo] <= '9')
                        v += fmt[eo] - '0';
                    else if (fmt[eo] >= 'a' && fmt[eo] <= 'f')
                        v += fmt[eo] - 'a' + 10;
                    else if (fmt[eo] >= 'A' && fmt[eo] <= 'F')
                        v += fmt[eo] - 'A' + 10;
                    else {
                        /* non hex character, so we abort and just
                         * send the whole thing unescaped (including \x)
                         */
                        put_byte(buf, '\\');
                        eo = so + 1;
                        break;
                    }

                    /* we only extract two hex characters */
                    if (i == 1) {
                        put_byte(buf, v);
                        eo++;
                        break;
                    }

                    i++;
                    v <<= 4;
                }
                break;
              }

              default:
                put_data(buf, fmt + so, 2);
                eo++;
                break;
            }
        } else {

            /* % escape. we recognize %%, %host, %port, %user, %pass.
             * %proxyhost, %proxyport. Anything else we just send
             * unescaped (including the %).
             */

            if (fmt[eo] == '%') {
                put_byte(buf, '%');
                eo++;
            }
            else if (strnicmp(fmt + eo, "host", 4) == 0) {
                char dest[512];
                sk_getaddr(addr, dest, lenof(dest));
                put_data(buf, dest, strlen(dest));
                eo += 4;
            }
            else if (strnicmp(fmt + eo, "port", 4) == 0) {
                put_fmt(buf, "%d", port);
                eo += 4;
            }
            else if (strnicmp(fmt + eo, "user", 4) == 0) {
                const char *username = conf_get_str(conf, CONF_proxy_username);
                put_data(buf, username, strlen(username));
                eo += 4;
                if (!*username)
                    flags |= TELNET_CMD_MISSING_USERNAME;
            }
            else if (strnicmp(fmt + eo, "pass", 4) == 0) {
                const char *password = conf_get_str(conf, CONF_proxy_password);
                put_data(buf, password, strlen(password));
                eo += 4;
                if (!*password)
                    flags |= TELNET_CMD_MISSING_PASSWORD;
            }
            else if (strnicmp(fmt + eo, "proxyhost", 9) == 0) {
                const char *host = conf_get_str(conf, CONF_proxy_host);
                put_data(buf, host, strlen(host));
                eo += 9;
            }
            else if (strnicmp(fmt + eo, "proxyport", 9) == 0) {
                int port = conf_get_int(conf, CONF_proxy_port);
                put_fmt(buf, "%d", port);
                eo += 9;
            }
            else {
                /* we don't escape this, so send the % now, and
                 * don't advance eo, so that we'll consider the
                 * text immediately following the % as unescaped.
                 */
                put_byte(buf, '%');
            }
        }

        /* resume scanning for additional escapes after this one. */
        so = eo;
    }

    /* if there is any unescaped text at the end of the line, send it */
    if (eo != so) {
        put_data(buf, fmt + so, eo - so);
    }

    if (flags_out)
        *flags_out = flags;
    return strbuf_to_str(buf);
}

typedef struct TelnetProxyNegotiator {
    int crLine;
    Conf *conf;
    char *formatted_cmd;
    prompts_t *prompts;
    int username_prompt_index, password_prompt_index;
    ProxyNegotiator pn;
} TelnetProxyNegotiator;

static ProxyNegotiator *proxy_telnet_new(const ProxyNegotiatorVT *vt)
{
    TelnetProxyNegotiator *s = snew(TelnetProxyNegotiator);
    memset(s, 0, sizeof(*s));
    s->pn.vt = vt;
    return &s->pn;
}

static void proxy_telnet_free(ProxyNegotiator *pn)
{
    TelnetProxyNegotiator *s = container_of(pn, TelnetProxyNegotiator, pn);
    if (s->conf)
        conf_free(s->conf);
    if (s->prompts)
        free_prompts(s->prompts);
    burnstr(s->formatted_cmd);
    delete_callbacks_for_context(s);
    sfree(s);
}

static void proxy_telnet_process_queue_callback(void *vctx)
{
    TelnetProxyNegotiator *s = (TelnetProxyNegotiator *)vctx;
    proxy_negotiator_process_queue(&s->pn);
}

static void proxy_telnet_process_queue(ProxyNegotiator *pn)
{
    TelnetProxyNegotiator *s = container_of(pn, TelnetProxyNegotiator, pn);

    crBegin(s->crLine);

    s->conf = conf_copy(pn->ps->conf);

    /*
     * Make an initial attempt to figure out the command we want, and
     * see if it tried to include a username or password that we don't
     * have.
     */
    {
        unsigned flags;
        s->formatted_cmd = format_telnet_command(
            pn->ps->remote_addr, pn->ps->remote_port, s->conf, &flags);

        if (pn->itr && (flags & (TELNET_CMD_MISSING_USERNAME |
                                 TELNET_CMD_MISSING_PASSWORD))) {
            burnstr(s->formatted_cmd);
            s->formatted_cmd = NULL;

            /*
             * We're missing at least one of the two parts, and we
             * have an Interactor we can use to prompt for them, so
             * try it.
             */
            s->prompts = proxy_new_prompts(pn->ps);
            s->prompts->to_server = true;
            s->prompts->from_server = false;
            s->prompts->name = dupstr("Telnet proxy authentication");
            if (flags & TELNET_CMD_MISSING_USERNAME) {
                s->username_prompt_index = s->prompts->n_prompts;
                add_prompt(s->prompts, dupstr("Proxy username: "), true);
            } else {
                s->username_prompt_index = -1;
            }
            if (flags & TELNET_CMD_MISSING_PASSWORD) {
                s->password_prompt_index = s->prompts->n_prompts;
                add_prompt(s->prompts, dupstr("Proxy password: "), false);
            } else {
                s->password_prompt_index = -1;
            }

            /*
             * This prompt is presented extremely early in PuTTY's
             * setup. (Very promptly, you might say.)
             *
             * In particular, we can get here through a chain of
             * synchronous calls from backend_init, which means (in
             * GUI PuTTY) that the terminal we'll be sending this
             * prompt to may not have its Ldisc set up yet (due to
             * cyclic dependencies among all the things that have to
             * be initialised).
             *
             * So we'll start by having ourself called back via a
             * toplevel callback, to make sure we don't call
             * seat_get_userpass_input until we've returned from
             * backend_init and the frontend has finished getting
             * everything ready.
             */
            queue_toplevel_callback(proxy_telnet_process_queue_callback, s);
            crReturnV;

            while (true) {
                SeatPromptResult spr = seat_get_userpass_input(
                    interactor_announce(pn->itr), s->prompts);
                if (spr.kind == SPRK_OK) {
                    break;
                } else if (spr_is_abort(spr)) {
                    proxy_spr_abort(pn, spr);
                    crStopV;
                }
                crReturnV;
            }

            if (s->username_prompt_index != -1) {
                conf_set_str(
                    s->conf, CONF_proxy_username,
                    prompt_get_result_ref(
                        s->prompts->prompts[s->username_prompt_index]));
            }

            if (s->password_prompt_index != -1) {
                conf_set_str(
                    s->conf, CONF_proxy_password,
                    prompt_get_result_ref(
                        s->prompts->prompts[s->password_prompt_index]));
            }

            free_prompts(s->prompts);
            s->prompts = NULL;
        }

        /*
         * Now format the command a second time, with the results of
         * those prompts written into s->conf.
         */
        s->formatted_cmd = format_telnet_command(
            pn->ps->remote_addr, pn->ps->remote_port, s->conf, NULL);
    }

    /*
     * Log the command, with some changes. Firstly, we regenerate it
     * with the password masked; secondly, we escape control
     * characters so that the log message is printable.
     */
    conf_set_str(s->conf, CONF_proxy_password, "*password*");
    {
        char *censored_cmd = format_telnet_command(
            pn->ps->remote_addr, pn->ps->remote_port, s->conf, NULL);

        strbuf *logmsg = strbuf_new();
        put_datapl(logmsg, PTRLEN_LITERAL("Sending Telnet proxy command: "));
        put_c_string_literal(logmsg, ptrlen_from_asciz(censored_cmd));

        plug_log(pn->ps->plug, &pn->ps->sock, PLUGLOG_PROXY_MSG, NULL, 0,
                 logmsg->s, 0);
        strbuf_free(logmsg);
        sfree(censored_cmd);
    }

    /*
     * Actually send the command.
     */
    put_dataz(pn->output, s->formatted_cmd);

    /*
     * Unconditionally report success. We don't hang around waiting
     * for error messages from the proxy, because this proxy type is
     * so ad-hoc that we wouldn't know how to even recognise an error
     * message if we saw one, let alone what to do about it.
     */
    pn->done = true;

    crFinishV;
}

const struct ProxyNegotiatorVT telnet_proxy_negotiator_vt = {
    .new = proxy_telnet_new,
    .free = proxy_telnet_free,
    .process_queue = proxy_telnet_process_queue,
    .type = "Telnet",
};
