/*
 * Copyright (c) 2006 Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 * 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.
 *
 * 3. Neither the name of KTH nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY KTH AND ITS CONTRIBUTORS ``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 KTH OR ITS CONTRIBUTORS 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 "common.h"
RCSID("$Id$");

/*
 *
 */

enum handle_type { handle_context, handle_cred };

struct handle {
    int32_t idx;
    enum handle_type type;
    void *ptr;
    struct handle *next;
};

struct client {
    krb5_storage *sock;
    krb5_storage *logging;
    char *moniker;
    int32_t nHandle;
    struct handle *handles;
    struct sockaddr_storage sa;
    socklen_t salen;
    char servername[MAXHOSTNAMELEN];
};

FILE *logfile;
static char *targetname;
krb5_context context;

/*
 *
 */

static void
logmessage(struct client *c, const char *file, unsigned int lineno,
	   int level, const char *fmt, ...)
{
    char *message;
    va_list ap;
    int32_t ackid;
    int ret;

    va_start(ap, fmt);
    ret = vasprintf(&message, fmt, ap);
    va_end(ap);
    if (ret == -1)
	errx(1, "out of memory");

    if (logfile)
	fprintf(logfile, "%s:%u: %d %s\n", file, lineno, level, message);

    if (c->logging) {
	if (krb5_store_int32(c->logging, eLogInfo) != 0)
	    errx(1, "krb5_store_int32: log level");
	if (krb5_store_string(c->logging, file) != 0)
	    errx(1, "krb5_store_string: filename");
	if (krb5_store_int32(c->logging, lineno) != 0)
	    errx(1, "krb5_store_string: filename");
	if (krb5_store_string(c->logging, message) != 0)
	    errx(1, "krb5_store_string: message");
	if (krb5_ret_int32(c->logging, &ackid) != 0)
	    errx(1, "krb5_ret_int32: ackid");
    }
    free(message);
}

/*
 *
 */

static int32_t
add_handle(struct client *c, enum handle_type type, void *data)
{
    struct handle *h;

    h = ecalloc(1, sizeof(*h));

    h->idx = ++c->nHandle;
    h->type = type;
    h->ptr = data;
    h->next = c->handles;
    c->handles = h;

    return h->idx;
}

static void
del_handle(struct handle **h, int32_t idx)
{
    OM_uint32 min_stat;

    if (idx == 0)
	return;

    while (*h) {
	if ((*h)->idx == idx) {
	    struct handle *p = *h;
	    *h = (*h)->next;
	    switch(p->type) {
	    case handle_context: {
		gss_ctx_id_t c = p->ptr;
		gss_delete_sec_context(&min_stat, &c, NULL);
		break; }
	    case handle_cred: {
		gss_cred_id_t c = p->ptr;
		gss_release_cred(&min_stat, &c);
		break; }
	    }
	    free(p);
	    return;
	}
	h = &((*h)->next);
    }
    errx(1, "tried to delete an unexisting handle");
}

static void *
find_handle(struct handle *h, int32_t idx, enum handle_type type)
{
    if (idx == 0)
	return NULL;

    while (h) {
	if (h->idx == idx) {
	    if (type == h->type)
		return h->ptr;
	    errx(1, "monger switched type on handle!");
	}
	h = h->next;
    }
    return NULL;
}


static int32_t
convert_gss_to_gsm(OM_uint32 maj_stat)
{
    switch(maj_stat) {
    case 0:
	return GSMERR_OK;
    case GSS_S_CONTINUE_NEEDED:
	return GSMERR_CONTINUE_NEEDED;
    case GSS_S_DEFECTIVE_TOKEN:
        return GSMERR_INVALID_TOKEN;
    case GSS_S_BAD_MIC:
	return GSMERR_AP_MODIFIED;
    default:
	return GSMERR_ERROR;
    }
}

static int32_t
convert_krb5_to_gsm(krb5_error_code ret)
{
    switch(ret) {
    case 0:
	return GSMERR_OK;
    default:
	return GSMERR_ERROR;
    }
}

/*
 *
 */

static int32_t
acquire_cred(struct client *c,
	     krb5_principal principal,
	     krb5_get_init_creds_opt *opt,
	     int32_t *handle)
{
    krb5_error_code ret;
    krb5_creds cred;
    krb5_ccache id;
    gss_cred_id_t gcred;
    OM_uint32 maj_stat, min_stat;

    *handle = 0;

    krb5_get_init_creds_opt_set_forwardable (opt, 1);
    krb5_get_init_creds_opt_set_renew_life (opt, 3600 * 24 * 30);

    memset(&cred, 0, sizeof(cred));

    ret = krb5_get_init_creds_password (context,
					&cred,
					principal,
					NULL,
					NULL,
					NULL,
					0,
					NULL,
					opt);
    if (ret) {
	logmessage(c, __FILE__, __LINE__, 0,
		   "krb5_get_init_creds failed: %d", ret);
	return convert_krb5_to_gsm(ret);
    }

    ret = krb5_cc_new_unique(context, "MEMORY", NULL, &id);
    if (ret)
	krb5_err (context, 1, ret, "krb5_cc_initialize");

    ret = krb5_cc_initialize (context, id, cred.client);
    if (ret)
	krb5_err (context, 1, ret, "krb5_cc_initialize");

    ret = krb5_cc_store_cred (context, id, &cred);
    if (ret)
	krb5_err (context, 1, ret, "krb5_cc_store_cred");

    krb5_free_cred_contents (context, &cred);

    maj_stat = gss_krb5_import_cred(&min_stat,
				    id,
				    NULL,
				    NULL,
				    &gcred);
    krb5_cc_close(context, id);
    if (maj_stat) {
	logmessage(c, __FILE__, __LINE__, 0,
		   "krb5 import creds failed with: %d", maj_stat);
	return convert_gss_to_gsm(maj_stat);
    }

    *handle = add_handle(c, handle_cred, gcred);

    return 0;
}


/*
 *
 */

#define HandleOP(h) \
handle##h(enum gssMaggotOp op, struct client *c)

/*
 *
 */

static int
HandleOP(GetVersionInfo)
{
    put32(c, GSSMAGGOTPROTOCOL);
    errx(1, "GetVersionInfo");
}

static int
HandleOP(GoodBye)
{
    struct handle *h = c->handles;
    unsigned int i = 0;

    while (h) {
	h = h->next;
	i++;
    }

    if (i)
	logmessage(c, __FILE__, __LINE__, 0,
		   "Did not toast all resources: %d", i);
    return 1;
}

static int
HandleOP(InitContext)
{
    OM_uint32 maj_stat, min_stat, ret_flags;
    int32_t hContext, hCred, flags;
    krb5_data target_name, in_token;
    int32_t new_context_id = 0, gsm_error = 0;
    krb5_data out_token = { 0 , NULL };

    gss_ctx_id_t ctx;
    gss_cred_id_t creds;
    gss_name_t gss_target_name;
    gss_buffer_desc input_token;
    gss_buffer_desc output_token = {0, 0};
    gss_OID oid = GSS_C_NO_OID;
    gss_buffer_t input_token_ptr = GSS_C_NO_BUFFER;

    ret32(c, hContext);
    ret32(c, hCred);
    ret32(c, flags);
    retdata(c, target_name);
    retdata(c, in_token);

    logmessage(c, __FILE__, __LINE__, 0,
	       "targetname: <%.*s>", (int)target_name.length,
	       (char *)target_name.data);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	hContext = 0;
    creds = find_handle(c->handles, hCred, handle_cred);
    if (creds == NULL)
	abort();

    input_token.length = target_name.length;
    input_token.value = target_name.data;

    maj_stat = gss_import_name(&min_stat,
			       &input_token,
			       GSS_KRB5_NT_PRINCIPAL_NAME,
			       &gss_target_name);
    if (GSS_ERROR(maj_stat)) {
	logmessage(c, __FILE__, __LINE__, 0,
		   "import name creds failed with: %d", maj_stat);
	gsm_error = convert_gss_to_gsm(maj_stat);
	goto out;
    }

    /* oid from flags */

    if (in_token.length) {
	input_token.length = in_token.length;
	input_token.value = in_token.data;
	input_token_ptr = &input_token;
	if (ctx == NULL)
	    krb5_errx(context, 1, "initcreds, context NULL, but not first req");
    } else {
	input_token.length = 0;
	input_token.value = NULL;
	if (ctx)
	    krb5_errx(context, 1, "initcreds, context not NULL, but first req");
    }

    if ((flags & GSS_C_DELEG_FLAG) != 0)
	logmessage(c, __FILE__, __LINE__, 0, "init_sec_context delegating");
    if ((flags & GSS_C_DCE_STYLE) != 0)
	logmessage(c, __FILE__, __LINE__, 0, "init_sec_context dce-style");

    maj_stat = gss_init_sec_context(&min_stat,
				    creds,
				    &ctx,
				    gss_target_name,
				    oid,
				    flags & 0x7f,
				    0,
				    NULL,
				    input_token_ptr,
				    NULL,
				    &output_token,
				    &ret_flags,
				    NULL);
    if (GSS_ERROR(maj_stat)) {
	if (hContext != 0)
	    del_handle(&c->handles, hContext);
	new_context_id = 0;
	logmessage(c, __FILE__, __LINE__, 0,
		   "gss_init_sec_context returns code: %d/%d",
		   maj_stat, min_stat);
    } else {
	if (input_token.length == 0)
	    new_context_id = add_handle(c, handle_context, ctx);
	else
	    new_context_id = hContext;
    }

    gsm_error = convert_gss_to_gsm(maj_stat);

    if (output_token.length) {
	out_token.data = output_token.value;
	out_token.length = output_token.length;
    }

out:
    logmessage(c, __FILE__, __LINE__, 0,
	       "InitContext return code: %d", gsm_error);

    put32(c, new_context_id);
    put32(c, gsm_error);
    putdata(c, out_token);

    gss_release_name(&min_stat, &gss_target_name);
    if (output_token.length)
	gss_release_buffer(&min_stat, &output_token);
    krb5_data_free(&in_token);
    krb5_data_free(&target_name);

    return 0;
}

static int
HandleOP(AcceptContext)
{
    OM_uint32 maj_stat, min_stat, ret_flags;
    int32_t hContext, deleg_hcred, flags;
    krb5_data in_token;
    int32_t new_context_id = 0, gsm_error = 0;
    krb5_data out_token = { 0 , NULL };

    gss_ctx_id_t ctx;
    gss_cred_id_t deleg_cred = GSS_C_NO_CREDENTIAL;
    gss_buffer_desc input_token, output_token;

    ret32(c, hContext);
    ret32(c, flags);
    retdata(c, in_token);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	hContext = 0;

    if (in_token.length) {
	input_token.length = in_token.length;
	input_token.value = in_token.data;
    } else {
	input_token.length = 0;
	input_token.value = NULL;
    }

    maj_stat = gss_accept_sec_context(&min_stat,
				      &ctx,
				      GSS_C_NO_CREDENTIAL,
				      &input_token,
				      GSS_C_NO_CHANNEL_BINDINGS,
				      NULL,
				      NULL,
				      &output_token,
				      &ret_flags,
				      NULL,
				      &deleg_cred);
    if (GSS_ERROR(maj_stat)) {
	if (hContext != 0)
	    del_handle(&c->handles, hContext);
	logmessage(c, __FILE__, __LINE__, 0,
		   "gss_accept_sec_context returns code: %d/%d",
		   maj_stat, min_stat);
	new_context_id = 0;
    } else {
	if (hContext == 0)
	    new_context_id = add_handle(c, handle_context, ctx);
	else
	    new_context_id = hContext;
    }
    if (output_token.length) {
	out_token.data = output_token.value;
	out_token.length = output_token.length;
    }
    if ((ret_flags & GSS_C_DCE_STYLE) != 0)
	logmessage(c, __FILE__, __LINE__, 0, "accept_sec_context dce-style");
    if ((ret_flags & GSS_C_DELEG_FLAG) != 0) {
	deleg_hcred = add_handle(c, handle_cred, deleg_cred);
	logmessage(c, __FILE__, __LINE__, 0,
		   "accept_context delegated handle: %d", deleg_hcred);
    } else {
	gss_release_cred(&min_stat, &deleg_cred);
	deleg_hcred = 0;
    }


    gsm_error = convert_gss_to_gsm(maj_stat);

    put32(c, new_context_id);
    put32(c, gsm_error);
    putdata(c, out_token);
    put32(c, deleg_hcred);

    if (output_token.length)
	gss_release_buffer(&min_stat, &output_token);
    krb5_data_free(&in_token);

    return 0;
}

static int
HandleOP(ToastResource)
{
    int32_t handle;

    ret32(c, handle);
    logmessage(c, __FILE__, __LINE__, 0, "toasting %d", handle);
    del_handle(&c->handles, handle);
    put32(c, GSMERR_OK);

    return 0;
}

static int
HandleOP(AcquireCreds)
{
    char *name, *password;
    int32_t gsm_error, flags, handle = 0;
    krb5_principal principal = NULL;
    krb5_get_init_creds_opt *opt = NULL;
    krb5_error_code ret;

    retstring(c, name);
    retstring(c, password);
    ret32(c, flags);

    logmessage(c, __FILE__, __LINE__, 0,
	       "username: %s password: %s", name, password);

    ret = krb5_parse_name(context, name, &principal);
    if (ret) {
	gsm_error = convert_krb5_to_gsm(ret);
	goto out;
    }

    ret = krb5_get_init_creds_opt_alloc (context, &opt);
    if (ret)
	krb5_err(context, 1, ret, "krb5_get_init_creds_opt_alloc");

    krb5_get_init_creds_opt_set_pa_password(context, opt, password, NULL);

    gsm_error = acquire_cred(c, principal, opt, &handle);

out:
    logmessage(c, __FILE__, __LINE__, 0,
	       "AcquireCreds handle: %d return code: %d", handle, gsm_error);

    if (opt)
	krb5_get_init_creds_opt_free (context, opt);
    if (principal)
	krb5_free_principal(context, principal);
    free(name);
    free(password);

    put32(c, gsm_error);
    put32(c, handle);

    return 0;
}

static int
HandleOP(Sign)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, seqno;
    krb5_data token;
    gss_ctx_id_t ctx;
    gss_buffer_desc input_token, output_token;

    ret32(c, hContext);
    ret32(c, flags);
    ret32(c, seqno);
    retdata(c, token);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "sign: reference to unknown context");

    input_token.length = token.length;
    input_token.value = token.data;

    maj_stat = gss_get_mic(&min_stat, ctx, 0, &input_token,
			   &output_token);
    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_get_mic failed");

    krb5_data_free(&token);

    token.data = output_token.value;
    token.length = output_token.length;

    put32(c, 0); /* XXX fix gsm_error */
    putdata(c, token);

    gss_release_buffer(&min_stat, &output_token);

    return 0;
}

static int
HandleOP(Verify)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, seqno;
    krb5_data msg, mic;
    gss_ctx_id_t ctx;
    gss_buffer_desc msg_token, mic_token;
    gss_qop_t qop;

    ret32(c, hContext);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "verify: reference to unknown context");

    ret32(c, flags);
    ret32(c, seqno);
    retdata(c, msg);

    msg_token.length = msg.length;
    msg_token.value = msg.data;

    retdata(c, mic);

    mic_token.length = mic.length;
    mic_token.value = mic.data;

    maj_stat = gss_verify_mic(&min_stat, ctx, &msg_token,
			      &mic_token, &qop);
    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_verify_mic failed");

    krb5_data_free(&mic);
    krb5_data_free(&msg);

    put32(c, 0); /* XXX fix gsm_error */

    return 0;
}

static int
HandleOP(GetVersionAndCapabilities)
{
    int32_t cap = HAS_MONIKER;
    char name[256] = "unknown", *str;
    int ret;

    if (targetname)
	cap |= ISSERVER; /* is server */

#ifdef HAVE_UNAME
    {
	struct utsname ut;
	if (uname(&ut) == 0) {
	    snprintf(name, sizeof(name), "%s-%s-%s",
		     ut.sysname, ut.version, ut.machine);
	}
    }
#endif

    ret = asprintf(&str, "gssmask %s %s", PACKAGE_STRING, name);
    if (ret == -1)
	errx(1, "out of memory");

    put32(c, GSSMAGGOTPROTOCOL);
    put32(c, cap);
    putstring(c, str);
    free(str);

    return 0;
}

static int
HandleOP(GetTargetName)
{
    if (targetname)
	putstring(c, targetname);
    else
	putstring(c, "");
    return 0;
}

static int
HandleOP(SetLoggingSocket)
{
    int32_t portnum;
    krb5_socket_t sock;
    int ret;

    ret32(c, portnum);

    logmessage(c, __FILE__, __LINE__, 0,
	       "logging port on peer is: %d", (int)portnum);

    socket_set_port((struct sockaddr *)(&c->sa), htons(portnum));

    sock = socket(((struct sockaddr *)&c->sa)->sa_family, SOCK_STREAM, 0);
    if (sock == rk_INVALID_SOCKET)
	return 0;

    ret = connect(sock, (struct sockaddr *)&c->sa, c->salen);
    if (ret < 0) {
	logmessage(c, __FILE__, __LINE__, 0, "failed connect to log port: %s",
		   strerror(errno));
	rk_closesocket(sock);
	return 0;
    }

    if (c->logging)
	krb5_storage_free(c->logging);
    c->logging = krb5_storage_from_socket(sock);
    rk_closesocket(sock);

    krb5_store_int32(c->logging, eLogSetMoniker);
    store_string(c->logging, c->moniker);

    logmessage(c, __FILE__, __LINE__, 0, "logging turned on");

    return 0;
}


static int
HandleOP(ChangePassword)
{
    errx(1, "ChangePassword");
}

static int
HandleOP(SetPasswordSelf)
{
    errx(1, "SetPasswordSelf");
}

static int
HandleOP(Wrap)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, seqno;
    krb5_data token;
    gss_ctx_id_t ctx;
    gss_buffer_desc input_token, output_token;
    int conf_state;

    ret32(c, hContext);
    ret32(c, flags);
    ret32(c, seqno);
    retdata(c, token);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "wrap: reference to unknown context");

    input_token.length = token.length;
    input_token.value = token.data;

    maj_stat = gss_wrap(&min_stat, ctx, flags, 0, &input_token,
			&conf_state, &output_token);
    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_wrap failed");

    krb5_data_free(&token);

    token.data = output_token.value;
    token.length = output_token.length;

    put32(c, 0); /* XXX fix gsm_error */
    putdata(c, token);

    gss_release_buffer(&min_stat, &output_token);

    return 0;
}


static int
HandleOP(Unwrap)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, seqno;
    krb5_data token;
    gss_ctx_id_t ctx;
    gss_buffer_desc input_token, output_token;
    int conf_state;
    gss_qop_t qop_state;

    ret32(c, hContext);
    ret32(c, flags);
    ret32(c, seqno);
    retdata(c, token);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "unwrap: reference to unknown context");

    input_token.length = token.length;
    input_token.value = token.data;

    maj_stat = gss_unwrap(&min_stat, ctx, &input_token,
			  &output_token, &conf_state, &qop_state);

    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_unwrap failed: %d/%d", maj_stat, min_stat);

    krb5_data_free(&token);
    if (maj_stat == GSS_S_COMPLETE) {
	token.data = output_token.value;
	token.length = output_token.length;
    } else {
	token.data = NULL;
	token.length = 0;
    }
    put32(c, 0); /* XXX fix gsm_error */
    putdata(c, token);

    if (maj_stat == GSS_S_COMPLETE)
	gss_release_buffer(&min_stat, &output_token);

    return 0;
}

static int
HandleOP(Encrypt)
{
    return handleWrap(op, c);
}

static int
HandleOP(Decrypt)
{
    return handleUnwrap(op, c);
}

static int
HandleOP(ConnectLoggingService2)
{
    errx(1, "ConnectLoggingService2");
}

static int
HandleOP(GetMoniker)
{
    putstring(c, c->moniker);
    return 0;
}

static int
HandleOP(CallExtension)
{
    errx(1, "CallExtension");
}

static int
HandleOP(AcquirePKInitCreds)
{
    int32_t flags;
    krb5_data pfxdata;
    char fn[] = "FILE:/tmp/pkcs12-creds-XXXXXXX";
    int fd;

    ret32(c, flags);
    retdata(c, pfxdata);

    fd = mkstemp(fn + 5);
    if (fd < 0)
	errx(1, "mkstemp");

    net_write(fd, pfxdata.data, pfxdata.length);
    krb5_data_free(&pfxdata);
    close(fd);

    put32(c, -1); /* hResource */
    put32(c, GSMERR_NOT_SUPPORTED);
    return 0;
}

static int
HandleOP(WrapExt)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, bflags;
    krb5_data token, header, trailer;
    gss_ctx_id_t ctx;
    unsigned char *p;
    int conf_state, iov_len;
    gss_iov_buffer_desc iov[6];

    ret32(c, hContext);
    ret32(c, flags);
    ret32(c, bflags);
    retdata(c, header);
    retdata(c, token);
    retdata(c, trailer);

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "wrap: reference to unknown context");

    memset(&iov, 0, sizeof(iov));

    iov_len = sizeof(iov)/sizeof(iov[0]);

    if (bflags & WRAP_EXP_ONLY_HEADER)
	iov_len -= 2; /* skip trailer and padding, aka dce-style */

    iov[0].type = GSS_IOV_BUFFER_TYPE_HEADER | GSS_IOV_BUFFER_TYPE_FLAG_ALLOCATE;
    if (header.length != 0) {
	iov[1].type = GSS_IOV_BUFFER_TYPE_SIGN_ONLY;
	iov[1].buffer.length = header.length;
	iov[1].buffer.value = header.data;
    } else {
	iov[1].type = GSS_IOV_BUFFER_TYPE_EMPTY;
    }
    iov[2].type = GSS_IOV_BUFFER_TYPE_DATA;
    iov[2].buffer.length = token.length;
    iov[2].buffer.value = token.data;
    if (trailer.length != 0) {
	iov[3].type = GSS_IOV_BUFFER_TYPE_SIGN_ONLY;
	iov[3].buffer.length = trailer.length;
	iov[3].buffer.value = trailer.data;
    } else {
	iov[3].type = GSS_IOV_BUFFER_TYPE_EMPTY;
    }
    iov[4].type = GSS_IOV_BUFFER_TYPE_PADDING | GSS_IOV_BUFFER_TYPE_FLAG_ALLOCATE;
    iov[5].type = GSS_IOV_BUFFER_TYPE_TRAILER | GSS_IOV_BUFFER_TYPE_FLAG_ALLOCATE;

    maj_stat = gss_wrap_iov_length(&min_stat, ctx, flags, 0, &conf_state,
				   iov, iov_len);
    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_wrap_iov_length failed");

    maj_stat = gss_wrap_iov(&min_stat, ctx, flags, 0, &conf_state,
			    iov, iov_len);
    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_wrap_iov failed");

    krb5_data_free(&token);

    token.length = iov[0].buffer.length + iov[2].buffer.length + iov[4].buffer.length + iov[5].buffer.length;
    token.data = malloc(token.length);

    p = token.data;
    memcpy(p, iov[0].buffer.value, iov[0].buffer.length);
    p += iov[0].buffer.length;
    memcpy(p, iov[2].buffer.value, iov[2].buffer.length);
    p += iov[2].buffer.length;
    memcpy(p, iov[4].buffer.value, iov[4].buffer.length);
    p += iov[4].buffer.length;
    memcpy(p, iov[5].buffer.value, iov[5].buffer.length);
#ifndef __clang_analyzer__
    p += iov[5].buffer.length;
#endif

    gss_release_iov_buffer(NULL, iov, iov_len);

    put32(c, 0); /* XXX fix gsm_error */
    putdata(c, token);

    free(token.data);

    return 0;
}


static int
HandleOP(UnwrapExt)
{
    OM_uint32 maj_stat, min_stat;
    int32_t hContext, flags, bflags;
    krb5_data token, header, trailer;
    gss_ctx_id_t ctx;
    gss_iov_buffer_desc iov[3];
    int conf_state, iov_len;
    gss_qop_t qop_state;

    ret32(c, hContext);
    ret32(c, flags);
    ret32(c, bflags);
    retdata(c, header);
    retdata(c, token);
    retdata(c, trailer);

    iov_len = sizeof(iov)/sizeof(iov[0]);

    if (bflags & WRAP_EXP_ONLY_HEADER)
	iov_len -= 1; /* skip trailer and padding, aka dce-style */

    ctx = find_handle(c->handles, hContext, handle_context);
    if (ctx == NULL)
	errx(1, "unwrap: reference to unknown context");

    if (header.length != 0) {
	iov[0].type = GSS_IOV_BUFFER_TYPE_SIGN_ONLY;
	iov[0].buffer.length = header.length;
	iov[0].buffer.value = header.data;
    } else {
	iov[0].type = GSS_IOV_BUFFER_TYPE_EMPTY;
    }
    iov[1].type = GSS_IOV_BUFFER_TYPE_DATA;
    iov[1].buffer.length = token.length;
    iov[1].buffer.value = token.data;

    if (trailer.length != 0) {
	iov[2].type = GSS_IOV_BUFFER_TYPE_SIGN_ONLY;
	iov[2].buffer.length = trailer.length;
	iov[2].buffer.value = trailer.data;
    } else {
	iov[2].type = GSS_IOV_BUFFER_TYPE_EMPTY;
    }

    maj_stat = gss_unwrap_iov(&min_stat, ctx, &conf_state, &qop_state,
			      iov, iov_len);

    if (maj_stat != GSS_S_COMPLETE)
	errx(1, "gss_unwrap failed: %d/%d", maj_stat, min_stat);

    if (maj_stat == GSS_S_COMPLETE) {
	token.data = iov[1].buffer.value;
	token.length = iov[1].buffer.length;
    } else {
	token.data = NULL;
	token.length = 0;
    }
    put32(c, 0); /* XXX fix gsm_error */
    putdata(c, token);

    return 0;
}

/*
 *
 */

struct handler {
    enum gssMaggotOp op;
    const char *name;
    int (*func)(enum gssMaggotOp, struct client *);
};

#define S(a) { e##a, #a, handle##a }

struct handler handlers[] = {
    S(GetVersionInfo),
    S(GoodBye),
    S(InitContext),
    S(AcceptContext),
    S(ToastResource),
    S(AcquireCreds),
    S(Encrypt),
    S(Decrypt),
    S(Sign),
    S(Verify),
    S(GetVersionAndCapabilities),
    S(GetTargetName),
    S(SetLoggingSocket),
    S(ChangePassword),
    S(SetPasswordSelf),
    S(Wrap),
    S(Unwrap),
    S(ConnectLoggingService2),
    S(GetMoniker),
    S(CallExtension),
    S(AcquirePKInitCreds),
    S(WrapExt),
    S(UnwrapExt),
};

#undef S

/*
 *
 */

static struct handler *
find_op(int32_t op)
{
    int i;

    for (i = 0; i < sizeof(handlers)/sizeof(handlers[0]); i++)
	if (handlers[i].op == op)
	    return &handlers[i];
    return NULL;
}

static struct client *
create_client(krb5_socket_t sock, int port, const char *moniker)
{
    struct client *c;
    int ret;

    c = ecalloc(1, sizeof(*c));

    if (moniker) {
	c->moniker = estrdup(moniker);
    } else {
	char hostname[MAXHOSTNAMELEN];
	gethostname(hostname, sizeof(hostname));
	ret = asprintf(&c->moniker, "gssmask: %s:%d", hostname, port);
	if (ret == -1)
	    c->moniker = NULL;
    }

    if (!c->moniker)
	errx(1, "out of memory");

    {
	c->salen = sizeof(c->sa);
	getpeername(sock, (struct sockaddr *)&c->sa, &c->salen);

	getnameinfo((struct sockaddr *)&c->sa, c->salen,
		    c->servername, sizeof(c->servername),
		    NULL, 0, NI_NUMERICHOST);
    }

    c->sock = krb5_storage_from_socket(sock);
    if (c->sock == NULL)
	errx(1, "krb5_storage_from_socket");

    rk_closesocket(sock);

    return c;
}

static void
free_client(struct client *c)
{
    while(c->handles)
	del_handle(&c->handles, c->handles->idx);

    free(c->moniker);
    krb5_storage_free(c->sock);
    if (c->logging)
	krb5_storage_free(c->logging);
    free(c);
}


static void *
handleServer(void *ptr)
{
    struct handler *handler;
    struct client *c;
    int32_t op;

    c = (struct client *)ptr;


    while(1) {
	ret32(c, op);

	handler = find_op(op);
	if (handler == NULL) {
	    logmessage(c, __FILE__, __LINE__, 0,
		       "op %d not supported", (int)op);
	    exit(1);
	}

	logmessage(c, __FILE__, __LINE__, 0,
		   "---> Got op %s from server %s",
		   handler->name, c->servername);

	if ((handler->func)(handler->op, c))
	    break;
    }

    return NULL;
}


static char *port_str;
static int version_flag;
static int help_flag;
static char *logfile_str;
static char *moniker_str;

static int port = 4711;

struct getargs args[] = {
    { "spn",	0,   arg_string,	&targetname,	"This host's SPN",
      "service/host@REALM" },
    { "port",	'p', arg_string,	&port_str,	"Use this port",
      "number-of-service" },
    { "logfile", 0,  arg_string,	&logfile_str,	"logfile",
      "number-of-service" },
    { "moniker", 0,  arg_string,	&moniker_str,	"nickname",
      "name" },
    { "version", 0,  arg_flag,		&version_flag,	"Print version",
      NULL },
    { "help",	 0,  arg_flag,		&help_flag,	NULL,
      NULL }
};

static void
usage(int ret)
{
    arg_printusage (args,
		    sizeof(args) / sizeof(args[0]),
		    NULL,
		    "");
    exit (ret);
}

int
main(int argc, char **argv)
{
    int optidx	= 0;
    krb5_error_code ret;

    setprogname (argv[0]);

    if (getarg (args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
	usage (1);

    if (help_flag)
	usage (0);

    if (version_flag) {
	print_version (NULL);
	return 0;
    }

    if (optidx != argc)
	usage (1);

    if (port_str) {
	char *ptr;

	port = strtol (port_str, &ptr, 10);
	if (port == 0 && ptr == port_str)
	    errx (1, "Bad port `%s'", port_str);
    }

    ret = krb5_init_context(&context);
    if (ret)
	errx(1, "Error initializing kerberos: %d", ret);

    {
	const char *lf = logfile_str;
	if (lf == NULL)
	    lf = "/dev/tty";

	logfile = fopen(lf, "w");
	if (logfile == NULL)
	    err(1, "error opening %s", lf);
    }

    mini_inetd(htons(port), NULL);
    fprintf(logfile, "connected\n");

    {
	struct client *c;

	c = create_client(0, port, moniker_str);
	/* close(0); */

	handleServer(c);

	free_client(c);
    }

    krb5_free_context(context);

    return 0;
}
