# include "phantasmal/ssh.h"
# include "phantasmal/ssh_asn1.h"

# define DEBUG SSH_DEBUG

inherit SSH_TRANSPORT;
private inherit SSH_UTILS;
private inherit ASN1_UTILS;

static int client(string str);


/* ========================================================================= *
 *			   Section 1: authentication			     *
 * ========================================================================= */

private int logged_in;		/* already logged in? */

/*
 * NAME:	userauth_banner()
 * DESCRIPTION:	display a banner during the authentication period
 */
private int userauth_banner(string str)
{
    return ::message(make_mesg(SSH_MSG_USERAUTH_BANNER) +
		     make_string(str) +
		     make_string("en"));
}

/*
 * NAME:        ssh_dss_verify()
 * DESCRIPTION: verify that the signature was created with a given key, be it
 *              DSA or RSA.
 */
private int ssh_dss_verify(string algo, string m, string public_key, string signature)
{
    string p, q, g, y, r, s, s_blob, u, v, w, c, e, n;
    int offset, length;
    string format;

    offset = 0;
    format = get_string(signature, offset); offset += 4 + strlen(format);
    s_blob = get_string(signature, offset); offset += 4 + strlen(s_blob);

    if (format != algo) {
	/* The wrong type of signature. */
	return FALSE;
    }

    offset = 0;
    format = get_string(public_key, offset); offset += 4 + strlen(format);

    if (format != algo) {
	/* The wrong type of public key. */
	return FALSE;
    }

    switch (format) {
    case "ssh-dss":
	p      = get_mpint(public_key, offset);  offset += 4 + strlen(p);
	q      = get_mpint(public_key, offset);  offset += 4 + strlen(q);
	g      = get_mpint(public_key, offset);  offset += 4 + strlen(g);
	y      = get_mpint(public_key, offset);  offset += 4 + strlen(y);

	if (strlen(s_blob) != 40) {
	    /* Signature blob should be 40 bytes long, two 160 bit integers. */
	    return FALSE;
	}
	r = "\0" + s_blob[ 0..19];
	s = "\0" + s_blob[20..39];
	
	w = asn_pow(s, asn_sub(q, "\2", q), q);
	u = asn_mult(w, "\0" + hash_sha1(m), q);
	v = asn_mult(w, r, q);
	return asn_cmp(asn_mod(asn_mult(asn_pow(g, u, p),
					asn_pow(y, v, p),
					p),
			       q),
		       r) == 0;
    case "ssh-rsa": {
	int i, sz;

	e = get_mpint(public_key, offset); offset += 4 + strlen(e);
	n = get_mpint(public_key, offset); offset += 4 + strlen(n);

	s = "\0" + s_blob;
	v = asn_pow(s, e, n);

	if (strlen(v) == 0 || v[0] != 0x01) {
	    return FALSE;
	}
	for (i = 1, sz = strlen(v); i < sz; i++) {
	    switch (v[i]) {
	    case 0xff:
		break;
	    case 0x00: {
		mixed *asn1;

		v = v[i + 1 ..];
		asn1 = parse_asn1(v, 0);
		if (asn1) {
		    /*
		     * Format looks like this:
		     *
		     * ASN_SEQUENCE {
		     *   ASN_SEQUENCE {
		     *     ASN_OBJECT_ID 2B:0E:03:02:1A
		     *     ASN_NULL
		     *   }
		     *   ASN_OCTET_STR ...SHA1 result...
		     * }
		     *
		     * See also ftp://ftp.isi.edu/in-notes/rfc2437.txt
		     */
		    asn1 = asn1[0];
		    return
			asn1[ASN_TYPE]   == ASN_CONSTRUCTOR &&
			asn1[ASN_NUMBER] == ASN_SEQUENCE &&
			sizeof(asn1[ASN_CONTENTS]) >= 2 &&
			asn1[ASN_CONTENTS][1][ASN_TYPE]     == ASN_PRIMITIVE &&
			asn1[ASN_CONTENTS][1][ASN_NUMBER]   == ASN_OCTET_STR &&
			asn1[ASN_CONTENTS][1][ASN_CONTENTS] == hash_sha1(m);
		}
		return FALSE;
	    }
	    default:
		return FALSE;
	    }
	}
	return FALSE;
    }
    default:
	return FALSE;
    }
}

/*
 * NAME:	userauth()
 * DESCRIPTION:	respond to a userauth service request
 */
static int userauth(string str)
{
    string name, service, method, password;
    int offset;

    switch (str[0]) {
    case SSH_MSG_SERVICE_REQUEST:
	if (get_string(str, 1) == "ssh-userauth" && !logged_in) {
	    ::message(make_mesg(SSH_MSG_SERVICE_ACCEPT) +
		      make_string("ssh-userauth"));
	} else {
	    ::message(make_mesg(SSH_MSG_DISCONNECT) +
		      make_int(SSH_DISCONNECT_SERVICE_NOT_AVAILABLE) +
		      make_string("service not available") +
		      make_string("en"));
	    return MODE_DISCONNECT;
	}
	break;

    case SSH_MSG_USERAUTH_REQUEST:
	if (!logged_in) {
	    offset = 1;                    name = get_string(str, offset);
	    offset += 4 + strlen(name);    service = get_string(str, offset);
	    offset += 4 + strlen(service); method = get_string(str, offset);
	    offset += 4 + strlen(method);

	    if (service == "ssh-connection") {
		switch (method) {
		case "none":
		    ::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
			      make_string("publickey,password") +
			      "\0");
		    break;
		case "publickey":
		    if (!str[offset]) {
			/* Testing the water, which algorithm is allowed. */
			string algo;
			string blob;

			algo = get_string(str, offset + 1);
			blob = get_string(str, offset + 1 + 4 + strlen(algo));
			switch (algo) {
			case "ssh-dss":
			case "ssh-rsa":
			    if (SSHD->valid_public_key(name, blob)) {
				::message(make_mesg(SSH_MSG_USERAUTH_PK_OK) +
					  make_string(algo) +
					  make_string(blob));
			    } else {
				::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
					  make_string("publickey,password") +
					  "\0");
			    }
			    break;
			default:
			    ::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
				      make_string("publickey,password") +
				      "\0");
			    break;
			}
		    } else {
			string algo;
			string publickey;
			string signature;
			string data;

			offset++;
                        algo = get_string(str, offset);
			offset += 4 + strlen(algo);
			publickey = get_string(str, offset);
			offset += 4 + strlen(publickey);
			signature = get_string(str, offset);

			if (algo == "ssh-dss" || algo == "ssh-rsa") {
			    /*
			     * Signature is made from this data:
			     *
			     * string    session identifier
			     * byte      SSH_MSG_USERAUTH_REQUEST
			     * string    user name
			     * string    service
			     * string    "publickey"
			     * boolean   TRUE
			     * string    public key algorithm name
			     * string    public key to be used for authentication
			     */
			    data =
				make_string(query_session_id()) +
				make_mesg(SSH_MSG_USERAUTH_REQUEST) +
				make_string(name) +
				make_string(service) +
				make_string("publickey") +
				"\1" +
				make_string(algo) +
				make_string(publickey);
			    if (SSHD->valid_public_key(name, publickey) &&
				(ssh_dss_verify(algo, data, publickey, signature))) {
				if (ssh_get_user(name)) {
				    ssh_login();
				    logged_in = TRUE;
				    ::message(make_mesg(SSH_MSG_USERAUTH_SUCCESS));
				} else {
				    /* no such user? */
				    ::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
					      make_string("publickey,password") +
					      "\0");
				    return MODE_DISCONNECT;
				}
			    } else {
				::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
					  make_string("publickey,password") +
					  "\0");
			    }
			} else {
			    ::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
				      make_string("publickey,password") +
				      "\0");
			}
		    }
		    break;
		case "password":
		    if (!str[offset]) {
			password = get_string(str, offset + 1);
			if (ssh_get_user(name) && ssh_check_password(password))
			{
			    ssh_login();
			    logged_in = TRUE;
			    ::message(make_mesg(SSH_MSG_USERAUTH_SUCCESS));
			    break;
			}
			DEBUG(0, "login failed for " + name);
			::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
				  make_string("publickey,password") +
				  "\0");
			if (!ssh_get_user(name)) {
			    /* not allowed to try again */
			    return MODE_DISCONNECT;
			}
			break;
		    }
		    ::message(make_mesg(SSH_MSG_USERAUTH_FAILURE) +
			      make_string("publickey,password") +
			      "\0");
		    break;
		default:
		    break;
		}
	    }
	}
	break;

    default:
	if (logged_in) {
	    return client(str);
	}
	break;
    }

    return MODE_NOCHANGE;
}


/* ========================================================================= *
 *			  Section 2: connection layer			     *
 * ========================================================================= */

private int channel;		/* channel ID */
private int window_size;	/* transmit window */
private int packet_size;	/* maximum packet size */
private int program;		/* program started? */

/*
 * NAME:	message()
 * DESCRIPTION:	send a message to the client
 */
atomic int message(string str)
{
    int keep_sending;

    if (channel >= 0 && window_size >= strlen(str)) {
	window_size -= strlen(str);
	keep_sending = TRUE;
	while (strlen(str) > packet_size) {
	    keep_sending &= ::message(make_mesg(SSH_MSG_CHANNEL_DATA) +
				      make_int(channel) +
				      make_string(str[.. packet_size - 1]));
	    str = str[packet_size ..];
	}
	return keep_sending & ::message(make_mesg(SSH_MSG_CHANNEL_DATA) +
					make_int(channel) +
					make_string(str));
    }
}

/*
 * NAME:	client()
 * DESCRIPTION:	handle a client message
 */
static int client(string str)
{
    int offset, channel_id;
    string type;

    switch (str[0]) {
    case SSH_MSG_GLOBAL_REQUEST:
	type = get_string(str, 1);
	offset = strlen(type) + 5;
	if (str[offset]) {
	    ::message(make_mesg(SSH_MSG_REQUEST_FAILURE));
	}
	break;

    case SSH_MSG_CHANNEL_OPEN:
	type = get_string(str, 1);
	offset = 12;
	channel_id = get_int(str, offset);
	offset += 4;
	if (type != "session") {
	    ::message(make_mesg(SSH_MSG_CHANNEL_OPEN_FAILURE) +
		      make_int(channel_id) +
		      make_int(SSH_OPEN_UNKNOWN_CHANNEL_TYPE) +
		      make_string("unknown channel type") +
		      make_string("en"));
	    break;
	}
	if (channel >= 0) {
	    ::message(make_mesg(SSH_MSG_CHANNEL_OPEN_FAILURE) +
		      make_int(channel_id) +
		      make_int(SSH_OPEN_RESOURCE_SHORTAGE) +
		      make_string("out of channels") +
		      make_string("en"));
	    break;
	}

	channel = channel_id;
	window_size = get_int(str, offset);
	offset += 4;
	packet_size = get_int(str, offset);
	::message(make_mesg(SSH_MSG_CHANNEL_OPEN_CONFIRMATION) +
		  make_int(channel_id) +
		  make_int(channel_id) +
		  make_int(0xffffffff) +
		  make_int(2048));
	break;

    case SSH_MSG_CHANNEL_CLOSE:
	if (get_int(str, 1) == channel) {
	    ::message(make_mesg(SSH_MSG_CHANNEL_CLOSE) +
		      make_int(channel));
	    channel = -1;
	    program = FALSE;
	}
	break;

    case SSH_MSG_CHANNEL_WINDOW_ADJUST:
	if (get_int(str, 1) == channel) {
	    window_size += get_int(str, 5);
	}
	break;

    case SSH_MSG_CHANNEL_DATA:
	if (get_int(str, 1) == channel && program) {
	    str = get_string(str, 5);
	    sscanf(str, "%s\r\n", str);
	    sscanf(str, "%s\n", str);
	    return user_input(str);
	}
	break;

    case SSH_MSG_CHANNEL_REQUEST:
	channel_id = get_int(str, 1);
	type = get_string(str, 5);
	offset = strlen(type) + 9;
	if (channel_id == channel && type == "shell" && !program) {
	    program = TRUE;
	    if (str[offset]) {
		::message(make_mesg(SSH_MSG_CHANNEL_SUCCESS) +
			  make_int(channel_id));
	    }
	    ssh_shell();
	} else if (str[offset]) {
	    ::message(make_mesg(SSH_MSG_CHANNEL_FAILURE) +
		      make_int(channel_id));
	}
	break;

    case SSH_MSG_CHANNEL_EXTENDED_DATA:
    case SSH_MSG_CHANNEL_EOF:
	break;	/* ignore */

    default: 
	::message(make_mesg(SSH_MSG_UNIMPLEMENTED) +
		  make_int(recv_seqno()));
	break;
    }

    return MODE_NOCHANGE;
}

/*
 * NAME:	create_ssh()
 * DESCRIPTION:	initialize secure shell
 */
static void create_ssh()
{
    create_transport();
    channel = -1;
    set_port(SSH_PORT);
}
