/*++
/* NAME
/*	scache 8
/* SUMMARY
/*	Postfix shared connection cache server
/* SYNOPSIS
/*	\fBscache\fR [generic Postfix daemon options]
/* DESCRIPTION
/*	The \fBscache\fR(8) server maintains a shared multi-connection
/*	cache. This information can be used by, for example, Postfix
/*	SMTP clients or other Postfix delivery agents.
/*
/*	The connection cache is organized into logical destination
/*	names, physical endpoint names, and connections.
/*
/*	As a specific example, logical SMTP destinations specify
/*	(transport, domain, port), and physical SMTP endpoints
/*	specify (transport, IP address, port).  An SMTP connection
/*	may be saved after a successful mail transaction.
/*
/*	In the general case, one logical destination may refer to
/*	zero or more physical endpoints, one physical endpoint may
/*	be referenced by zero or more logical destinations, and
/*	one endpoint may refer to zero or more connections.
/*
/*	The exact syntax of a logical destination or endpoint name
/*	is application dependent; the \fBscache\fR(8) server does
/*	not care.  A connection is stored as a file descriptor together
/*	with application-dependent information that is needed to
/*	re-activate a connection object. Again, the \fBscache\fR(8)
/*	server is completely unaware of the details of that
/*	information.
/*
/*	All information is stored with a finite time to live (ttl).
/*	The connection cache daemon terminates when no client is
/*	connected for \fBmax_idle\fR time units.
/*
/*	This server implements the following requests:
/* .IP "\fBsave_endp\fI ttl endpoint endpoint_properties file_descriptor\fR"
/*	Save the specified file descriptor and connection property data
/*	under the specified endpoint name. The endpoint properties
/*	are used by the client to re-activate a passivated connection
/*	object.
/* .IP "\fBfind_endp\fI endpoint\fR"
/*	Look up cached properties and a cached file descriptor for the
/*	specified endpoint.
/* .IP "\fBsave_dest\fI ttl destination destination_properties endpoint\fR"
/*	Save the binding between a logical destination and an
/*	endpoint under the destination name, together with destination
/*	specific connection properties. The destination properties
/*	are used by the client to re-activate a passivated connection
/*	object.
/* .IP "\fBfind_dest\fI destination\fR"
/*	Look up cached destination properties, cached endpoint properties,
/*	and a cached file descriptor for the specified logical destination.
/* SECURITY
/* .ad
/* .fi
/*	The \fBscache\fR(8) server is not security-sensitive. It does not
/*	talk to the network, and it does not talk to local users.
/*	The \fBscache\fR(8) server can run chrooted at fixed low privilege.
/*
/*	The \fBscache\fR(8) server is not a trusted process. It must
/*	not be used to store information that is security sensitive.
/* DIAGNOSTICS
/*	Problems and transactions are logged to \fBsyslogd\fR(8)
/*	or \fBpostlogd\fR(8).
/* BUGS
/*	The session cache cannot be shared among multiple machines.
/*
/*	When a connection expires from the cache, it is closed without
/*	the appropriate protocol specific handshake.
/* CONFIGURATION PARAMETERS
/* .ad
/* .fi
/*	Changes to \fBmain.cf\fR are picked up automatically as \fBscache\fR(8)
/*	processes run for only a limited amount of time. Use the command
/*	"\fBpostfix reload\fR" to speed up a change.
/*
/*	The text below provides only a parameter summary. See
/*	\fBpostconf\fR(5) for more details including examples.
/* RESOURCE CONTROLS
/* .ad
/* .fi
/* .IP "\fBconnection_cache_ttl_limit (2s)\fR"
/*	The maximal time-to-live value that the \fBscache\fR(8) connection
/*	cache server
/*	allows.
/* .IP "\fBconnection_cache_status_update_time (600s)\fR"
/*	How frequently the \fBscache\fR(8) server logs usage statistics with
/*	connection cache hit and miss rates for logical destinations and for
/*	physical endpoints.
/* MISCELLANEOUS CONTROLS
/* .ad
/* .fi
/* .IP "\fBconfig_directory (see 'postconf -d' output)\fR"
/*	The default location of the Postfix main.cf and master.cf
/*	configuration files.
/* .IP "\fBdaemon_timeout (18000s)\fR"
/*	How much time a Postfix daemon process may take to handle a
/*	request before it is terminated by a built-in watchdog timer.
/* .IP "\fBipc_timeout (3600s)\fR"
/*	The time limit for sending or receiving information over an internal
/*	communication channel.
/* .IP "\fBmax_idle (100s)\fR"
/*	The maximum amount of time that an idle Postfix daemon process waits
/*	for an incoming connection before terminating voluntarily.
/* .IP "\fBprocess_id (read-only)\fR"
/*	The process ID of a Postfix command or daemon process.
/* .IP "\fBprocess_name (read-only)\fR"
/*	The process name of a Postfix command or daemon process.
/* .IP "\fBsyslog_facility (mail)\fR"
/*	The syslog facility of Postfix logging.
/* .IP "\fBsyslog_name (see 'postconf -d' output)\fR"
/*	A prefix that is prepended to the process name in syslog
/*	records, so that, for example, "smtpd" becomes "prefix/smtpd".
/* .PP
/*	Available in Postfix 3.3 and later:
/* .IP "\fBservice_name (read-only)\fR"
/*	The master.cf service name of a Postfix daemon process.
/* SEE ALSO
/*	smtp(8), SMTP client
/*	postconf(5), configuration parameters
/*	master(8), process manager
/*	postlogd(8), Postfix logging
/*	syslogd(8), system logging
/* README FILES
/* .ad
/* .fi
/*	Use "\fBpostconf readme_directory\fR" or
/*	"\fBpostconf html_directory\fR" to locate this information.
/* .na
/* .nf
/*	CONNECTION_CACHE_README, Postfix connection cache
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* HISTORY
/*	This service was introduced with Postfix version 2.2.
/* 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 <time.h>

/* Utility library. */

#include <msg.h>
#include <iostuff.h>
#include <htable.h>
#include <ring.h>
#include <events.h>

/* Global library. */

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

/* Single server skeleton. */

#include <mail_server.h>
#include <mail_conf.h>

/* Application-specific. */

 /*
  * Tunable parameters.
  */
int     var_scache_ttl_lim;
int     var_scache_stat_time;

 /*
  * Request parameters.
  */
static VSTRING *scache_request;
static VSTRING *scache_dest_label;
static VSTRING *scache_dest_prop;
static VSTRING *scache_endp_label;
static VSTRING *scache_endp_prop;

#ifdef CANT_WRITE_BEFORE_SENDING_FD
static VSTRING *scache_dummy;

#endif

 /*
  * Session cache instance.
  */
static SCACHE *scache;

 /*
  * Statistics.
  */
static int scache_dest_hits;
static int scache_dest_miss;
static int scache_dest_count;
static int scache_endp_hits;
static int scache_endp_miss;
static int scache_endp_count;
static int scache_sess_count;
time_t  scache_start_time;

 /*
  * Silly little macros.
  */
#define STR(x)			vstring_str(x)
#define VSTREQ(x,y)		(strcmp(STR(x),y) == 0)

/* scache_save_endp_service - protocol to save endpoint->stream binding */

static void scache_save_endp_service(VSTREAM *client_stream)
{
    const char *myname = "scache_save_endp_service";
    int     ttl;
    int     fd;
    SCACHE_SIZE size;

    if (attr_scan(client_stream,
		  ATTR_FLAG_STRICT,
		  RECV_ATTR_INT(MAIL_ATTR_TTL, &ttl),
		  RECV_ATTR_STR(MAIL_ATTR_LABEL, scache_endp_label),
		  RECV_ATTR_STR(MAIL_ATTR_PROP, scache_endp_prop),
		  ATTR_TYPE_END) != 3
	|| ttl <= 0) {
	msg_warn("%s: bad or missing request parameter", myname);
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_BAD),
		   ATTR_TYPE_END);
	return;
    } else if (
#ifdef CANT_WRITE_BEFORE_SENDING_FD
	       attr_print(client_stream, ATTR_FLAG_NONE,
			  SEND_ATTR_STR(MAIL_ATTR_DUMMY, ""),
			  ATTR_TYPE_END) != 0
	       || vstream_fflush(client_stream) != 0
	       || read_wait(vstream_fileno(client_stream),
			    client_stream->timeout) < 0	/* XXX */
	       ||
#endif
	       (fd = LOCAL_RECV_FD(vstream_fileno(client_stream))) < 0) {
	msg_warn("%s: unable to receive file descriptor: %m", myname);
	(void) attr_print(client_stream, ATTR_FLAG_NONE,
			  SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_FAIL),
			  ATTR_TYPE_END);
	return;
    } else {
	scache_save_endp(scache,
			 ttl > var_scache_ttl_lim ? var_scache_ttl_lim : ttl,
			 STR(scache_endp_label), STR(scache_endp_prop), fd);
	(void) attr_print(client_stream, ATTR_FLAG_NONE,
			  SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_OK),
			  ATTR_TYPE_END);
	scache_size(scache, &size);
	if (size.endp_count > scache_endp_count)
	    scache_endp_count = size.endp_count;
	if (size.sess_count > scache_sess_count)
	    scache_sess_count = size.sess_count;
	return;
    }
}

/* scache_find_endp_service - protocol to find connection for endpoint */

static void scache_find_endp_service(VSTREAM *client_stream)
{
    const char *myname = "scache_find_endp_service";
    int     fd;

    if (attr_scan(client_stream,
		  ATTR_FLAG_STRICT,
		  RECV_ATTR_STR(MAIL_ATTR_LABEL, scache_endp_label),
		  ATTR_TYPE_END) != 1) {
	msg_warn("%s: bad or missing request parameter", myname);
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_BAD),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   ATTR_TYPE_END);
	return;
    } else if ((fd = scache_find_endp(scache, STR(scache_endp_label),
				      scache_endp_prop)) < 0) {
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_FAIL),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   ATTR_TYPE_END);
	scache_endp_miss++;
	return;
    } else {
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_OK),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, STR(scache_endp_prop)),
		   ATTR_TYPE_END);
	if (vstream_fflush(client_stream) != 0
#ifdef CANT_WRITE_BEFORE_SENDING_FD
	    || attr_scan(client_stream, ATTR_FLAG_STRICT,
			 RECV_ATTR_STR(MAIL_ATTR_DUMMY, scache_dummy),
			 ATTR_TYPE_END) != 1
#endif
	    || LOCAL_SEND_FD(vstream_fileno(client_stream), fd) < 0
#ifdef MUST_READ_AFTER_SENDING_FD
	    || attr_scan(client_stream, ATTR_FLAG_STRICT,
			 RECV_ATTR_STR(MAIL_ATTR_DUMMY, scache_dummy),
			 ATTR_TYPE_END) != 1
#endif
	    )
	    msg_warn("%s: cannot send file descriptor: %m", myname);
	if (close(fd) < 0)
	    msg_warn("close(%d): %m", fd);
	scache_endp_hits++;
	return;
    }
}

/* scache_save_dest_service - protocol to save destination->endpoint binding */

static void scache_save_dest_service(VSTREAM *client_stream)
{
    const char *myname = "scache_save_dest_service";
    int     ttl;
    SCACHE_SIZE size;

    if (attr_scan(client_stream,
		  ATTR_FLAG_STRICT,
		  RECV_ATTR_INT(MAIL_ATTR_TTL, &ttl),
		  RECV_ATTR_STR(MAIL_ATTR_LABEL, scache_dest_label),
		  RECV_ATTR_STR(MAIL_ATTR_PROP, scache_dest_prop),
		  RECV_ATTR_STR(MAIL_ATTR_LABEL, scache_endp_label),
		  ATTR_TYPE_END) != 4
	|| ttl <= 0) {
	msg_warn("%s: bad or missing request parameter", myname);
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_BAD),
		   ATTR_TYPE_END);
	return;
    } else {
	scache_save_dest(scache,
			 ttl > var_scache_ttl_lim ? var_scache_ttl_lim : ttl,
			 STR(scache_dest_label), STR(scache_dest_prop),
			 STR(scache_endp_label));
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_OK),
		   ATTR_TYPE_END);
	scache_size(scache, &size);
	if (size.dest_count > scache_dest_count)
	    scache_dest_count = size.dest_count;
	if (size.endp_count > scache_endp_count)
	    scache_endp_count = size.endp_count;
	return;
    }
}

/* scache_find_dest_service - protocol to find connection for destination */

static void scache_find_dest_service(VSTREAM *client_stream)
{
    const char *myname = "scache_find_dest_service";
    int     fd;

    if (attr_scan(client_stream,
		  ATTR_FLAG_STRICT,
		  RECV_ATTR_STR(MAIL_ATTR_LABEL, scache_dest_label),
		  ATTR_TYPE_END) != 1) {
	msg_warn("%s: bad or missing request parameter", myname);
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_BAD),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   ATTR_TYPE_END);
	return;
    } else if ((fd = scache_find_dest(scache, STR(scache_dest_label),
				      scache_dest_prop,
				      scache_endp_prop)) < 0) {
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_FAIL),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, ""),
		   ATTR_TYPE_END);
	scache_dest_miss++;
	return;
    } else {
	attr_print(client_stream, ATTR_FLAG_NONE,
		   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_OK),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, STR(scache_dest_prop)),
		   SEND_ATTR_STR(MAIL_ATTR_PROP, STR(scache_endp_prop)),
		   ATTR_TYPE_END);
	if (vstream_fflush(client_stream) != 0
#ifdef CANT_WRITE_BEFORE_SENDING_FD
	    || attr_scan(client_stream, ATTR_FLAG_STRICT,
			 RECV_ATTR_STR(MAIL_ATTR_DUMMY, scache_dummy),
			 ATTR_TYPE_END) != 1
#endif
	    || LOCAL_SEND_FD(vstream_fileno(client_stream), fd) < 0
#ifdef MUST_READ_AFTER_SENDING_FD
	    || attr_scan(client_stream, ATTR_FLAG_STRICT,
			 RECV_ATTR_STR(MAIL_ATTR_DUMMY, scache_dummy),
			 ATTR_TYPE_END) != 1
#endif
	    )
	    msg_warn("%s: cannot send file descriptor: %m", myname);
	if (close(fd) < 0)
	    msg_warn("close(%d): %m", fd);
	scache_dest_hits++;
	return;
    }
}

/* scache_service - perform service for client */

static void scache_service(VSTREAM *client_stream, char *unused_service,
			           char **argv)
{

    /*
     * Sanity check. This service takes no command-line arguments.
     */
    if (argv[0])
	msg_fatal("unexpected command-line argument: %s", argv[0]);

    /*
     * This routine runs whenever a client connects to the UNIX-domain socket
     * dedicated to the scache service. All connection-management stuff is
     * handled by the common code in multi_server.c.
     * 
     * XXX Workaround: with some requests, the client sends a dummy message
     * after the server replies (yes that's a botch). When the scache server
     * is slow, this dummy message may become concatenated with the next
     * request from the same client. The do-while loop below will repeat
     * instead of discarding the client request. We must process it now
     * because there will be no select() notification.
     */
    do {
	if (attr_scan(client_stream,
		      ATTR_FLAG_MORE | ATTR_FLAG_STRICT,
		      RECV_ATTR_STR(MAIL_ATTR_REQ, scache_request),
		      ATTR_TYPE_END) == 1) {
	    if (VSTREQ(scache_request, SCACHE_REQ_SAVE_DEST)) {
		scache_save_dest_service(client_stream);
	    } else if (VSTREQ(scache_request, SCACHE_REQ_FIND_DEST)) {
		scache_find_dest_service(client_stream);
	    } else if (VSTREQ(scache_request, SCACHE_REQ_SAVE_ENDP)) {
		scache_save_endp_service(client_stream);
	    } else if (VSTREQ(scache_request, SCACHE_REQ_FIND_ENDP)) {
		scache_find_endp_service(client_stream);
	    } else {
		msg_warn("unrecognized request: \"%s\", ignored",
			 STR(scache_request));
		attr_print(client_stream, ATTR_FLAG_NONE,
			   SEND_ATTR_INT(MAIL_ATTR_STATUS, SCACHE_STAT_BAD),
			   ATTR_TYPE_END);
	    }
	}
    } while (vstream_peek(client_stream) > 0);
    vstream_fflush(client_stream);
}

/* scache_status_dump - log and reset cache statistics */

static void scache_status_dump(char *unused_name, char **unused_argv)
{
    if (scache_dest_hits || scache_dest_miss
	|| scache_endp_hits || scache_endp_miss
	|| scache_dest_count || scache_endp_count
	|| scache_sess_count)
	msg_info("statistics: start interval %.15s",
		 ctime(&scache_start_time) + 4);

    if (scache_dest_hits || scache_dest_miss) {
	msg_info("statistics: domain lookup hits=%d miss=%d success=%d%%",
		 scache_dest_hits, scache_dest_miss,
		 scache_dest_hits * 100
		 / (scache_dest_hits + scache_dest_miss));
	scache_dest_hits = scache_dest_miss = 0;
    }
    if (scache_endp_hits || scache_endp_miss) {
	msg_info("statistics: address lookup hits=%d miss=%d success=%d%%",
		 scache_endp_hits, scache_endp_miss,
		 scache_endp_hits * 100
		 / (scache_endp_hits + scache_endp_miss));
	scache_endp_hits = scache_endp_miss = 0;
    }
    if (scache_dest_count || scache_endp_count || scache_sess_count) {
	msg_info("statistics: max simultaneous domains=%d addresses=%d connection=%d",
		 scache_dest_count, scache_endp_count, scache_sess_count);
	scache_dest_count = 0;
	scache_endp_count = 0;
	scache_sess_count = 0;
    }
    scache_start_time = event_time();
}

/* scache_status_update - log and reset cache statistics periodically */

static void scache_status_update(int unused_event, void *context)
{
    scache_status_dump((char *) 0, (char **) 0);
    event_request_timer(scache_status_update, context, var_scache_stat_time);
}

/* post_jail_init - initialization after privilege drop */

static void post_jail_init(char *unused_name, char **unused_argv)
{

    /*
     * Pre-allocate the cache instance.
     */
    scache = scache_multi_create();

    /*
     * Pre-allocate buffers.
     */
    scache_request = vstring_alloc(10);
    scache_dest_label = vstring_alloc(10);
    scache_dest_prop = vstring_alloc(10);
    scache_endp_label = vstring_alloc(10);
    scache_endp_prop = vstring_alloc(10);
#ifdef CANT_WRITE_BEFORE_SENDING_FD
    scache_dummy = vstring_alloc(10);
#endif

    /*
     * Disable the max_use limit. We still terminate when no client is
     * connected for $idle_limit time units.
     */
    var_use_limit = 0;

    /*
     * Dump and reset cache statistics every so often.
     */
    event_request_timer(scache_status_update, (void *) 0, var_scache_stat_time);
    scache_start_time = event_time();
}

/* scache_post_accept - announce our protocol */

static void scache_post_accept(VSTREAM *stream, char *unused_name,
			           char **unused_argv, HTABLE *unused_table)
{

    /*
     * Announce the protocol.
     */
    attr_print(stream, ATTR_FLAG_NONE,
	       SEND_ATTR_STR(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_SCACHE),
	       ATTR_TYPE_END);
    (void) vstream_fflush(stream);
}

MAIL_VERSION_STAMP_DECLARE;

/* main - pass control to the multi-threaded skeleton */

int     main(int argc, char **argv)
{
    static const CONFIG_TIME_TABLE time_table[] = {
	VAR_SCACHE_TTL_LIM, DEF_SCACHE_TTL_LIM, &var_scache_ttl_lim, 1, 0,
	VAR_SCACHE_STAT_TIME, DEF_SCACHE_STAT_TIME, &var_scache_stat_time, 1, 0,
	0,
    };

    /*
     * Fingerprint executables and core dumps.
     */
    MAIL_VERSION_STAMP_ALLOCATE;

    multi_server_main(argc, argv, scache_service,
		      CA_MAIL_SERVER_TIME_TABLE(time_table),
		      CA_MAIL_SERVER_POST_INIT(post_jail_init),
		      CA_MAIL_SERVER_POST_ACCEPT(scache_post_accept),
		      CA_MAIL_SERVER_EXIT(scache_status_dump),
		      CA_MAIL_SERVER_SOLITARY,
		      0);
}
