/*++
/* NAME
/*	scache_clnt 3
/* SUMMARY
/*	session cache manager client
/* SYNOPSIS
/*	#include <scache.h>
/* DESCRIPTION
/*	SCACHE *scache_clnt_create(server, timeout, idle_limit, ttl_limit)
/*	const char *server;
/*	int	timeout;
/*	int	idle_limit;
/*	int	ttl_limit;
/* DESCRIPTION
/*	This module implements the client-side protocol of the
/*	session cache service.
/*
/*	scache_clnt_create() creates a session cache service client.
/*
/*	Arguments:
/* .IP server
/*	The session cache service name.
/* .IP timeout
/*	Time limit for connect, send or receive operations.
/* .IP idle_limit
/*	Idle time after which the client disconnects.
/* .IP ttl_limit
/*	Upper bound on the time that a connection is allowed to persist.
/* DIAGNOSTICS
/*	Fatal error: memory allocation problem;
/*	warning: communication error;
/*	panic: internal consistency failure.
/* SEE ALSO
/*	scache(3), generic session cache API
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*--*/

/* System library. */

#include <sys_defs.h>
#include <errno.h>

/* Utility library. */

#include <msg.h>
#include <mymalloc.h>
#include <auto_clnt.h>
#include <stringops.h>

/*#define msg_verbose 1*/

/* Global library. */

#include <mail_proto.h>
#include <mail_params.h>
#include <scache.h>

/* Application-specific. */

 /*
  * SCACHE_CLNT is a derived type from the SCACHE super-class.
  */
typedef struct {
    SCACHE  scache[1];			/* super-class */
    AUTO_CLNT *auto_clnt;		/* client endpoint */
#ifdef CANT_WRITE_BEFORE_SENDING_FD
    VSTRING *dummy;			/* dummy buffer */
#endif
} SCACHE_CLNT;

#define STR(x) vstring_str(x)

#define SCACHE_MAX_TRIES	2

/* scache_clnt_handshake - receive server protocol announcement */

static int scache_clnt_handshake(VSTREAM *stream)
{
    return (attr_scan(stream, ATTR_FLAG_STRICT,
		   RECV_ATTR_STREQ(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_SCACHE),
		      ATTR_TYPE_END));
}

/* scache_clnt_save_endp - save endpoint */

static void scache_clnt_save_endp(SCACHE *scache, int endp_ttl,
				          const char *endp_label,
				          const char *endp_prop, int fd)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) scache;
    const char *myname = "scache_clnt_save_endp";
    VSTREAM *stream;
    int     status;
    int     tries;
    int     count = 0;

    if (msg_verbose)
	msg_info("%s: endp=%s prop=%s fd=%d",
		 myname, endp_label, endp_prop, fd);

    /*
     * Sanity check.
     */
    if (endp_ttl <= 0)
	msg_panic("%s: bad endp_ttl: %d", myname, endp_ttl);

    /*
     * Try a few times before disabling the cache. We use synchronous calls;
     * the session cache service is CPU bound and making the client
     * asynchronous would just complicate the code.
     */
    for (tries = 0; sp->auto_clnt != 0; tries++) {
	if ((stream = auto_clnt_access(sp->auto_clnt)) != 0) {
	    errno = 0;
	    count += 1;
	    if (attr_print(stream, ATTR_FLAG_NONE,
			 SEND_ATTR_STR(MAIL_ATTR_REQ, SCACHE_REQ_SAVE_ENDP),
			   SEND_ATTR_INT(MAIL_ATTR_TTL, endp_ttl),
			   SEND_ATTR_STR(MAIL_ATTR_LABEL, endp_label),
			   SEND_ATTR_STR(MAIL_ATTR_PROP, endp_prop),
			   ATTR_TYPE_END) != 0
		|| vstream_fflush(stream)
#ifdef CANT_WRITE_BEFORE_SENDING_FD
		|| attr_scan(stream, ATTR_FLAG_STRICT,
			     RECV_ATTR_STR(MAIL_ATTR_DUMMY, sp->dummy),
			     ATTR_TYPE_END) != 1
#endif
		|| LOCAL_SEND_FD(vstream_fileno(stream), fd) < 0
		|| attr_scan(stream, ATTR_FLAG_STRICT,
			     RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
			     ATTR_TYPE_END) != 1) {
		if (msg_verbose || count > 1 || (errno && errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else {
		if (msg_verbose && status != 0)
		    msg_warn("%s: descriptor save failed with status %d",
			     myname, status);
		break;
	    }
	}
	/* Give up or recover. */
	if (tries >= SCACHE_MAX_TRIES - 1) {
	    msg_warn("disabling connection caching");
	    auto_clnt_free(sp->auto_clnt);
	    sp->auto_clnt = 0;
	    break;
	}
	sleep(1);				/* XXX make configurable */
	auto_clnt_recover(sp->auto_clnt);
    }
    /* Always close the descriptor before returning. */
    if (close(fd) < 0)
	msg_warn("%s: close(%d): %m", myname, fd);
}

/* scache_clnt_find_endp - look up cached session */

static int scache_clnt_find_endp(SCACHE *scache, const char *endp_label,
				         VSTRING *endp_prop)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) scache;
    const char *myname = "scache_clnt_find_endp";
    VSTREAM *stream;
    int     status;
    int     tries;
    int     fd;

    /*
     * Try a few times before disabling the cache. We use synchronous calls;
     * the session cache service is CPU bound and making the client
     * asynchronous would just complicate the code.
     */
    for (tries = 0; sp->auto_clnt != 0; tries++) {
	if ((stream = auto_clnt_access(sp->auto_clnt)) != 0) {
	    errno = 0;
	    if (attr_print(stream, ATTR_FLAG_NONE,
			 SEND_ATTR_STR(MAIL_ATTR_REQ, SCACHE_REQ_FIND_ENDP),
			   SEND_ATTR_STR(MAIL_ATTR_LABEL, endp_label),
			   ATTR_TYPE_END) != 0
		|| vstream_fflush(stream)
		|| attr_scan(stream, ATTR_FLAG_STRICT,
			     RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
			     RECV_ATTR_STR(MAIL_ATTR_PROP, endp_prop),
			     ATTR_TYPE_END) != 2) {
		if (msg_verbose || (errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else if (status != 0) {
		if (msg_verbose)
		    msg_info("%s: not found: %s", myname, endp_label);
		return (-1);
	    } else if (
#ifdef CANT_WRITE_BEFORE_SENDING_FD
		       attr_print(stream, ATTR_FLAG_NONE,
				  SEND_ATTR_STR(MAIL_ATTR_DUMMY, ""),
				  ATTR_TYPE_END) != 0
		       || vstream_fflush(stream) != 0
		       || read_wait(vstream_fileno(stream),
				    stream->timeout) < 0 ||	/* XXX */
#endif
		       (fd = LOCAL_RECV_FD(vstream_fileno(stream))) < 0) {
		if (msg_verbose || (errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else {
#ifdef MUST_READ_AFTER_SENDING_FD
		(void) attr_print(stream, ATTR_FLAG_NONE,
				  SEND_ATTR_STR(MAIL_ATTR_DUMMY, ""),
				  ATTR_TYPE_END);
		(void) vstream_fflush(stream);
#endif
		if (msg_verbose)
		    msg_info("%s: endp=%s prop=%s fd=%d",
			     myname, endp_label, STR(endp_prop), fd);
		return (fd);
	    }
	}
	/* Give up or recover. */
	if (tries >= SCACHE_MAX_TRIES - 1) {
	    msg_warn("disabling connection caching");
	    auto_clnt_free(sp->auto_clnt);
	    sp->auto_clnt = 0;
	    return (-1);
	}
	sleep(1);				/* XXX make configurable */
	auto_clnt_recover(sp->auto_clnt);
    }
    return (-1);
}

/* scache_clnt_save_dest - create destination/endpoint association */

static void scache_clnt_save_dest(SCACHE *scache, int dest_ttl,
				          const char *dest_label,
				          const char *dest_prop,
				          const char *endp_label)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) scache;
    const char *myname = "scache_clnt_save_dest";
    VSTREAM *stream;
    int     status;
    int     tries;

    if (msg_verbose)
	msg_info("%s: dest_label=%s dest_prop=%s endp_label=%s",
		 myname, dest_label, dest_prop, endp_label);

    /*
     * Sanity check.
     */
    if (dest_ttl <= 0)
	msg_panic("%s: bad dest_ttl: %d", myname, dest_ttl);

    /*
     * Try a few times before disabling the cache. We use synchronous calls;
     * the session cache service is CPU bound and making the client
     * asynchronous would just complicate the code.
     */
    for (tries = 0; sp->auto_clnt != 0; tries++) {
	if ((stream = auto_clnt_access(sp->auto_clnt)) != 0) {
	    errno = 0;
	    if (attr_print(stream, ATTR_FLAG_NONE,
			 SEND_ATTR_STR(MAIL_ATTR_REQ, SCACHE_REQ_SAVE_DEST),
			   SEND_ATTR_INT(MAIL_ATTR_TTL, dest_ttl),
			   SEND_ATTR_STR(MAIL_ATTR_LABEL, dest_label),
			   SEND_ATTR_STR(MAIL_ATTR_PROP, dest_prop),
			   SEND_ATTR_STR(MAIL_ATTR_LABEL, endp_label),
			   ATTR_TYPE_END) != 0
		|| vstream_fflush(stream)
		|| attr_scan(stream, ATTR_FLAG_STRICT,
			     RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
			     ATTR_TYPE_END) != 1) {
		if (msg_verbose || (errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else {
		if (msg_verbose && status != 0)
		    msg_warn("%s: destination save failed with status %d",
			     myname, status);
		break;
	    }
	}
	/* Give up or recover. */
	if (tries >= SCACHE_MAX_TRIES - 1) {
	    msg_warn("disabling connection caching");
	    auto_clnt_free(sp->auto_clnt);
	    sp->auto_clnt = 0;
	    break;
	}
	sleep(1);				/* XXX make configurable */
	auto_clnt_recover(sp->auto_clnt);
    }
}

/* scache_clnt_find_dest - look up cached session */

static int scache_clnt_find_dest(SCACHE *scache, const char *dest_label,
				         VSTRING *dest_prop,
				         VSTRING *endp_prop)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) scache;
    const char *myname = "scache_clnt_find_dest";
    VSTREAM *stream;
    int     status;
    int     tries;
    int     fd;

    /*
     * Try a few times before disabling the cache. We use synchronous calls;
     * the session cache service is CPU bound and making the client
     * asynchronous would just complicate the code.
     */
    for (tries = 0; sp->auto_clnt != 0; tries++) {
	if ((stream = auto_clnt_access(sp->auto_clnt)) != 0) {
	    errno = 0;
	    if (attr_print(stream, ATTR_FLAG_NONE,
			 SEND_ATTR_STR(MAIL_ATTR_REQ, SCACHE_REQ_FIND_DEST),
			   SEND_ATTR_STR(MAIL_ATTR_LABEL, dest_label),
			   ATTR_TYPE_END) != 0
		|| vstream_fflush(stream)
		|| attr_scan(stream, ATTR_FLAG_STRICT,
			     RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
			     RECV_ATTR_STR(MAIL_ATTR_PROP, dest_prop),
			     RECV_ATTR_STR(MAIL_ATTR_PROP, endp_prop),
			     ATTR_TYPE_END) != 3) {
		if (msg_verbose || (errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else if (status != 0) {
		if (msg_verbose)
		    msg_info("%s: not found: %s", myname, dest_label);
		return (-1);
	    } else if (
#ifdef CANT_WRITE_BEFORE_SENDING_FD
		       attr_print(stream, ATTR_FLAG_NONE,
				  SEND_ATTR_STR(MAIL_ATTR_DUMMY, ""),
				  ATTR_TYPE_END) != 0
		       || vstream_fflush(stream) != 0
		       || read_wait(vstream_fileno(stream),
				    stream->timeout) < 0 ||	/* XXX */
#endif
		       (fd = LOCAL_RECV_FD(vstream_fileno(stream))) < 0) {
		if (msg_verbose || (errno != EPIPE && errno != ENOENT))
		    msg_warn("problem talking to service %s: %m",
			     VSTREAM_PATH(stream));
		/* Give up or recover. */
	    } else {
#ifdef MUST_READ_AFTER_SENDING_FD
		(void) attr_print(stream, ATTR_FLAG_NONE,
				  SEND_ATTR_STR(MAIL_ATTR_DUMMY, ""),
				  ATTR_TYPE_END);
		(void) vstream_fflush(stream);
#endif
		if (msg_verbose)
		    msg_info("%s: dest=%s dest_prop=%s endp_prop=%s fd=%d",
		    myname, dest_label, STR(dest_prop), STR(endp_prop), fd);
		return (fd);
	    }
	}
	/* Give up or recover. */
	if (tries >= SCACHE_MAX_TRIES - 1) {
	    msg_warn("disabling connection caching");
	    auto_clnt_free(sp->auto_clnt);
	    sp->auto_clnt = 0;
	    return (-1);
	}
	sleep(1);				/* XXX make configurable */
	auto_clnt_recover(sp->auto_clnt);
    }
    return (-1);
}

/* scache_clnt_size - dummy */

static void scache_clnt_size(SCACHE *unused_scache, SCACHE_SIZE *size)
{
    /* XXX Crap in a hurry. */
    size->dest_count = 0;
    size->endp_count = 0;
    size->sess_count = 0;
}

/* scache_clnt_free - destroy cache */

static void scache_clnt_free(SCACHE *scache)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) scache;

    if (sp->auto_clnt)
	auto_clnt_free(sp->auto_clnt);
#ifdef CANT_WRITE_BEFORE_SENDING_FD
    vstring_free(sp->dummy);
#endif
    myfree((void *) sp);
}

/* scache_clnt_create - initialize */

SCACHE *scache_clnt_create(const char *server, int timeout,
			           int idle_limit, int ttl_limit)
{
    SCACHE_CLNT *sp = (SCACHE_CLNT *) mymalloc(sizeof(*sp));
    char   *service;

    sp->scache->save_endp = scache_clnt_save_endp;
    sp->scache->find_endp = scache_clnt_find_endp;
    sp->scache->save_dest = scache_clnt_save_dest;
    sp->scache->find_dest = scache_clnt_find_dest;
    sp->scache->size = scache_clnt_size;
    sp->scache->free = scache_clnt_free;

    service = concatenate("local:" MAIL_CLASS_PRIVATE "/", server, (char *) 0);
    sp->auto_clnt = auto_clnt_create(service, timeout, idle_limit, ttl_limit);
    auto_clnt_control(sp->auto_clnt,
		      AUTO_CLNT_CTL_HANDSHAKE, scache_clnt_handshake,
		      AUTO_CLNT_CTL_END);
    myfree(service);

#ifdef CANT_WRITE_BEFORE_SENDING_FD
    sp->dummy = vstring_alloc(1);
#endif

    return (sp->scache);
}
