/**********
 * Copyright (c) 2003-2004 Greg Parker.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY GREG PARKER ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 **********/

#include "includes.h"
#include "connection.h"

#include "xmalloc.h"
#include "assert.h"
#include "session.h"
#include "transport.h"
#include "packetizer.h"
#include "vt100/vt100.h"
#include "ssh2.h"
#include "data/prefs.h"
#include "data/publickeys.h"
#include "forms/about.h"
#include "openssh/buffer.h"
#include "openssh/bufaux.h"
#include "openssh/key.h"
#include "openssh/match.h"
#include "rsrc/rsrc.h"

#define DATA_MAX 32768
#define RECEIVE_WINDOW_MIN DATA_MAX
#define RECEIVE_WINDOW_MAX DATA_MAX*4


static void connection_send_pty_request(ssh_session_t *ss, uint32_t channel) CONNECTION_SEGMENT;
static void connection_send_shell_request(ssh_session_t *ss, uint32_t channel) CONNECTION_SEGMENT;
static void connection_send_window_change(ssh_session_t *ss, uint32_t channel, int charsWide, int charsHigh, int pixelsWide, int pixelsHigh) CONNECTION_SEGMENT;
static void connection_send_service_request(ssh_session_t *ss, char *name) CONNECTION_SEGMENT;
static void connection_send_userauth(ssh_session_t *ss, char *username, char *authmethod, uint8_t *authdata, uint16_t authdatalen) CONNECTION_SEGMENT;
static void connection_send_userauth_password(ssh_session_t *ss, char *username, char *password) CONNECTION_SEGMENT;
static void connection_send_userauth_publickey(ssh_session_t *ss, char *username, Key *key, Boolean check) CONNECTION_SEGMENT;
static void connection_send_authmethod_request(ssh_session_t *ss, char *username) CONNECTION_SEGMENT;
static void connection_send_channel_open(ssh_session_t *ss, uint32_t channel, uint32_t receiveWindow, uint32_t packetMax) CONNECTION_SEGMENT;
static void connection_send_window_adjust(ssh_session_t *ss, uint32_t channel, uint32_t adjustment) CONNECTION_SEGMENT;
static void connection_send_channel_close(ssh_session_t *ss, uint32_t channel) CONNECTION_SEGMENT;
static void connection_send_global_request_failure(ssh_session_t *ss) CONNECTION_SEGMENT;
static void connection_send_channel_request_failure(ssh_session_t *ss, uint32_t channel) CONNECTION_SEGMENT;
static void connection_send_channel_data(ssh_session_t *ss, uint32_t channel, uint8_t *bytes, uint16_t len) CONNECTION_SEGMENT;

static connection_state_t connection_check_next_publickey(ssh_session_t *ss, queue_t *q) CONNECTION_SEGMENT;
static connection_state_t connection_get_password(ssh_session_t *ss) CONNECTION_SEGMENT;
static connection_state_t connection_get_passphrase(ssh_session_t *ss, queue_t *q) CONNECTION_SEGMENT;
static void connection_increase_receive_window(ssh_session_t *ss) CONNECTION_SEGMENT;
static void connection_handle_typed_data(ssh_session_t *ss, uint32_t type, uint8_t *data, uint32_t datalen, int closing) CONNECTION_SEGMENT;
static connection_state_t connection_handle_unexpected_message(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_userauth_service_accept(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_banner(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_userauth_success(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_try_next_userauth(ssh_session_t *ss) CONNECTION_SEGMENT;
static connection_state_t connection_handle_userauth_failure(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_good_publickey(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_bad_publickey(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_password_change(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_channel_open(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_channel_open_failure(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_pty_success(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_pty_failure(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_shell_success(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_shell_failure(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_window_adjust(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_channel_close(ssh_session_t *ss, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_handle_data(ssh_session_t *ss, Buffer *packet, int closing) CONNECTION_SEGMENT;
static connection_state_t connection_handle_extended_data(ssh_session_t *ss, Buffer *packet, int closing) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_userauth_service(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_password(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_userauth(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_pubkey_check(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_pubkey_passphrase(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_channel_open(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_pty(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_waiting_for_shell(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_running(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_closing(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static connection_state_t connection_state_closed(ssh_session_t *ss, uint8_t msg, Buffer *packet) CONNECTION_SEGMENT;
static void connection_cleanup(ssh_session_t *ss) CONNECTION_SEGMENT;


/***********************************************************************
 * raw packet output
 */

static void connection_send_pty_request(ssh_session_t *ss, uint32_t channel)
{
    Buffer payload;
    int charsWide;
    int charsHigh;
    char *termtype;

    connection_log(__PRETTY_FUNCTION__);

    ssh_size(ss, &charsWide, &charsHigh);
    termtype = PrefsGetString(prefTerminalType, "xterm");

    printf("   Opening pty...\r\n");

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_REQUEST);
    buffer_put_int(&payload, channel);
    buffer_put_cstring(&payload, "pty-req");
    buffer_put_char(&payload, 1); // want-reply
    buffer_put_cstring(&payload, termtype); // TERM
    buffer_put_int(&payload, charsWide); // tty width  (chars)
    buffer_put_int(&payload, charsHigh); // tty height (chars)
    buffer_put_int(&payload, 0);  // tty width  (pixels) fixme
    buffer_put_int(&payload, 0);  // tty height (pixels) fixme
    buffer_put_cstring(&payload, ""); // encoded terminal modes
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent pty request\r\n");

    arena_free(termtype);
}


static void connection_send_shell_request(ssh_session_t *ss, uint32_t channel)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    printf("   Starting shell...\r\n");

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_REQUEST);
    buffer_put_int(&payload, channel);
    buffer_put_cstring(&payload, "shell");
    buffer_put_char(&payload, 1); // want-reply
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent shell request\r\n");
}


static void connection_send_window_change(ssh_session_t *ss, uint32_t channel, 
                                          int charsWide, int charsHigh, 
                                          int pixelsWide, int pixelsHigh)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_REQUEST);
    buffer_put_int(&payload, channel);
    buffer_put_cstring(&payload, "window-change");
    buffer_put_char(&payload, 0); // want-reply
    buffer_put_int(&payload, charsWide);
    buffer_put_int(&payload, charsHigh);
    buffer_put_int(&payload, pixelsWide);
    buffer_put_int(&payload, pixelsHigh);
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent WINCH\r\n");    
}

static void connection_send_service_request(ssh_session_t *ss, char *name)
{
    Buffer packet;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&packet);
    buffer_put_char(&packet, SSH2_MSG_SERVICE_REQUEST);
    buffer_put_cstring(&packet, name);
    transport_send_payload(ss, &packet);
    buffer_free(&packet);
    connection_log("connection: sent service request for '%s'\r\n", name);
}


static void 
connection_send_userauth(ssh_session_t *ss, char *username, 
                         char *authmethod, uint8_t *authdata, 
                         uint16_t authdatalen)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_USERAUTH_REQUEST);
    buffer_put_cstring(&payload, username);
    buffer_put_cstring(&payload, "ssh-connection"); // service
    buffer_put_cstring(&payload, authmethod);
    if (authdata) {
        buffer_append(&payload, authdata, authdatalen);
    }
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent userauth request for username '%s' using authmethod '%s'\r\n", username, authmethod);
}


static void 
connection_send_userauth_password(ssh_session_t *ss, char *username, char *password)
{
    Buffer authdata;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&authdata);
    buffer_put_char(&authdata, 0); // boolean FALSE
    buffer_put_cstring(&authdata, password);
    connection_send_userauth(ss, username, "password", 
                             buffer_ptr(&authdata), buffer_len(&authdata));
    buffer_free(&authdata);
}


static void 
connection_send_userauth_publickey(ssh_session_t *ss, char *username, 
                                   Key *key, Boolean check)
{
    Buffer authdata;
    Buffer keyblob;
    int realAuth = (check ? 0 : 1);

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&authdata);
    buffer_put_char(&authdata, realAuth); // FALSE if this is validity check
    buffer_put_cstring(&authdata, key_ssh_name(key));

    buffer_init(&keyblob);
    key_to_blob_buffer(key, &keyblob);
    buffer_put_string(&authdata, buffer_ptr(&keyblob), buffer_len(&keyblob));
    buffer_free(&keyblob);

    if (realAuth) {
        // yuck - signature covers data that isn't needed until
        // connection_send_userauth()
        Buffer signbuf;
        uint8_t *signature;
        uint16_t signaturelen;
        buffer_init(&signbuf);
        buffer_put_string(&signbuf, ss->t.session_id, ss->t.session_id_len);
        buffer_put_char(&signbuf, SSH2_MSG_USERAUTH_REQUEST);
        buffer_put_cstring(&signbuf, username);
        buffer_put_cstring(&signbuf, "ssh-connection");
        buffer_put_cstring(&signbuf, "publickey");
        buffer_append(&signbuf, buffer_ptr(&authdata), buffer_len(&authdata));
        
        // sign signdata with privkey and append signature to authdata
        key_sign(key, &signature, &signaturelen, 
                 buffer_ptr(&signbuf), buffer_len(&signbuf));
        buffer_put_string(&authdata, signature, signaturelen);
        xfree(signature);
        buffer_free(&signbuf);
    }
    connection_send_userauth(ss, username, "publickey", 
                             buffer_ptr(&authdata), buffer_len(&authdata));
    // wipe keys from buffer
    memset(buffer_ptr(&authdata), 0, buffer_len(&authdata));
    buffer_free(&authdata);
}


static void 
connection_send_authmethod_request(ssh_session_t *ss, char *username)
{
    connection_log(__PRETTY_FUNCTION__);

    // To request the list of authentication methods, 
    // send a USERAUTH_REQUEST with authentication type "none". 
    // This will either succeed with no auth (USERAUTH_SUCCESS), 
    // or be rejected with a list of authentication methods supported.

    connection_send_userauth(ss, username, "none", NULL, 0);
}


static void 
connection_send_channel_open(ssh_session_t *ss, uint32_t channel, 
                             uint32_t receiveWindow, uint32_t packetMax)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    printf("   Opening channel...\r\n");

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_OPEN);
    buffer_put_cstring(&payload, "session");
    buffer_put_int(&payload, channel);
    buffer_put_int(&payload, receiveWindow); // initial window size
    buffer_put_int(&payload, packetMax); // packet size
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent channel open\r\n");
}


static void 
connection_send_window_adjust(ssh_session_t *ss, uint32_t channel, 
                              uint32_t adjustment)
{
    // this gets used a lot, so keep a static Buffer
    static Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    if (!payload.buf) buffer_init(&payload);
    buffer_clear(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_WINDOW_ADJUST);
    buffer_put_int(&payload, channel);
    buffer_put_int(&payload, adjustment);
    transport_send_payload(ss, &payload);
    connection_log("connection: sent window adjust\r\n");
}


static void 
connection_send_channel_close(ssh_session_t *ss, uint32_t channel)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_CLOSE);
    buffer_put_int(&payload, channel);
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent channel close\r\n");
}

static void 
connection_send_global_request_failure(ssh_session_t *ss)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_REQUEST_FAILURE);
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent global request failure\r\n");
}


static void 
connection_send_channel_request_failure(ssh_session_t *ss, uint32_t channel)
{
    Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    buffer_init(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_FAILURE);
    buffer_put_int(&payload, channel);
    transport_send_payload(ss, &payload);
    buffer_free(&payload);
    connection_log("connection: sent channel request failure\r\n");
}


// does NOT enforce send window and max packet size
static void 
connection_send_channel_data(ssh_session_t *ss, uint32_t channel, 
                             uint8_t *bytes, uint16_t len)
{
    static Buffer payload;

    connection_log(__PRETTY_FUNCTION__);

    if (!payload.buf) buffer_init(&payload);
    buffer_clear(&payload);
    buffer_put_char(&payload, SSH2_MSG_CHANNEL_DATA);
    buffer_put_int(&payload, channel);
    buffer_put_string(&payload, bytes, len);
    transport_send_payload(ss, &payload);
}


/***********************************************************************
 * misc
 */


static connection_state_t
connection_check_next_publickey(ssh_session_t *ss, queue_t *q)
{
    Key *k;

    connection_log(__PRETTY_FUNCTION__);

    assert (!queue_empty(q));

    ss->c.current_pubkey = (MemHandle)queue_dequeue(q);
    k = PublicKeyForRecord(MemHandleLock(ss->c.current_pubkey));
    MemHandleUnlock(ss->c.current_pubkey);
    if (!k) return 0;

    connection_send_userauth_publickey(ss, ss->username, k, true);
    return CONNECTION_STATE_WAITING_FOR_PUBKEY_CHECK;
}


static connection_state_t
connection_get_password(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    ss->c.passwordAttempts++;

    if (ss->c.passwordAttempts == 1) {
        // Look for a saved username and password, but only once
        // fixme wait for banner?
        // fixme
    }

    if (ss->c.passwordAttempts > 3) {
        // too many password attempts
        // fixme don't bother enforcing this?
    }

    // get name and password from GUI
    // reuse username if known
    // fixme
    ssh_request_password(ss->c.passwordAttempts);

    return CONNECTION_STATE_WAITING_FOR_PASSWORD;
}


static connection_state_t
connection_get_passphrase(ssh_session_t *ss, queue_t *q)
{
    connection_log(__PRETTY_FUNCTION__);

    // get name and password and public key choice from GUI
    // reuse username if known
    // fixme
    ssh_request_passphrase(q);

    return CONNECTION_STATE_WAITING_FOR_PUBKEY_PASSPHRASE;
}


void connection_use_password(ssh_session_t *ss, char *username, char *password)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state == CONNECTION_STATE_WAITING_FOR_PASSWORD) {
        if (ss->username) arena_free(ss->username);
        ss->username = arena_strdup(username);
        connection_send_userauth_password(ss, username, password);
        ss->c.state = CONNECTION_STATE_WAITING_FOR_USERAUTH;
    } else {
        printf("connection: got password outside waiting-for-password\r\n");
    }
}


Boolean connection_use_passphrase(ssh_session_t *ss, char *username, char *passphrase, MemHandle pubkey)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state == CONNECTION_STATE_WAITING_FOR_PUBKEY_PASSPHRASE) {
        Key *priv;

        if (!pubkey) {
            // user cancel - try something else
            // clear list of good pubkeys so we finish pubkey attempts
            while (!queue_empty(ss->c.good_pubkeys)) {
                queue_dequeue(ss->c.good_pubkeys);
            }
            ss->c.state = connection_try_next_userauth(ss);
            return true;
        }

        priv = PrivateKeyForRecord(MemHandleLock(pubkey), passphrase);
        MemHandleUnlock(pubkey);

        if (!priv) return false; // bad passphrase, try again

        if (ss->username) arena_free(ss->username);
        ss->username = arena_strdup(username);

        connection_send_userauth_publickey(ss, username, priv, false);
        key_free(priv);

        // remove this pubkey from good_pubkeys
        // in case server rejects it despite the validity check
        // If this is the last valid key, pubkey authentication won't continue
        queue_remove_if_present(ss->c.good_pubkeys, pubkey);

        ss->c.state = CONNECTION_STATE_WAITING_FOR_USERAUTH;
        return true;
    } else {
        printf("connection: got passphrase outside waiting-for-passphrase\r\n");
        return false;
    }
}


void connection_use_bounds(ssh_session_t *ss, RectangleType bounds, 
                           int charsWide, int charsHigh)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state == CONNECTION_STATE_RUNNING) {
        connection_send_window_change(ss, ss->c.remoteChannel, 
                                      charsWide, charsHigh, 
                                      bounds.extent.x, bounds.extent.y);
    } else {
        // fixme need to send it later? maybe not
    }
}

static void connection_increase_receive_window(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.receiveWindow < RECEIVE_WINDOW_MIN) {
        uint32_t delta = RECEIVE_WINDOW_MAX - ss->c.receiveWindow;
        connection_send_window_adjust(ss, ss->c.remoteChannel, delta);
        ss->c.receiveWindow += delta;
    }
}


static void connection_handle_typed_data(ssh_session_t *ss, uint32_t type, 
                                         uint8_t *data, uint32_t datalen, 
                                         int closing)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.receiveWindow < datalen) {
        printf("connection: receive window full\r\n");
        return; // ignore data
    }
    ss->c.receiveWindow -= datalen;
    if (ss->c.receiveWindow < RECEIVE_WINDOW_MIN  &&  !closing) {
        connection_increase_receive_window(ss);
    }

    if (type == 0  ||  type == 1) {
        // fixme handle stderr differently?
        ssh_receive_bytes(ss, data, datalen);
    } else {
        printf("connection: got %d bytes of data typed %d\r\n", datalen, type);
    }
}



/***********************************************************************
 * incoming message handlers
 */

static connection_state_t 
connection_handle_unexpected_message(ssh_session_t *ss, 
                                     uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_GLOBAL_REQUEST) {
        // all global requests are refused
        uint16_t len;
        uint8_t *request;
        uint8_t want_reply;
        request = buffer_get_string(packet, &len);
        want_reply = buffer_get_char(packet);
        // do NOT require empty - there might be unknown request-specific data
        printf("connection (state %d): refused global request '%s'\r\n", 
                   ss->c.state, request);
        arena_free(request);
        if (want_reply) {
            connection_send_global_request_failure(ss);
        }
        return ss->c.state;
    } else if (msg == SSH2_MSG_CHANNEL_REQUEST) {
        // all channel requests are currently refused with a printf
        // EXCEPT: keepalive@openssh.com and exit-status are silently refused
        // fixme requests that may be desirable (would implement elsewhere):
        // xon-xoff, signal, exit-status, exit-signal, keepalive@openssh.com
        uint16_t len;
        uint32_t channel;
        uint8_t *request;
        uint8_t want_reply;
        channel = buffer_get_int(packet);
        request = buffer_get_string(packet, &len);
        want_reply = buffer_get_char(packet);
        // do NOT require empty - there might be unknown request-specific data
        // Don't print keepalives or exit-status
        if (0 != StrCompare(request, "keepalive@openssh.com")  &&  
            0 != StrCompare(request, "exit-status"))
        {
            printf("connection (state %d): refused channel request '%s'\r\n",
                   ss->c.state, request);
        }
        arena_free(request);
        if (ss->c.localChannel == 0  ||  channel != ss->c.localChannel) {
            // wrong channel or no channel - die
            ssh_kill(ss);
            return ss->c.state;
        }
        if (want_reply) {
            connection_send_channel_request_failure(ss, ss->c.remoteChannel);
        }
        return ss->c.state;
    } else {
        // fixme die?
        // fixme are there any other messages?
        // fixme send UNIMPLEMENTED?
        printf("connection (state %d): ignored unknown message %d\r\n", 
                   ss->c.state, msg);
        return ss->c.state;
    }
}


static connection_state_t 
connection_handle_userauth_service_accept(ssh_session_t *ss, Buffer *packet)
{
    uint16_t len;
    char *service = buffer_get_string(packet, &len);

    connection_log(__PRETTY_FUNCTION__);

    if (len != strlen("ssh-userauth")  ||  
        0 != memcmp(service, "ssh-userauth", len)) 
    {
        printf("connection: service-accept for wrong service '%s'\r\n",
                   service);
        arena_free(service);
        ssh_kill(ss);
        return ss->c.state;
    }

    arena_free(service);
    buffer_require_empty(packet);

    printf("   Authenticating (none) ... ");

    connection_send_authmethod_request(ss, ss->username);
    return CONNECTION_STATE_WAITING_FOR_USERAUTH;
}


static connection_state_t 
connection_handle_banner(ssh_session_t *ss, Buffer *packet)
{
    uint16_t len;
    char *msg;

    connection_log(__PRETTY_FUNCTION__);

    msg = buffer_get_string(packet, &len);
    arena_free(buffer_get_string(packet, &len)); // ignore language
    buffer_require_empty(packet);

    Banner(msg);

    arena_free(msg);
    return ss->c.state;
}


static connection_state_t 
connection_handle_userauth_success(ssh_session_t *ss, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    buffer_require_empty(packet);

    printf("succeeded\r\n"); // terminate "Authenticating..." line

    ss->c.localChannel = 42;
    ss->c.receiveWindow = RECEIVE_WINDOW_MAX;
    
    connection_send_channel_open(ss, ss->c.localChannel, 
                                 ss->c.receiveWindow, DATA_MAX);

    return CONNECTION_STATE_WAITING_FOR_CHANNEL_OPEN;    
}


static connection_state_t
connection_try_next_userauth(ssh_session_t *ss)
{
    connection_state_t result = 0;
    char *authmethod_name = NULL;

    connection_log(__PRETTY_FUNCTION__);

    // Choose an untried authmethod and try it.
    // publickey is only tried once
    // password is tried repeatedly until the user or server gives up

    while (result == 0) {
        authmethod_name = match_list(ss->c.untriedAuthmethods, 
                                     ss->c.usableAuthmethods, NULL);
        if (!authmethod_name) break;

        connection_log("authmethod %s", authmethod_name);

        if (0 == strcmp(authmethod_name, "publickey")) {
            
            // Public Key Authentication Process
            // 1: send public key acceptibility query for passphrase-less keys
            // 2: send auth attempts for acceptable passphrase-less keys
            // 3: send public key acceptibility query for passphrase-ful keys
            // 4: query user for passphrase-ful key and passphrase
            // 5: send auth attempt for passphrase-ful key
            // 1 and 2 are performed simultaneously (if an acceptable key is 
            //   found, immediately try to auth with it)
            // 4 and 5 are repeated until user or server gives up

            switch (ss->c.pubkey_state) {
            case PUBKEY_STARTING:
                printf("   Authenticating (publickey) ... ");
                ss->c.pubkey_state = PUBKEY_QUERY_PHRASELESS;
                // FALL-THROUGH to PUBKEY_QUERY_PHRASELESS
            case PUBKEY_QUERY_PHRASELESS:
                // steps 1 and 2
                if (!ss->c.phraseless_pubkeys) {
                    ss->c.phraseless_pubkeys = PhraselessPublicKeys();
                }
                if (!queue_empty(ss->c.phraseless_pubkeys)) {
                    result = connection_check_next_publickey(ss, ss->c.phraseless_pubkeys);
                    break;
                } else {
                    queue_free(ss->c.phraseless_pubkeys);
                    ss->c.phraseless_pubkeys = NULL;
                    ss->c.pubkey_state = PUBKEY_QUERY_PHRASEFUL;
                }
                // FALL-THROUGH to PUBKEY_QUERY_PHRASEFUL
            case PUBKEY_QUERY_PHRASEFUL:
                // step 3
                if (!ss->c.phraseful_pubkeys) {
                    ss->c.phraseful_pubkeys = PhrasefulPublicKeys();
                }
                if (!ss->c.good_pubkeys) {
                    ss->c.good_pubkeys = queue_new();
                }
                if (!queue_empty(ss->c.phraseful_pubkeys)) {
                    result = connection_check_next_publickey(ss, ss->c.phraseful_pubkeys);
                    break;
                } else {
                    queue_free(ss->c.phraseful_pubkeys);
                    ss->c.phraseful_pubkeys = NULL;
                    ss->c.pubkey_state = PUBKEY_GET_PASSPHRASE;
                }
                // FALL-THROUGH to PUBKEY_GET_PASSPHRASE
            case PUBKEY_GET_PASSPHRASE:
                // step 4 and 5
                if (!queue_empty(ss->c.good_pubkeys)) {
                    result = connection_get_passphrase(ss, ss->c.good_pubkeys);
                    break;
                } else {
                    queue_free(ss->c.good_pubkeys);
                    ss->c.good_pubkeys = NULL;
                    ss->c.pubkey_state = PUBKEY_DONE;
                }
                // FALL-THROUGH to PUBKEY_DONE
            case PUBKEY_DONE:
                // all steps completed with no successful auth - don't try more
                remove_match("publickey", ss->c.untriedAuthmethods);
                // terminate previous "Authenticating ..." line
                printf("failed\r\n");
                break;
            }
        }
        else if (0 == strcmp(authmethod_name, "password")) {
            // Try password authentication
            // Password auth is always considered "untried"; don't remove it.
            printf("   Authenticating (password) ... ");
            
            result = connection_get_password(ss);
        }

        arena_free(authmethod_name);
    }

    if (result == 0) {
        // No acceptable auth methods left. Bail.        
        printf("userauth: no more untried authmethods available (server offered '%s'\r\n", ss->c.usableAuthmethods);
        ssh_kill(ss);
        result = ss->c.state;
    }

    return result;
}

static connection_state_t 
connection_handle_userauth_failure(ssh_session_t *ss, Buffer *packet)
{
    uint16_t len;
    uint8_t partialSuccess;
    connection_state_t result = 0;

    connection_log(__PRETTY_FUNCTION__);

    // terminate previous "Authenticating ..." line
    printf("failed\r\n");

    if (ss->c.usableAuthmethods) arena_free(ss->c.usableAuthmethods);
    ss->c.usableAuthmethods = buffer_get_string(packet, &len);
    partialSuccess = buffer_get_char(packet);
    buffer_require_empty(packet);

    if (partialSuccess) {
        // "more authentications are needed" - not supported, bail
        printf("userauth: server requires more than simple password authentication (server offered '%s')\r\n", ss->c.usableAuthmethods);
        ssh_kill(ss);
        result = ss->c.state;
        goto done;
    }

    result = connection_try_next_userauth(ss);

 done:
    return result;
}


static connection_state_t
connection_handle_good_publickey(ssh_session_t *ss, Buffer *packet)
{
    // fixme verify good packet
    // fixme verify that returned results match the key
    connection_state_t result;
    char *p;

    connection_log(__PRETTY_FUNCTION__);

    p = MemHandleLock(ss->c.current_pubkey);
    if (*p == 0) {
        // not encrypted - send auth attempt with this key
        Key *priv = PrivateKeyForRecord(p, "");
        MemHandleUnlock(ss->c.current_pubkey);
        ss->c.current_pubkey = NULL;

        if (priv) {
            connection_send_userauth_publickey(ss, ss->username, priv, false);
            result = CONNECTION_STATE_WAITING_FOR_USERAUTH;
            key_free(priv);
        } else {
            // bogus key - try something else
            printf("bogus publickey! ");
            result = connection_try_next_userauth(ss);
        }
    } else {
        // encrypted - keep this key and try to validate more keys
        MemHandleUnlock(ss->c.current_pubkey);
        queue_enqueue(ss->c.good_pubkeys, ss->c.current_pubkey);
        ss->c.current_pubkey = NULL;

        result = connection_try_next_userauth(ss);
    }

    // clean up
    return result;
}


static connection_state_t
connection_handle_bad_publickey(ssh_session_t *ss, Buffer *packet)
{
    uint16_t len;

    connection_log(__PRETTY_FUNCTION__);

    // server rejected this public key - try something else
    if (ss->c.pubkey_state == PUBKEY_GET_PASSPHRASE) {
        // passphraseful key rejected by server - tell user
        FrmCustomAlert(AlertFormID, "Public key rejected by server.", " "," ");
    }

    // server might be refusing publickey auth now
    if (ss->c.usableAuthmethods) arena_free(ss->c.usableAuthmethods);
    ss->c.usableAuthmethods = buffer_get_string(packet, &len);
    buffer_get_char(packet); // partial success (ignore here)
    buffer_require_empty(packet);

    ss->c.current_pubkey = NULL;
    return connection_try_next_userauth(ss);
}


static connection_state_t 
connection_handle_password_change(ssh_session_t *ss, Buffer *packet)
{
    // fixme
    /*
    uint16_t promptLen, languageLen;
    uint8_t *prompt = buffer_get_string(packet, &promptLen);
    uint8_t *language = buffer_get_string(packet, &languageLen);
    arena_free(language);
    buffer_require_empty(packet);
    connection_get_new_password(ss, prompt, promptLen);
    arena_free(prompt);
    return CONNECTION_STATE_WAITING_FOR_NEW_PASSWORD;
    */
    connection_log(__PRETTY_FUNCTION__);

    printf("connection: password change unimplemented\r\n");
    return ss->c.state;
}


static connection_state_t 
connection_handle_channel_open(ssh_session_t *ss, Buffer *packet)
{
    uint32_t recipientChannel;
    uint32_t senderChannel;
    uint32_t windowSize;
    uint32_t packetSize;
    
    connection_log(__PRETTY_FUNCTION__);

    recipientChannel = buffer_get_int(packet);
    if (recipientChannel != ss->c.localChannel) {
        printf("connection: channel-open-confirmation had unexpected recipient channel (wanted %d, got %d)\r\n", ss->c.localChannel, recipientChannel);
        ssh_kill(ss);
        return ss->c.state;
    }

    senderChannel = buffer_get_int(packet);
    windowSize = buffer_get_int(packet);
    packetSize = buffer_get_int(packet);
    buffer_require_empty(packet);

    ss->c.remoteChannel = senderChannel;
    ss->c.sendWindow = windowSize;
    ss->c.sendMaxPacket = packetSize;
    ss->c.channelOpen = 1;

    connection_send_pty_request(ss, ss->c.remoteChannel);
    return CONNECTION_STATE_WAITING_FOR_PTY;
}


static connection_state_t 
connection_handle_channel_open_failure(ssh_session_t *ss, Buffer *packet)
{
    uint32_t channel;
    uint32_t reasonCode;
    uint16_t len;
    uint8_t *reasonString;
    uint16_t languageLen;
    uint8_t *languageString;

    connection_log(__PRETTY_FUNCTION__);

    channel = buffer_get_int(packet);
    // don't bother verifying channel - we're about to die anyway

    reasonCode = buffer_get_int(packet);
    reasonString = buffer_get_string(packet, &len);
    languageString = buffer_get_string(packet, &languageLen);
    // don't bother requiring empty packet - we're about to die anyway

    printf("connection: channel-open-failure because %d '%s'\r\n", 
               reasonCode, reasonString);
    arena_free(reasonString);
    arena_free(languageString);

    ssh_kill(ss);
    return ss->c.state;
}


static connection_state_t 
connection_handle_pty_success(ssh_session_t *ss, Buffer *packet)
{
    uint32_t channel = buffer_get_int(packet);

    connection_log(__PRETTY_FUNCTION__);

    if (channel != ss->c.localChannel) {
        printf("connection: pty success on wrong channel (wanted %d, got %d)\r\n", ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }

    buffer_require_empty(packet);

    connection_send_shell_request(ss, ss->c.remoteChannel);
    return CONNECTION_STATE_WAITING_FOR_SHELL;
}


static connection_state_t 
connection_handle_pty_failure(ssh_session_t *ss, Buffer *packet)
{
    // don't bother verifying channel - we're about to die anyway
    // don't bother requiring empty packet - we're about to die anyway
    connection_log(__PRETTY_FUNCTION__);

    printf("connection: pty failure\r\n");
    ssh_kill(ss);
    return ss->c.state;
}


static connection_state_t 
connection_handle_shell_success(ssh_session_t *ss, Buffer *packet)
{
    uint32_t channel = buffer_get_int(packet);

    connection_log(__PRETTY_FUNCTION__);

    if (channel != ss->c.localChannel) {
        printf("connection: shell success on wrong channel (wanted %d, got %d)\r\n", 
               ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }

    buffer_require_empty(packet);

    // nothing to send
    // fixme wire up tty if needed

    printf("Connected to host '%s'.\r\n", ss->s.hostname);

    return CONNECTION_STATE_RUNNING;
}


static connection_state_t 
connection_handle_shell_failure(ssh_session_t *ss, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    // don't bother verifying channel - we're about to die anyway
    // don't bother requiring empty packet - we're about to die anyway
    printf("connection: shell failure\r\n");
    ssh_kill(ss);
    return ss->c.state;
}


static connection_state_t
connection_handle_window_adjust(ssh_session_t *ss, Buffer *packet)
{
    uint32_t channel;
    uint32_t delta;
    
    connection_log(__PRETTY_FUNCTION__);

    channel = buffer_get_int(packet);
    if (channel != ss->c.localChannel) {
        printf("connection: window adjust got wrong channel (wanted %d, got %d)\r\n", ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }

    delta = buffer_get_int(packet);
    buffer_require_empty(packet);
    ss->c.sendWindow += delta;
    return ss->c.state;
}


static connection_state_t
connection_handle_channel_close(ssh_session_t *ss, Buffer *packet)
{
    uint32_t channel;

    connection_log(__PRETTY_FUNCTION__);

    channel = buffer_get_int(packet);
    if (channel != ss->c.localChannel) {
        printf("connection: close got wrong channel (wanted %d, got %d)\r\n", ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }
    buffer_require_empty(packet);

    ss->c.remoteCloseReceived = 1;
    ssh_close(ss);
    return ss->c.state;
}


static connection_state_t
connection_handle_data(ssh_session_t *ss, Buffer *packet, int closing)
{
    uint32_t channel;
    uint8_t *data;
    uint16_t len;

    connection_log("conn handle data: 0x%x (len 0x%x)", packet, packet->alloc);

    channel = buffer_get_int(packet);
    if (channel != ss->c.localChannel) {
        printf("connection: data got wrong channel (wanted %d, got %d)\r\n", ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }
    data = buffer_get_string(packet, &len);
    buffer_require_empty(packet);
    connection_handle_typed_data(ss, 0, data, len, closing);
    arena_free(data);
    return ss->c.state;
}


static connection_state_t 
connection_handle_extended_data(ssh_session_t *ss, Buffer *packet, int closing)
{
    uint32_t channel;
    uint32_t type;
    uint8_t *data;
    uint16_t len;

    connection_log(__PRETTY_FUNCTION__);

    channel = buffer_get_int(packet);
    if (channel != ss->c.localChannel) {
        printf("connection: extended data got wrong channel (wanted %d, got %d)\r\n", ss->c.localChannel, channel);
        ssh_kill(ss);
        return ss->c.state;
    }
    type = buffer_get_int(packet);
    data = buffer_get_string(packet, &len);
    buffer_require_empty(packet);
    connection_handle_typed_data(ss, type, data, len, closing);
    arena_free(data);
    return ss->c.state;
}








/***********************************************************************
 * state handlers
 */


// fixme move to transport
static connection_state_t 
connection_state_waiting_for_userauth_service(ssh_session_t *ss, 
                                              uint8_t msg, 
                                              Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_SERVICE_ACCEPT) {
        return connection_handle_userauth_service_accept(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_waiting_for_password(ssh_session_t *ss, 
                                      uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    // not expecting anything in particular - GUI triggers state change
    if (msg == SSH2_MSG_USERAUTH_BANNER) {
        return connection_handle_banner(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t
connection_state_waiting_for_userauth(ssh_session_t *ss, 
                                      uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_USERAUTH_SUCCESS) {
        return connection_handle_userauth_success(ss, packet);
    } else if (msg == SSH2_MSG_USERAUTH_FAILURE) {
        return connection_handle_userauth_failure(ss, packet);
    } else if (msg == SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ) {
        return connection_handle_password_change(ss, packet);
    } else if (msg == SSH2_MSG_USERAUTH_BANNER) {
        return connection_handle_banner(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t
connection_state_waiting_for_pubkey_check(ssh_session_t *ss, 
                                          uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_USERAUTH_PK_OK) {
        return connection_handle_good_publickey(ss, packet);
    } else if (msg == SSH2_MSG_USERAUTH_FAILURE) {
        return connection_handle_bad_publickey(ss, packet);
    } else if (msg == SSH2_MSG_USERAUTH_BANNER) {
        return connection_handle_banner(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_waiting_for_pubkey_passphrase(ssh_session_t *ss, 
                                               uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    // not expecting anything in particular - GUI triggers state change
    if (msg == SSH2_MSG_USERAUTH_BANNER) {
        return connection_handle_banner(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t
connection_state_waiting_for_channel_open(ssh_session_t *ss, 
                                          uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_CHANNEL_OPEN_CONFIRMATION) {
        return connection_handle_channel_open(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_OPEN_FAILURE) {
        return connection_handle_channel_open_failure(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_waiting_for_pty(ssh_session_t *ss, 
                                 uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_CHANNEL_SUCCESS) {
        return connection_handle_pty_success(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_FAILURE) {
        return connection_handle_pty_failure(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_WINDOW_ADJUST) {
        return connection_handle_window_adjust(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_CLOSE) {
        return connection_handle_channel_close(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_waiting_for_shell(ssh_session_t *ss, 
                                   uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_CHANNEL_SUCCESS) {
        return connection_handle_shell_success(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_FAILURE) {
        return connection_handle_shell_failure(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_WINDOW_ADJUST) {
        return connection_handle_window_adjust(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_CLOSE) {
        return connection_handle_channel_close(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_running(ssh_session_t *ss, 
                         uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_CHANNEL_DATA) {
        return connection_handle_data(ss, packet, 0);
    } else if (msg == SSH2_MSG_CHANNEL_EXTENDED_DATA) {
        return connection_handle_extended_data(ss, packet, 0);
    } else if (msg == SSH2_MSG_CHANNEL_WINDOW_ADJUST) {
        return connection_handle_window_adjust(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_CLOSE) {
        return connection_handle_channel_close(ss, packet);
    } else if (msg == SSH2_MSG_CHANNEL_EOF) {
        // silently ignore (fixme correct?)
        return ss->c.state;
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}



static connection_state_t 
connection_state_closing(ssh_session_t *ss, 
                         uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    if (msg == SSH2_MSG_CHANNEL_DATA) {
        return connection_handle_data(ss, packet, 1);
    } else if (msg == SSH2_MSG_CHANNEL_EXTENDED_DATA) {
        return connection_handle_extended_data(ss, packet, 1);
    } else if (msg == SSH2_MSG_CHANNEL_WINDOW_ADJUST) {
        // ignore window adjust - we're sending no more data
        return ss->c.state;
    } else if (msg == SSH2_MSG_CHANNEL_CLOSE) {
        return connection_handle_channel_close(ss, packet);
    } else {
        return connection_handle_unexpected_message(ss, msg, packet);
    }
}


static connection_state_t 
connection_state_closed(ssh_session_t *ss, 
                        uint8_t msg, Buffer *packet)
{
    connection_log(__PRETTY_FUNCTION__);

    // shouldn't happen
    printf("connection: packet sent to connection_state_closed!\r\n");
    return connection_handle_unexpected_message(ss, msg, packet);
}

/***********************************************************************
 * incoming packet handler 
 */


// msg is gone from packet
void connection_receive_packet(ssh_session_t *ss, uint8_t msg, Buffer *packet)
{
    connection_state_t s = ss->c.state;

    connection_log("conn (state %d): received msg %d\r\n", ss->c.state, msg);

    switch (ss->c.state) {
    case CONNECTION_STATE_WAITING_FOR_USERAUTH_SERVICE:
        s = connection_state_waiting_for_userauth_service(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_PASSWORD:
    case CONNECTION_STATE_WAITING_FOR_NEW_PASSWORD:
        s = connection_state_waiting_for_password(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_USERAUTH:
        s = connection_state_waiting_for_userauth(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_PUBKEY_CHECK:
        s = connection_state_waiting_for_pubkey_check(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_PUBKEY_PASSPHRASE:
        s = connection_state_waiting_for_pubkey_passphrase(ss, msg, packet);
        break;

    case CONNECTION_STATE_WAITING_FOR_CHANNEL_OPEN:
        s = connection_state_waiting_for_channel_open(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_PTY:
        s = connection_state_waiting_for_pty(ss, msg, packet);
        break;
    case CONNECTION_STATE_WAITING_FOR_SHELL:
        s = connection_state_waiting_for_shell(ss, msg, packet);
        break;
    case CONNECTION_STATE_RUNNING:
        s = connection_state_running(ss, msg, packet);
        break;
    case CONNECTION_STATE_CLOSING:
        s = connection_state_closing(ss, msg, packet);
        break;
    case CONNECTION_STATE_CLOSED:
        s = connection_state_closed(ss, msg, packet);
        break;

    default:
        printf("connection: BUSTED\r\n");
        break;
    }

    ss->c.state = s;
}


void connection_send_data(ssh_session_t *ss, uint8_t *bytes, uint16_t len)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state == CONNECTION_STATE_RUNNING) {
        // fixme enforce packet sizes and send window
        connection_send_channel_data(ss, ss->c.remoteChannel, bytes, len);
    }
}


/***********************************************************************
 * startup and shutdown 
 */

void connection_start(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state == CONNECTION_STATE_STARTING) {
        char *auths = arena_calloc(1+strlen("publickey,")+strlen("password,"));
        if (PrefsGetInt(prefAuthPublicKey, 1)) strcat(auths, "publickey,");
        if (PrefsGetInt(prefAuthPassword, 1)) strcat(auths, "password,");
        // use password if nothing is specified
        if (strlen(auths) == 0) strcat(auths, "password,"); 
        auths[strlen(auths)-1] = '\0'; // squish trailing comma

        ss->c.untriedAuthmethods = auths;

        printf("Logging in to host '%s'\r\n", ss->s.hostname);
        connection_send_service_request(ss, "ssh-userauth");
        ss->c.state = CONNECTION_STATE_WAITING_FOR_USERAUTH_SERVICE;
    }
}


static void connection_cleanup(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.untriedAuthmethods) arena_free(ss->c.untriedAuthmethods);
    if (ss->c.usableAuthmethods) arena_free(ss->c.usableAuthmethods);
    queue_free(ss->c.phraseful_pubkeys);
    queue_free(ss->c.phraseless_pubkeys);
    queue_free(ss->c.good_pubkeys);
    memset(&ss->c, 0, sizeof(ss->c));
    ss->c.state = CONNECTION_STATE_CLOSED;
}


void connection_kill(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state != CONNECTION_STATE_CLOSED) {
        connection_cleanup(ss);
    }
}


// clean shutdown
// send CHANNEL_CLOSE and wait for remote, if necessary
void connection_close(ssh_session_t *ss)
{
    connection_log(__PRETTY_FUNCTION__);

    if (ss->c.state != CONNECTION_STATE_CLOSED) {
        if (! ss->c.channelOpen) {
            // channel not open - no two-way handshake needed
            connection_kill(ss);
        } else {
            // handle two-way channel close
            if (!ss->c.localCloseSent) {
                // tell remote that we want to close
                connection_send_channel_close(ss, ss->c.remoteChannel);
                ss->c.localCloseSent = 1;
            }
            if (!ss->c.remoteCloseReceived) {
                // continue processing packets until remote's channel close arrives
                ss->c.state = CONNECTION_STATE_CLOSING;
            } 
            if (ss->c.localCloseSent  &&  ss->c.remoteCloseReceived) {
                // two-way handshake done - really close
                ss->c.channelOpen = 0;
                ss->c.localCloseSent = 0;
                ss->c.remoteCloseReceived = 0;
                connection_kill(ss);
            }
        }
    }
}


int connection_is_open(ssh_session_t *ss) {
    connection_log(__PRETTY_FUNCTION__);

    return (ss->c.state != CONNECTION_STATE_STARTING  &&  
            ss->c.state != CONNECTION_STATE_CLOSING  &&  
            ss->c.state != CONNECTION_STATE_CLOSED);
}


int connection_is_closing(ssh_session_t *ss) {
    connection_log(__PRETTY_FUNCTION__);

    return (ss->c.state == CONNECTION_STATE_CLOSING);
}
