/*
 * Amanda, The Advanced Maryland Automatic Network Disk Archiver
 * Copyright (c) 1999 University of Maryland
 * All Rights Reserved.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of U.M. not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  U.M. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 *
 * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors: the Amanda Development Team.  Its members are listed in a
 * file named AUTHORS, in the root directory of this distribution.
 */

/*
 * $Id: krb5-security.c,v 1.22 2006/06/16 10:55:05 martinea Exp $
 *
 * krb5-security.c - kerberos V5 security module
 *
 * XXX still need to check for initial keyword on connect so we can skip
 * over shell garbage and other stuff that krb5 might want to spew out.
 */

#include "amanda.h"
#include "amutil.h"
#include "event.h"
#include "packet.h"
#include "security.h"
#include "security-util.h"
#include "stream.h"
#include "sockaddr-util.h"

#ifdef KRB5_HEIMDAL_INCLUDES
#include "com_err.h"
#endif

#define BROKEN_MEMORY_CCACHE

#ifdef BROKEN_MEMORY_CCACHE
/*
 * If you don't have atexit() or on_exit(), you could just consider
 * making atexit() empty and clean up your ticket files some other
 * way
 */
#ifndef HAVE_ATEXIT
#ifdef HAVE_ON_EXIT
#define atexit(func)    on_exit(func, 0)
#else
#error "You don't have atexit() or on_exit()"
#endif  /* HAVE_ON_EXIT */
#endif  /* ! HAVE_ATEXIT */
#endif
#ifndef KRB5_HEIMDAL_INCLUDES
#include <gssapi/gssapi_generic.h>
#else
#include <gssapi/gssapi.h>
#endif
#include <krb5.h>

#ifndef KRB5_ENV_CCNAME
#define KRB5_ENV_CCNAME "KRB5CCNAME"
#endif

/*
 * consider undefining when kdestroy() is fixed.  The current version does
 * not work under krb5-1.2.4 in rh7.3, perhaps others.
 */
#define KDESTROY_VIA_UNLINK     1

/*
 * Where the keytab lives, if defined.  Otherwise it expects something in the
 * config file.
 */
/* #define AMANDA_KEYTAB        "/.amanda-v5-keytab" */

/*
 * The name of the principal we authenticate with, if defined.  Otherwise
 * it expects something in the config file.
 */
/* #define      AMANDA_PRINCIPAL        "service/amanda" */

/*
 * The lifetime of our tickets in seconds.  This may or may not need to be
 * configurable.
 */
#define AMANDA_TKT_LIFETIME     (12*60*60)


/*
 * The name of the service in /etc/services.  This probably shouldn't be
 * configurable.
 */
#define AMANDA_KRB5_SERVICE_NAME        "k5amanda"

/*
 * The default port to use if above entry in /etc/services doesn't exist
 */
#define AMANDA_KRB5_DEFAULT_PORT        10082

/*
 * The timeout in seconds for each step of the GSS negotiation phase
 */
#define GSS_TIMEOUT                     30

/*
 * This is the tcp stream buffer size
 */
#define KRB5_STREAM_BUFSIZE     (32768 * 2)

/*
 * This is the max number of outgoing connections we can have at once.
 * planner/amcheck/etc will open a bunch of connections as it tries
 * to contact everything.  We need to limit this to avoid blowing
 * the max number of open file descriptors a process can have.
 */
#define AMANDA_KRB5_MAXCONN     40


/*
 * Number of seconds krb5 has to start up
 */
#define	CONNECT_TIMEOUT	20

/*
 * Cache the local hostname
 */
static char myhostname[MAX_HOSTNAME_LENGTH+1];


/*
 * Interface functions
 */
static void krb5_accept(const struct security_driver *,
    char *(*)(char *, void *),
    int, int,
    void (*)(security_handle_t *, pkt_t *),
    void *);
static void krb5_connect(const char *,
    char *(*)(char *, void *), 
    void (*)(void *, security_handle_t *, security_status_t), void *, void *);

static void krb5_init(void);
#ifdef BROKEN_MEMORY_CCACHE
static void cleanup(void);
#endif
static const char *get_tgt(char *keytab_name, char *principal_name);
static int	   gss_server(struct tcp_conn *);
static int	   gss_client(struct sec_handle *);
static const char *gss_error(OM_uint32, OM_uint32);
static char       *krb5_checkuser(char *host, char *name, char *realm);

static int k5_encrypt(void *cookie, void *buf, ssize_t buflen,
		      void **encbuf, ssize_t *encbuflen);
static int k5_decrypt(void *cookie, void *buf, ssize_t buflen,
		      void **encbuf, ssize_t *encbuflen);

static ssize_t krb5_tcpm_recv_token(struct tcp_conn *rc, int *handle,
				    char **errmsg, char **buf, ssize_t *size,
				    int timeout);
/*
 * This is our interface to the outside world.
 */
const security_driver_t krb5_security_driver = {
    "KRB5",
    krb5_connect,
    krb5_accept,
    sec_get_authenticated_peer_name_hostname,
    sec_close,
    stream_sendpkt,
    stream_recvpkt,
    stream_recvpkt_cancel,
    tcpma_stream_server,
    tcpma_stream_accept,
    tcpma_stream_client,
    tcpma_stream_close,
    tcpma_stream_close_async,
    sec_stream_auth,
    sec_stream_id,
    tcpm_stream_write,
    tcpm_stream_write_async,
    tcpm_stream_read,
    tcpm_stream_read_sync,
    tcpm_stream_read_to_shm_ring,
    tcpm_stream_read_cancel,
    tcpm_stream_pause,
    tcpm_stream_resume,
    tcpm_close_connection,
    k5_encrypt,
    k5_decrypt,
    generic_data_write,
    generic_data_write_non_blocking,
    generic_data_read
};

static int newhandle = 1;

/*
 * Local functions
 */
static int runkrb5(struct sec_handle *);

char *keytab_name;
char *principal_name;

/*
 * krb5 version of a security handle allocator.  Logically sets
 * up a network "connection".
 */
static void
krb5_connect(
    const char *hostname,
    char *	(*conf_fn)(char *, void *),
    void	(*fn)(void *, security_handle_t *, security_status_t),
    void *	arg,
    void *	datap)
{
    struct sec_handle *rh;
    int result;
    char *canonname;

    assert(fn != NULL);
    assert(hostname != NULL);

    auth_debug(1, "krb5: krb5_connect: %s\n", hostname);

    krb5_init();

    rh = g_malloc(sizeof(*rh));
    security_handleinit(&rh->sech, &krb5_security_driver);
    rh->dle_hostname = g_strdup(hostname);
    rh->hostname = NULL;
    rh->rs = NULL;
    rh->ev_timeout = NULL;
    rh->rc = NULL;

    result = resolve_hostname(hostname, 0, NULL, &canonname);
    if(result != 0) {
	dbprintf(_("resolve_hostname(%s): %s\n"), hostname, gai_strerror(result));
	security_seterror(&rh->sech, _("resolve_hostname(%s): %s"), hostname,
			  gai_strerror(result));
	(*fn)(arg, &rh->sech, S_ERROR);
	return;
    }
    if (canonname == NULL) {
	dbprintf(_("resolve_hostname(%s) did not return a canonical name\n"), hostname);
	security_seterror(&rh->sech,
	        _("resolve_hostname(%s) did not return a canonical name"), hostname);
	(*fn)(arg, &rh->sech, S_ERROR);
       return;
    }

    rh->hostname = canonname;        /* will be replaced */
    canonname = NULL; /* steal reference */
    rh->rs = tcpma_stream_client(rh, newhandle++);
    if (rh->rc == NULL)
	goto error;

    rh->rc->conf_fn = conf_fn;
    rh->rc->datap = datap;
    rh->rc->recv_security_ok = NULL;
    rh->rc->prefix_packet = NULL;
    rh->rc->need_priv_port = 0;

    if (rh->rs == NULL)
	goto error;

    amfree(rh->hostname);
    rh->hostname = g_strdup(rh->rs->rc->hostname);

#ifdef AMANDA_KEYTAB
    keytab_name = AMANDA_KEYTAB;
#else
    if(conf_fn) {
        keytab_name = conf_fn("krb5keytab", datap);
    }
#endif
#ifdef AMANDA_PRINCIPAL
    principal_name = AMANDA_PRINCIPAL;
#else
    if(conf_fn) {
        principal_name = conf_fn("krb5principal", datap);
    }
#endif

    /*
     * We need to open a new connection.
     *
     * XXX need to eventually limit number of outgoing connections here.
     */
    if(rh->rc->read == -1) {
	if (runkrb5(rh) < 0)
	    goto error;
	rh->rc->refcnt++;
    }

    /*
     * The socket will be opened async so hosts that are down won't
     * block everything.  We need to register a write event
     * so we will know when the socket comes alive.
     *
     * Overload rh->rs->ev_read to provide a write event handle.
     * We also register a timeout.
     */
    g_mutex_lock(security_mutex);
    rh->fn.connect = fn;
    rh->arg = arg;
    rh->rs->rc->ev_write = event_create((event_id_t)(rh->rs->rc->write),
	EV_WRITEFD, sec_connect_callback, rh);
    rh->ev_timeout = event_create(CONNECT_TIMEOUT, EV_TIME,
	sec_connect_timeout, rh);
    event_activate(rh->rs->rc->ev_write);
    event_activate(rh->ev_timeout);
    g_mutex_unlock(security_mutex);

    amfree(canonname);
    return;

error:
    amfree(canonname);
    (*fn)(arg, &rh->sech, S_ERROR);
}

/*

 * Setup to handle new incoming connections
 */
static void
krb5_accept(
    const struct security_driver *driver,
    char       *(*conf_fn)(char *, void *),
    int		in,
    int		out,
    void	(*fn)(security_handle_t *, pkt_t *),
    void       *datap)
{
    sockaddr_union sin;
    socklen_t_equiv len;
    struct tcp_conn *rc;
    char hostname[NI_MAXHOST];
    int result;
    char *errmsg = NULL;
    struct passwd *pw;

    krb5_init();

    len = sizeof(sin);
    if (getpeername(in, (struct sockaddr *)&sin, &len) < 0) {
	dbprintf(_("getpeername returned: %s\n"),
		  strerror(errno));
	return;

    }
    if ((result = getnameinfo((struct sockaddr *)&sin, len,
			      hostname, NI_MAXHOST, NULL, 0, 0) != 0)) {
	dbprintf(_("getnameinfo failed: %s\n"),
		  gai_strerror(result));
	return;
    }
    if (check_name_give_sockaddr(hostname,
				 (struct sockaddr *)&sin, &errmsg) < 0) {
	dbprintf(_("check_name_give_sockaddr(%s): %s\n"),
		  hostname, errmsg);
	amfree(errmsg);
	return;
    }


    rc = sec_tcp_conn_get(NULL, hostname, 0);
    rc->conf_fn = conf_fn;
    rc->datap = datap;
    rc->recv_security_ok = NULL;
    rc->prefix_packet = NULL;
    rc->need_priv_port = 0;
    copy_sockaddr(&rc->peer, &sin);
    rc->read = in;
    rc->write = out;
    rc->driver = driver;
    if (gss_server(rc) < 0)
	error("gss_server failed: %s\n", rc->errmsg);
    rc->accept_fn = fn;
    sec_tcp_conn_read(rc);

    /* totally drop privileges at this point
     *(making the userid equal to the dumpuser)
     */
    pw = getpwnam(CLIENT_LOGIN);
    if (setreuid(pw->pw_uid, pw->pw_uid) == -1) {
	g_critical("setreuid failed: %s", strerror(errno));
	exit(1);
    }
}

/*
 * Forks a krb5 to the host listed in rc->hostname
 * Returns negative on error, with an errmsg in rc->errmsg.
 */
static int
runkrb5(
    struct sec_handle *	rh)
{
    int			server_socket;
    in_port_t		my_port, port;
    struct tcp_conn *	rc = rh->rc;
    const char *err;
    char *stream_msg = NULL;

    port = find_port_for_service(AMANDA_KRB5_SERVICE_NAME, "tcp");

    if ((err = get_tgt(keytab_name, principal_name)) != NULL) {
        security_seterror(&rh->sech, "%s: could not get TGT: %s",
            rc->hostname, err);
        return -1;
    }

    server_socket = stream_client_privileged(NULL, rc->hostname,
				     port,
				     STREAM_BUFSIZE,
				     STREAM_BUFSIZE,
				     &my_port,
				     0, &stream_msg);

    if (stream_msg) {
	security_seterror(&rh->sech, "%s", stream_msg);
	g_free(stream_msg);
	return -1;
    }
    if (server_socket < 0) {
	security_seterror(&rh->sech, "%s", strerror(errno));
	return -1;
    }

    rc->read = rc->write = server_socket;

    if (gss_client(rh) < 0) {
	return -1;
    }


    return 0;
}



/*

 * Negotiate a krb5 gss context from the client end.
 */
static int
gss_client(
    struct sec_handle *rh)
{
    struct sec_stream *rs = rh->rs;
    struct tcp_conn *rc = rs->rc;
    gss_buffer_desc send_tok, recv_tok, AA;
    gss_OID doid;
    OM_uint32 maj_stat, min_stat;
    unsigned int ret_flags;
    int rval = -1;
    int rvalue;
    gss_name_t gss_name;
    char *errmsg = NULL;

    auth_debug(1, "gss_client\n");

    send_tok.value = g_strjoin(NULL, "host/", rs->rc->hostname, NULL);
    send_tok.length = strlen(send_tok.value) + 1;
    maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
	&gss_name);
    if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
	security_seterror(&rh->sech, _("can't import name %s: %s"),
	    (char *)send_tok.value, gss_error(maj_stat, min_stat));
	amfree(send_tok.value);
	return (-1);
    }
    amfree(send_tok.value);
    rc->gss_context = GSS_C_NO_CONTEXT;
    maj_stat = gss_display_name(&min_stat, gss_name, &AA, &doid);
    dbprintf(_("gss_name %s\n"), (char *)AA.value);

    /*
     * Perform the context-establishement loop.
     *
     * Every generated token is stored in send_tok which is then
     * transmitted to the server; every received token is stored in
     * recv_tok (empty on the first pass) to be processed by
     * the next call to gss_init_sec_context.
     * 
     * GSS-API guarantees that send_tok's length will be non-zero
     * if and only if the server is expecting another token from us,
     * and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if
     * and only if the server has another token to send us.
     */

    recv_tok.value = NULL;
    for (recv_tok.length = 0;;) {
	min_stat = 0;
	maj_stat = gss_init_sec_context(&min_stat,
	    GSS_C_NO_CREDENTIAL,
	    &rc->gss_context,
	    gss_name,
	    GSS_C_NULL_OID,
	    (OM_uint32)GSS_C_MUTUAL_FLAG|GSS_C_REPLAY_FLAG,
	    0, NULL,	/* no channel bindings */
	    (recv_tok.length == 0 ? GSS_C_NO_BUFFER : &recv_tok),
	    NULL,	/* ignore mech type */
	    &send_tok,
	    &ret_flags,
	    NULL);	/* ignore time_rec */

	if (recv_tok.length != 0) {
	    amfree(recv_tok.value);
	    recv_tok.length = 0;
	}
	if (maj_stat != (OM_uint32)GSS_S_COMPLETE && maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED) {
	    security_seterror(&rh->sech,
		_("error getting gss context: %s %s"),
		gss_error(maj_stat, min_stat), (char *)send_tok.value);
	    goto done;
	}

	/*
	 * Send back the response
	 */
	if (send_tok.length != 0 && tcpm_send_token(rc, rs->handle, &errmsg, send_tok.value, send_tok.length) < 0) {
	    security_seterror(&rh->sech, "%s", rc->errmsg);
	    gss_release_buffer(&min_stat, &send_tok);
	    goto done;
	}
	gss_release_buffer(&min_stat, &send_tok);

	/*
	 * If we need to continue, then register for more packets
	 */
	if (maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED)
	    break;

        rvalue = krb5_tcpm_recv_token(rc, &rc->handle,
				 &rc->errmsg,
				 (void *)&recv_tok.value,
				 (ssize_t *)&recv_tok.length, 60);
	if (rvalue <= 0) {
	    if (rvalue < 0)
		security_seterror(&rh->sech,
		    _("recv error in gss loop: %s"), rc->errmsg);
	    else
		security_seterror(&rh->sech, _("EOF in gss loop"));
	    goto done;
	}
    }

    rval = 0;
    rc->auth = 1;
done:
    gss_release_name(&min_stat, &gss_name);
    return (rval);
}

/*
 * Negotiate a krb5 gss context from the server end.
 */
static int
gss_server(
    struct tcp_conn *rc)
{
    OM_uint32 maj_stat, min_stat, ret_flags;
    gss_buffer_desc send_tok, recv_tok, AA;
    gss_OID doid;
    gss_name_t gss_name;
    gss_cred_id_t gss_creds;
    char *p, *realm, *msg;
    int rval = -1;
    int rvalue;
    char errbuf[256];
    char *errmsg = NULL;

    auth_debug(1, "gss_server\n");

    assert(rc != NULL);

    /*
     * We need to be root while in gss_acquire_cred() to read the host key
     * out of the default keytab.  We also need to be root in
     * gss_accept_context() thanks to the replay cache code.
     */
    if (!set_root_privs(0)) {
	g_snprintf(errbuf, sizeof(errbuf),
	    _("can't take root privileges to read krb5 host key: %s"), strerror(errno));
	goto out;
    }

    rc->gss_context = GSS_C_NO_CONTEXT;
    send_tok.value = g_strjoin(NULL, "host/", myhostname, NULL);
    send_tok.length = strlen(send_tok.value) + 1;
    for (p = send_tok.value; *p != '\0'; p++) {
	if (isupper((int)*p))
	    *p = tolower(*p);
    }
    maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
	&gss_name);
    if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
	set_root_privs(0);
	g_snprintf(errbuf, sizeof(errbuf),
	    _("can't import name %s: %s"), (char *)send_tok.value,
	    gss_error(maj_stat, min_stat));
	amfree(send_tok.value);
	goto out;
    }
    amfree(send_tok.value);

    maj_stat = gss_display_name(&min_stat, gss_name, &AA, &doid);
    dbprintf(_("gss_name %s\n"), (char *)AA.value);
    maj_stat = gss_acquire_cred(&min_stat, gss_name, 0,
	GSS_C_NULL_OID_SET, GSS_C_ACCEPT, &gss_creds, NULL, NULL);
    if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
	g_snprintf(errbuf, sizeof(errbuf),
	    _("can't acquire creds for host key host/%s: %s"), myhostname,
	    gss_error(maj_stat, min_stat));
	gss_release_name(&min_stat, &gss_name);
	set_root_privs(0);
	goto out;
    }
    gss_release_name(&min_stat, &gss_name);

    for (recv_tok.length = 0;;) {
	recv_tok.value = NULL;
        rvalue = krb5_tcpm_recv_token(rc, &rc->handle,
				 &rc->errmsg,
				 /* (void *) is to avoid type-punning warning */
				 (char **)(void *)&recv_tok.value,
				 (ssize_t *)&recv_tok.length, 60);
	if (rvalue <= 0) {
	    if (rvalue < 0) {
		g_snprintf(errbuf, sizeof(errbuf),
		    _("recv error in gss loop: %s"), rc->errmsg);
		amfree(rc->errmsg);
	    } else
		g_snprintf(errbuf, sizeof(errbuf), _("EOF in gss loop"));
	    goto out;
	}

	maj_stat = gss_accept_sec_context(&min_stat, &rc->gss_context,
	    gss_creds, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS,
	    &gss_name, &doid, &send_tok, &ret_flags, NULL, NULL);

	if (maj_stat != (OM_uint32)GSS_S_COMPLETE &&
	    maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED) {
	    g_snprintf(errbuf, sizeof(errbuf),
		_("error accepting context: %s"), gss_error(maj_stat, min_stat));
	    amfree(recv_tok.value);
	    goto out;
	}
	amfree(recv_tok.value);

	if (send_tok.length != 0 && tcpm_send_token(rc, 0, &errmsg, send_tok.value, send_tok.length) < 0) {
	    strncpy(errbuf, rc->errmsg, sizeof(errbuf) - 1);
	    errbuf[sizeof(errbuf) - 1] = '\0';
	    amfree(rc->errmsg);
	    gss_release_buffer(&min_stat, &send_tok);
	    goto out;
	}
	gss_release_buffer(&min_stat, &send_tok);


	/*
	 * If we need to get more from the client, then register for
	 * more packets.
	 */
	if (maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED)
	    break;
    }

    maj_stat = gss_display_name(&min_stat, gss_name, &send_tok, &doid);
    if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
	g_snprintf(errbuf, sizeof(errbuf),
	    _("can't display gss name: %s"), gss_error(maj_stat, min_stat));
	gss_release_name(&min_stat, &gss_name);
	goto out;
    }
    gss_release_name(&min_stat, &gss_name);

    /* get rid of the realm */
    if ((p = strchr(send_tok.value, '@')) == NULL) {
	g_snprintf(errbuf, sizeof(errbuf),
	    _("malformed gss name: %s"), (char *)send_tok.value);
	amfree(send_tok.value);
	goto out;
    }
    *p = '\0';
    realm = ++p;

    /* 
     * If the principal doesn't match, complain
     */
    if ((msg = krb5_checkuser(rc->hostname, send_tok.value, realm)) != NULL) {
	g_snprintf(errbuf, sizeof(errbuf),
	    _("access not allowed from %s: %s"), (char *)send_tok.value, msg);
	amfree(send_tok.value);
	goto out;
    }
    amfree(send_tok.value);

    rval = 0;
out:
    set_root_privs(0);
    if (rval != 0) {
	rc->errmsg = g_strdup(errbuf);
    } else {
	rc->auth = 1;
    }
    auth_debug(1, _("gss_server returning %d\n"), rval);
    return (rval);
}

/*
 * Setup some things about krb5.  This should only be called once.
 */
static void
krb5_init(void)
{
    static int beenhere = 0;
    char *p;
    char *myfqhostname=NULL;

    if (beenhere)
	return;
    beenhere = 1;

#ifndef BROKEN_MEMORY_CCACHE
    putenv(g_strdup(KRB5_ENV_CCNAME"=MEMORY:amanda_ccache"));
#else
    /*
     * MEMORY ccaches seem buggy and cause a lot of internal heap
     * corruption.  malloc has been known to core dump.  This behavior
     * has been witnessed in Cygnus' kerbnet 1.2, MIT's krb V 1.0.5 and
     * MIT's krb V -current as of 3/17/1999.
     *
     * We just use a lame ccache scheme with a uid suffix.
     */
    atexit(cleanup);
    {
	char *ccache = g_strdup_printf(
		KRB5_ENV_CCNAME"=FILE:/tmp/amanda_ccache.%ld.%ld",
		(long)geteuid(), (long)getpid());
	putenv(ccache);
    }
#endif

    gethostname(myhostname, sizeof(myhostname) - 1);
    myhostname[sizeof(myhostname) - 1] = '\0';

    /*
     * In case it isn't fully qualified, do a DNS lookup.  Ignore
     * any errors (this is best-effort).
     */
    if (resolve_hostname(myhostname, SOCK_STREAM, NULL, &myfqhostname) == 0
	&& myfqhostname != NULL) {
	strncpy(myhostname, myfqhostname, sizeof(myhostname)-1);
	myhostname[sizeof(myhostname)-1] = '\0';
	amfree(myfqhostname);
    }

    /*
     * Lowercase the results.  We assume all host/ principals will be
     * lowercased.
     */
    for (p = myhostname; *p != '\0'; p++) {
	if (isupper((int)*p))
	    *p = tolower(*p);
    }
}

#ifdef BROKEN_MEMORY_CCACHE
static void
cleanup(void)
{
#ifdef KDESTROY_VIA_UNLINK
    char ccache[64];
    g_snprintf(ccache, sizeof(ccache), "/tmp/amanda_ccache.%ld.%ld",
        (long)geteuid(), (long)getpid());
    unlink(ccache);
#else
    kdestroy();
#endif
}
#endif

/*
 * Get a ticket granting ticket and stuff it in the cache
 */
static const char *
get_tgt(
    char *	keytab_name,
    char *	principal_name)
{
    krb5_context context;
    krb5_error_code ret;
    krb5_principal client = NULL, server = NULL;
    krb5_creds creds;
    krb5_keytab keytab;
    krb5_ccache ccache;
    krb5_timestamp now;
#ifdef KRB5_HEIMDAL_INCLUDES
    krb5_data tgtname = { KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME };
#else
    krb5_data tgtname = { 0, KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME };
#endif
    static char *error = NULL;

    if (error != NULL) {
	amfree(error);
	error = NULL;
    }
    if ((ret = krb5_init_context(&context)) != 0) {
	error = g_strdup_printf(_("error initializing krb5 context: %s"),
	    error_message(ret));
	return (error);
    }

    /*krb5_init_ets(context);*/

    if(!keytab_name) {
        error = g_strdup(_("error  -- no krb5 keytab defined"));
        return(error);
    }

    if(!principal_name) {
        error = g_strdup(_("error  -- no krb5 principal defined"));
        return(error);
    }

    /*
     * Resolve keytab file into a keytab object
     */
    if ((ret = krb5_kt_resolve(context, keytab_name, &keytab)) != 0) {
	error = g_strdup_printf(_("error resolving keytab %s: %s"), keytab_name,
	    error_message(ret));
	return (error);
    }

    /*
     * Resolve the amanda service held in the keytab into a principal
     * object
     */
    ret = krb5_parse_name(context, principal_name, &client);
    if (ret != 0) {
	error = g_strdup_printf(_("error parsing %s: %s"), principal_name,
	    error_message(ret));
	return (error);
    }

#ifdef KRB5_HEIMDAL_INCLUDES
    ret = krb5_build_principal_ext(context, &server,
        krb5_realm_length(*krb5_princ_realm(context, client)),
        krb5_realm_data(*krb5_princ_realm(context, client)),
        tgtname.length, tgtname.data,
        krb5_realm_length(*krb5_princ_realm(context, client)),
        krb5_realm_data(*krb5_princ_realm(context, client)),
        0);
#else
    ret = krb5_build_principal_ext(context, &server,
	krb5_princ_realm(context, client)->length,
	krb5_princ_realm(context, client)->data,
	tgtname.length, tgtname.data,
	krb5_princ_realm(context, client)->length,
	krb5_princ_realm(context, client)->data,
	0);
#endif
    if (ret != 0) {
	error = g_strdup_printf(_("error while building server name: %s"),
	    error_message(ret));
	return (error);
    }

    ret = krb5_timeofday(context, &now);
    if (ret != 0) {
	error = g_strdup_printf(_("error getting time of day: %s"), error_message(ret));
	return (error);
    }

    memset(&creds, 0, sizeof(creds));
    creds.times.starttime = 0;
    creds.times.endtime = now + AMANDA_TKT_LIFETIME;

    creds.client = client;
    creds.server = server;

    /*
     * Get a ticket for the service, using the keytab
     */
    ret = krb5_get_in_tkt_with_keytab(context, 0, NULL, NULL, NULL,
	keytab, 0, &creds, 0);

    if (ret != 0) {
	error = g_strdup_printf(_("error getting ticket for %s: %s"),
	    principal_name, error_message(ret));
	goto cleanup2;
    }

    if ((ret = krb5_cc_default(context, &ccache)) != 0) {
	error = g_strdup_printf(_("error initializing ccache: %s"), error_message(ret));
	goto cleanup;
    }
    if ((ret = krb5_cc_initialize(context, ccache, client)) != 0) {
	error = g_strdup_printf(_("error initializing ccache: %s"), error_message(ret));
	goto cleanup;
    }
    if ((ret = krb5_cc_store_cred(context, ccache, &creds)) != 0) {
	error = g_strdup_printf(_("error storing creds in ccache: %s"), 
	    error_message(ret));
	/* FALLTHROUGH */
    }
    krb5_cc_close(context, ccache);
cleanup:
    krb5_free_cred_contents(context, &creds);
cleanup2:
#if 0
    krb5_free_principal(context, client);
    krb5_free_principal(context, server);
#endif
    krb5_free_context(context);
    return (error);
}

#ifndef KDESTROY_VIA_UNLINK
/*
 * get rid of tickets
 */
static void
kdestroy(void)
{
    krb5_context context;
    krb5_ccache ccache;

    if ((krb5_init_context(&context)) != 0) {
	return;
    }
    if ((krb5_cc_default(context, &ccache)) != 0) {
	goto cleanup;
    }

    krb5_cc_destroy(context, ccache);
    krb5_cc_close(context, ccache);

cleanup:
     krb5_free_context(context);
     return;
}
#endif

/*
 * Formats an error from the gss api
 */
static const char *
gss_error(
    OM_uint32	major,
    OM_uint32	minor)
{
    static gss_buffer_desc msg;
    OM_uint32 min_stat, msg_ctx;

    if (msg.length > 0)
	gss_release_buffer(&min_stat, &msg);

    msg_ctx = 0;
    if (major == GSS_S_FAILURE)
	gss_display_status(&min_stat, minor, GSS_C_MECH_CODE, GSS_C_NULL_OID,
	    &msg_ctx, &msg);
    else
	gss_display_status(&min_stat, major, GSS_C_GSS_CODE, GSS_C_NULL_OID,
	    &msg_ctx, &msg);
    return ((const char *)msg.value);
}

static int
k5_encrypt(
    void *cookie,
    void *buf,
    ssize_t buflen,
    void **encbuf,
    ssize_t *encbuflen)
{
    struct tcp_conn *rc = cookie;
    gss_buffer_desc dectok;
    gss_buffer_desc enctok;
    OM_uint32 maj_stat, min_stat;
    int conf_state;

    if (rc->conf_fn && rc->conf_fn("kencrypt", rc->datap)) {
	auth_debug(1, _("krb5: k5_encrypt: enter %p\n"), rc);

	dectok.length = buflen;
	dectok.value  = buf;    

	if (rc->auth == 1) {
	    assert(rc->gss_context != GSS_C_NO_CONTEXT);
	    maj_stat = gss_seal(&min_stat, rc->gss_context, 1,
			        GSS_C_QOP_DEFAULT, &dectok, &conf_state,
				&enctok);
	    if (maj_stat != (OM_uint32)GSS_S_COMPLETE || conf_state == 0) {
		auth_debug(1, _("krb5 encrypt error to %s: %s\n"),
			   rc->hostname, gss_error(maj_stat, min_stat));
		return (-1);
	    }
	    auth_debug(1, _("krb5: k5_encrypt: give %zu bytes\n"),
		       enctok.length);
	    *encbuf = enctok.value;
	    *encbuflen = enctok.length;
	} else {
	    *encbuf = buf;
	    *encbuflen = buflen;
	}
	auth_debug(1, _("krb5: k5_encrypt: exit\n"));
    }
    return (0);
}


static int
k5_decrypt(
    void *cookie,
    void *buf,
    ssize_t buflen,
    void **decbuf,
    ssize_t *decbuflen)
{
    struct tcp_conn *rc = cookie;
    gss_buffer_desc enctok;
    gss_buffer_desc dectok;
    OM_uint32 maj_stat, min_stat;
    int conf_state, qop_state;

    if (rc->conf_fn && rc->conf_fn("kencrypt", rc->datap)) {
	auth_debug(1, _("krb5: k5_decrypt: enter\n"));
	if (rc->auth == 1) {
	    enctok.length = buflen;
	    enctok.value  = buf;    

	    auth_debug(1, _("krb5: k5_decrypt: decrypting %zu bytes\n"), enctok.length);

	    assert(rc->gss_context != GSS_C_NO_CONTEXT);
	    maj_stat = gss_unseal(&min_stat, rc->gss_context, &enctok, &dectok,
			      &conf_state, &qop_state);
	    if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
		auth_debug(1, _("krb5 decrypt error from %s: %s\n"),
			   rc->hostname, gss_error(maj_stat, min_stat));
		return (-1);
	    }
	    auth_debug(1, _("krb5: k5_decrypt: give %zu bytes\n"),
		       dectok.length);
	    *decbuf = dectok.value;
	    *decbuflen = dectok.length;
	} else {
	    *decbuf = buf;
	    *decbuflen = buflen;
	}
	auth_debug(1, _("krb5: k5_decrypt: exit\n"));
    } else {
	*decbuf = buf;
	*decbuflen = buflen;
    }
    return (0);
}

/*
 * check ~/.k5amandahosts to see if this principal is allowed in.  If it's
 * hardcoded, then we don't check the realm
 */
static char *
krb5_checkuser( char *	host,
    char *	name,
    char *	realm)
{
#ifdef AMANDA_PRINCIPAL
    if(g_str_equal(name, AMANDA_PRINCIPAL)) {
	return(NULL);
    } else {
	return(g_strdup(_("does not match compiled in default")));
    }
#else
    struct passwd *pwd;
    char *ptmp;
    char *result = _("generic error");	/* default is to not permit */
    FILE *fp = NULL;
    struct stat sbuf;
    uid_t localuid;
    char *line = NULL;
    char *filehost = NULL, *fileuser = NULL, *filerealm = NULL;

    assert( host != NULL);
    assert( name != NULL);

    if((pwd = getpwnam(CLIENT_LOGIN)) == NULL) {
	result = g_strdup_printf(_("can not find user %s"), CLIENT_LOGIN);
	return result;
    }
    localuid = pwd->pw_uid;

#ifdef USE_AMANDAHOSTS
    ptmp = g_strconcat(pwd->pw_dir, "/.k5amandahosts", NULL);
#else
    ptmp = g_strconcat(pwd->pw_dir, "/.k5login", NULL);
#endif

    if(!ptmp) {
	result = g_strdup_printf(_("could not find home directory for %s"), CLIENT_LOGIN);
	goto common_exit;
    }

    auth_debug(1, _("opening ptmp: %s\n"), (ptmp)?ptmp: "NULL!");
    if ((fp = fopen(ptmp, "r")) == NULL) {
	/* check to see if the ptmp file does nto exist. */
	if (errno == ENOENT) {
	    /*
	     * in this case we check to see if the principal matches
	     * the destination user mimicing the .k5login functionality.
	     */
	     if (!g_str_equal(name, CLIENT_LOGIN)) {
		result = g_strdup_printf(_("%s does not match %s"),
					 name, CLIENT_LOGIN);
		return result;
	    }
	    result = NULL;
	    goto common_exit;
	}
	result = g_strdup_printf(_("can not open %s"), ptmp);
	return result;
    }
    auth_debug(1, _("opened ptmp\n"));

    if (fstat(fileno(fp), &sbuf) != 0) {
	result = g_strdup_printf(_("cannot fstat %s: %s"), ptmp, strerror(errno));
	goto common_exit;
    }

    if (sbuf.st_uid != localuid) {
	result = g_strdup_printf(_("%s is owned by %ld, should be %ld"),
		ptmp, (long)sbuf.st_uid, (long)localuid);
	goto common_exit;
    }
    if ((sbuf.st_mode & 077) != 0) {
	result = g_strdup_printf(
	    _("%s: incorrect permissions; file must be accessible only by its owner"), ptmp);
	goto common_exit;
    }

    while ((line = pgets(fp)) != NULL) {
	if (line[0] == '\0') {
	    amfree(line);
	    continue;
	}

	/* if there's more than one column, then it's the host */
	if( (filehost = strtok(line, " \t")) == NULL) {
	    amfree(line);
	    continue;
	}

	/*
	 * if there's only one entry, then it's a username and we have
	 * no hostname.  (so the principal is allowed from anywhere.
	 */
	if((fileuser = strtok(NULL, " \t")) == NULL) {
	    fileuser = filehost;
	    filehost = NULL;
	}

	if(filehost && !g_str_equal(filehost, host)) {
	    amfree(line);
	    continue;
	} else {
		auth_debug(1, _("found a host match\n"));
	}

	if( (filerealm = strchr(fileuser, '@')) != NULL) {
	    *filerealm++ = '\0';
	}

	/*
	 * we have a match.  We're going to be a little bit insecure
	 * and indicate that the principal is correct but the realm is
	 * not if that's the case.  Technically we should say nothing
	 * and let the user figure it out, but it's helpful for debugging.
	 * You likely only get this far if you've turned on cross-realm auth
	 * anyway...
	 */
	auth_debug(1, _("comparing %s %s\n"), fileuser, name);
	if(g_str_equal(fileuser, name)) {
		auth_debug(1, _("found a match!\n"));
		if(realm && filerealm && (!g_str_equal(realm, filerealm))) {
			amfree(line);
			continue;
		}
		result = NULL;
		amfree(line);
		goto common_exit;
	}
	amfree(line);
    }
    result = g_strdup_printf(_("no match in %s"), ptmp);

common_exit:
    afclose(fp);
    return(result);
#endif /* AMANDA_PRINCIPAL */
}

/*
 *  return -1 on error
 *  return  0 on EOF:   *handle = H_EOF  && *size = 0    if socket closed
 *  return  0 on EOF:   *handle = handle && *size = 0    if stream closed
 *  return size     :   *handle = handle && *size = size for data read
 */

static ssize_t
krb5_tcpm_recv_token(
    struct tcp_conn    *rc,
    int *	handle,
    char **	errmsg,
    char **	buf,
    ssize_t *	size,
    int		timeout)
{
    unsigned int netint[2];

    assert(sizeof(netint) == 8);

    switch (net_read(rc->read, &netint, sizeof(netint), timeout)) {
    case -1:
	if (errmsg) {
	    g_free(*errmsg);
	    *errmsg = g_strdup_printf(_("recv error: %s"), strerror(errno));
	}
	auth_debug(1, _("krb5_tcpm_recv_token: A return(-1)\n"));
	return (-1);
    case 0:
	*size = 0;
	*handle = H_EOF;
	g_free(*errmsg);
	*errmsg = g_strdup("SOCKET_EOF");
	auth_debug(1, "krb5_tcpm_recv_token: A return(0)\n");
	return (0);
    default:
	break;
    }

    *size = (ssize_t)ntohl(netint[0]);
    *handle = (int)ntohl(netint[1]);
    /* amanda protocol packet can be above NETWORK_BLOCK_BYTES */
    if (*size > 128*NETWORK_BLOCK_BYTES || *size < 0) {
	if (isprint((int)(*size        ) & 0xFF) &&
	    isprint((int)(*size   >> 8 ) & 0xFF) &&
	    isprint((int)(*size   >> 16) & 0xFF) &&
	    isprint((int)(*size   >> 24) & 0xFF) &&
	    isprint((*handle      ) & 0xFF) &&
	    isprint((*handle >> 8 ) & 0xFF) &&
	    isprint((*handle >> 16) & 0xFF) &&
	    isprint((*handle >> 24) & 0xFF)) {
	    char s[101];
	    int i;
	    s[0] = ((int)(*size)  >> 24) & 0xFF;
	    s[1] = ((int)(*size)  >> 16) & 0xFF;
	    s[2] = ((int)(*size)  >>  8) & 0xFF;
	    s[3] = ((int)(*size)       ) & 0xFF;
	    s[4] = (*handle >> 24) & 0xFF;
	    s[5] = (*handle >> 16) & 0xFF;
	    s[6] = (*handle >> 8 ) & 0xFF;
	    s[7] = (*handle      ) & 0xFF;
	    i = 8; s[i] = ' ';
	    while(i<100 && isprint((int)s[i]) && s[i] != '\n') {
		switch(net_read(rc->read, &s[i], 1, 0)) {
		case -1: s[i] = '\0'; break;
		case  0: s[i] = '\0'; break;
		default:
			 dbprintf(_("read: %c\n"), s[i]); i++; s[i]=' ';
			 break;
		}
	    }
	    s[i] = '\0';
	    g_free(*errmsg);
	    *errmsg = g_strdup_printf(_("krb5_tcpm_recv_token: invalid size: %s"),
                                      s);
	    dbprintf(_("krb5_tcpm_recv_token: invalid size %s\n"), s);
	} else {
	    g_free(*errmsg);
	    *errmsg = g_strdup("krb5_tcpm_recv_token: invalid size");
	    dbprintf("krb5_tcpm_recv_token: invalid size %zd\n", *size);
	}
	*size = -1;
	return -1;
    }
    amfree(*buf);
    *buf = g_malloc((size_t)*size);

    if(*size == 0) {
	auth_debug(1, "krb5_tcpm_recv_token: read EOF from %d\n", *handle);
	g_free(*errmsg);
	*errmsg = g_strdup("EOF");
	return 0;
    }
    switch (net_read(rc->read, *buf, (size_t)*size, timeout)) {
    case -1:
	if (errmsg) {
	    g_free(*errmsg);
	    *errmsg = g_strdup_printf(_("recv error: %s"), strerror(errno));
	}
	auth_debug(1, _("krb5_tcpm_recv_token: B return(-1)\n"));
	return (-1);
    case 0:
	*size = 0;
	g_free(*errmsg);
	*errmsg = g_strdup("SOCKET_EOF");
	auth_debug(1, "krb5_tcpm_recv_token: B return(0)\n");
	return (0);
    default:
	break;
    }

    auth_debug(1, _("krb5_tcpm_recv_token: read %zd bytes from %d\n"), *size, *handle);

    if (*size > 0 && rc->driver->data_decrypt != NULL) {
	void *decbuf;
	ssize_t decsize;
	rc->driver->data_decrypt(rc, *buf, *size, &decbuf, &decsize);
	if (*buf != (char *)decbuf) {
	    amfree(*buf);
	    *buf = (char *)decbuf;
	}
	*size = decsize;
    }

    return((*size));
}

