/*++
/* NAME
/*	smtp_sasl_glue 3
/* SUMMARY
/*	Postfix SASL interface for SMTP client
/* SYNOPSIS
/*	#include smtp_sasl.h
/*
/*	void	smtp_sasl_initialize()
/*
/*	void	smtp_sasl_connect(session)
/*	SMTP_SESSION *session;
/*
/*	void	smtp_sasl_start(session, sasl_opts_name, sasl_opts_val)
/*	SMTP_SESSION *session;
/*
/*	int     smtp_sasl_passwd_lookup(session)
/*	SMTP_SESSION *session;
/*
/*	int	smtp_sasl_authenticate(session, why)
/*	SMTP_SESSION *session;
/*	DSN_BUF *why;
/*
/*	void	smtp_sasl_cleanup(session)
/*	SMTP_SESSION *session;
/*
/*	void	smtp_sasl_passivate(session, buf)
/*	SMTP_SESSION *session;
/*	VSTRING	*buf;
/*
/*	int	smtp_sasl_activate(session, buf)
/*	SMTP_SESSION *session;
/*	char	*buf;
/* DESCRIPTION
/*	smtp_sasl_initialize() initializes the SASL library. This
/*	routine must be called once at process startup, before any
/*	chroot operations.
/*
/*	smtp_sasl_connect() performs per-session initialization. This
/*	routine must be called once at the start of each connection.
/*
/*	smtp_sasl_start() performs per-session initialization. This
/*	routine must be called once per session before doing any SASL
/*	authentication. The sasl_opts_name and sasl_opts_val parameters are
/*	the postfix configuration parameters setting the security
/*	policy of the SASL authentication.
/*
/*	smtp_sasl_passwd_lookup() looks up the username/password
/*	for the current SMTP server. The result is zero in case
/*	of failure, a long jump in case of error.
/*
/*	smtp_sasl_authenticate() implements the SASL authentication
/*	dialog. The result is < 0 in case of protocol failure, zero in
/*	case of unsuccessful authentication, > 0 in case of success.
/*	The why argument is updated with a reason for failure.
/*	This routine must be called only when smtp_sasl_passwd_lookup()
/*	succeeds.
/*
/*	smtp_sasl_cleanup() cleans up. It must be called at the
/*	end of every SMTP session that uses SASL authentication.
/*	This routine is a noop for non-SASL sessions.
/*
/*	smtp_sasl_passivate() appends flattened SASL attributes to the
/*	specified buffer. The SASL attributes are not destroyed.
/*
/*	smtp_sasl_activate() restores SASL attributes from the
/*	specified buffer. The buffer is modified. A result < 0
/*	means there was an error.
/*
/*	Arguments:
/* .IP session
/*	Session context.
/* .IP mech_list
/*	String of SASL mechanisms (separated by blanks)
/* DIAGNOSTICS
/*	All errors are fatal.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Original author:
/*	Till Franke
/*	SuSE Rhein/Main AG
/*	65760 Eschborn, Germany
/*
/*	Adopted by:
/*	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 <stdlib.h>
#include <string.h>

 /*
  * Utility library
  */
#include <msg.h>
#include <mymalloc.h>
#include <stringops.h>
#include <split_at.h>

 /*
  * Global library
  */
#include <mail_params.h>
#include <string_list.h>
#include <maps.h>
#include <mail_addr_find.h>
#include <smtp_stream.h>

 /*
  * XSASL library.
  */
#include <xsasl.h>

 /*
  * Application-specific
  */
#include "smtp.h"
#include "smtp_sasl.h"
#include "smtp_sasl_auth_cache.h"

#ifdef USE_SASL_AUTH

 /*
  * Per-host login/password information.
  */
static MAPS *smtp_sasl_passwd_map;

 /*
  * Supported SASL mechanisms.
  */
STRING_LIST *smtp_sasl_mechs;

 /*
  * SASL implementation handle.
  */
static XSASL_CLIENT_IMPL *smtp_sasl_impl;

 /*
  * The 535 SASL authentication failure cache.
  */
#ifdef HAVE_SASL_AUTH_CACHE
static SMTP_SASL_AUTH_CACHE *smtp_sasl_auth_cache;

#endif

/* smtp_sasl_passwd_lookup - password lookup routine */

int     smtp_sasl_passwd_lookup(SMTP_SESSION *session)
{
    const char *myname = "smtp_sasl_passwd_lookup";
    SMTP_STATE *state = session->state;
    SMTP_ITERATOR *iter = session->iterator;
    const char *value;
    char   *passwd;

    /*
     * Sanity check.
     */
    if (smtp_sasl_passwd_map == 0)
	msg_panic("%s: passwd map not initialized", myname);

    /*
     * Look up the per-server password information. Try the hostname first,
     * then try the destination.
     * 
     * XXX Instead of using nexthop (the intended destination) we use dest
     * (either the intended destination, or a fall-back destination).
     * 
     * XXX SASL authentication currently depends on the host/domain but not on
     * the TCP port. If the port is not :25, we should append it to the table
     * lookup key. Code for this was briefly introduced into 2.2 snapshots,
     * but didn't canonicalize the TCP port, and did not append the port to
     * the MX hostname.
     */
    smtp_sasl_passwd_map->error = 0;
    if ((smtp_mode
	 && var_smtp_sender_auth && state->request->sender[0]
	 && (value = mail_addr_find(smtp_sasl_passwd_map,
				 state->request->sender, (char **) 0)) != 0)
	|| (smtp_sasl_passwd_map->error == 0
	    && (value = maps_find(smtp_sasl_passwd_map,
				  STR(iter->host), 0)) != 0)
	|| (smtp_sasl_passwd_map->error == 0
	    && (value = maps_find(smtp_sasl_passwd_map,
				  STR(iter->dest), 0)) != 0)) {
	if (session->sasl_username)
	    myfree(session->sasl_username);
	session->sasl_username = mystrdup(value);
	/* Historically, the delimiter may appear in the password. */
	passwd = split_at(session->sasl_username,
			  *var_smtp_sasl_passwd_res_delim);
	if (session->sasl_passwd)
	    myfree(session->sasl_passwd);
	session->sasl_passwd = mystrdup(passwd ? passwd : "");
	if (msg_verbose)
	    msg_info("%s: host `%s' user `%s' pass `%s'",
		     myname, STR(iter->host),
		     session->sasl_username, session->sasl_passwd);
	return (1);
    } else if (smtp_sasl_passwd_map->error) {
	msg_warn("%s: %s lookup error",
		 state->request->queue_id, smtp_sasl_passwd_map->title);
	vstream_longjmp(session->stream, SMTP_ERR_DATA);
    } else {
	if (msg_verbose)
	    msg_info("%s: no auth info found (sender=`%s', host=`%s')",
		     myname, state->request->sender, STR(iter->host));
	return (0);
    }
}

/* smtp_sasl_initialize - per-process initialization (pre jail) */

void    smtp_sasl_initialize(void)
{

    /*
     * Sanity check.
     */
    if (smtp_sasl_passwd_map || smtp_sasl_impl)
	msg_panic("smtp_sasl_initialize: repeated call");
    if (*var_smtp_sasl_passwd == 0)
	msg_fatal("specify a password table via the `%s' configuration parameter",
		  VAR_LMTP_SMTP(SASL_PASSWD));

    /*
     * Open the per-host password table and initialize the SASL library. Use
     * shared locks for reading, just in case someone updates the table.
     */
    smtp_sasl_passwd_map = maps_create(VAR_LMTP_SMTP(SASL_PASSWD),
				       var_smtp_sasl_passwd,
				       DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
				       | DICT_FLAG_UTF8_REQUEST);
    if ((smtp_sasl_impl = xsasl_client_init(var_smtp_sasl_type,
					    var_smtp_sasl_path)) == 0)
	msg_fatal("SASL library initialization");

    /*
     * Initialize optional supported mechanism matchlist
     */
    if (*var_smtp_sasl_mechs)
	smtp_sasl_mechs = string_list_init(VAR_SMTP_SASL_MECHS,
					   MATCH_FLAG_NONE,
					   var_smtp_sasl_mechs);

    /*
     * Initialize the 535 SASL authentication failure cache.
     */
    if (*var_smtp_sasl_auth_cache_name) {
#ifdef HAVE_SASL_AUTH_CACHE
	smtp_sasl_auth_cache =
	    smtp_sasl_auth_cache_init(var_smtp_sasl_auth_cache_name,
				      var_smtp_sasl_auth_cache_time);
#else
	msg_warn("not compiled with TLS support -- "
	    "ignoring the %s setting", VAR_LMTP_SMTP(SASL_AUTH_CACHE_NAME));
#endif
    }
}

/* smtp_sasl_connect - per-session client initialization */

void    smtp_sasl_connect(SMTP_SESSION *session)
{

    /*
     * This initialization happens whenever we instantiate an SMTP session
     * object. We don't instantiate a SASL client until we actually need one.
     */
    session->sasl_mechanism_list = 0;
    session->sasl_username = 0;
    session->sasl_passwd = 0;
    session->sasl_client = 0;
    session->sasl_reply = 0;
}

/* smtp_sasl_start - per-session SASL initialization */

void    smtp_sasl_start(SMTP_SESSION *session, const char *sasl_opts_name,
			        const char *sasl_opts_val)
{
    XSASL_CLIENT_CREATE_ARGS create_args;
    SMTP_ITERATOR *iter = session->iterator;

    if (msg_verbose)
	msg_info("starting new SASL client");
    if ((session->sasl_client =
	 XSASL_CLIENT_CREATE(smtp_sasl_impl, &create_args,
			     stream = session->stream,
			     service = var_procname,
			     server_name = STR(iter->host),
			     security_options = sasl_opts_val)) == 0)
	msg_fatal("SASL per-connection initialization failed");
    session->sasl_reply = vstring_alloc(20);
}

/* smtp_sasl_authenticate - run authentication protocol */

int     smtp_sasl_authenticate(SMTP_SESSION *session, DSN_BUF *why)
{
    const char *myname = "smtp_sasl_authenticate";
    SMTP_ITERATOR *iter = session->iterator;
    SMTP_RESP *resp;
    const char *mechanism;
    int     result;
    char   *line;
    int     steps = 0;

    /*
     * Sanity check.
     */
    if (session->sasl_mechanism_list == 0)
	msg_panic("%s: no mechanism list", myname);

    if (msg_verbose)
	msg_info("%s: %s: SASL mechanisms %s",
		 myname, session->namaddrport, session->sasl_mechanism_list);

    /*
     * Avoid repeated login failures after a recent 535 error.
     */
#ifdef HAVE_SASL_AUTH_CACHE
    if (smtp_sasl_auth_cache
	&& smtp_sasl_auth_cache_find(smtp_sasl_auth_cache, session)) {
	char   *resp_dsn = smtp_sasl_auth_cache_dsn(smtp_sasl_auth_cache);
	char   *resp_str = smtp_sasl_auth_cache_text(smtp_sasl_auth_cache);

	if (var_smtp_sasl_auth_soft_bounce && resp_dsn[0] == '5')
	    resp_dsn[0] = '4';
	dsb_update(why, resp_dsn, DSB_DEF_ACTION, DSB_MTYPE_DNS,
		   STR(iter->host), var_procname, resp_str,
		   "SASL [CACHED] authentication failed; server %s said: %s",
		   STR(iter->host), resp_str);
	return (0);
    }
#endif

    /*
     * Start the client side authentication protocol.
     */
    result = xsasl_client_first(session->sasl_client,
				session->sasl_mechanism_list,
				session->sasl_username,
				session->sasl_passwd,
				&mechanism, session->sasl_reply);
    if (result != XSASL_AUTH_OK) {
	dsb_update(why, "4.7.0", DSB_DEF_ACTION, DSB_SKIP_RMTA,
		   DSB_DTYPE_SASL, STR(session->sasl_reply),
		   "SASL authentication failed; "
		   "cannot authenticate to server %s: %s",
		   session->namaddr, STR(session->sasl_reply));
	return (-1);
    }
    /*-
     * Send the AUTH command and the optional initial client response.
     *
     * https://tools.ietf.org/html/rfc4954#page-4
     * Note that the AUTH command is still subject to the line length
     * limitations defined in [SMTP].  If use of the initial response argument
     * would cause the AUTH command to exceed this length, the client MUST NOT
     * use the initial response parameter...
     *
     * https://tools.ietf.org/html/rfc5321#section-4.5.3.1.4
     * The maximum total length of a command line including the command word
     * and the <CRLF> is 512 octets.
     *
     * Defer the initial response if the resulting command exceeds the limit.
     */
    if (LEN(session->sasl_reply) > 0
	&& strlen(mechanism) + LEN(session->sasl_reply) + 8 <= 512) {
	smtp_chat_cmd(session, "AUTH %s %s", mechanism,
		      STR(session->sasl_reply));
	VSTRING_RESET(session->sasl_reply);	/* no deferred initial reply */
    } else {
	smtp_chat_cmd(session, "AUTH %s", mechanism);
    }

    /*
     * Step through the authentication protocol until the server tells us
     * that we are done.  If session->sasl_reply is non-empty we have a
     * deferred initial reply and expect an empty initial challenge from the
     * server. If the server's initial challenge is non-empty we have a SASL
     * protocol violation with both sides wanting to go first.
     */
    while ((resp = smtp_chat_resp(session))->code / 100 == 3) {

	/*
	 * Sanity check.
	 */
	if (++steps > 100) {
	    dsb_simple(why, "4.3.0", "SASL authentication failed; "
		       "authentication protocol loop with server %s",
		       session->namaddr);
	    return (-1);
	}

	/*
	 * Process a server challenge.
	 */
	line = resp->str;
	(void) mystrtok(&line, "- \t\n");	/* skip over result code */

	if (LEN(session->sasl_reply) > 0) {

	    /*
	     * Deferred initial response, the server challenge must be empty.
	     * Cleared after actual transmission to the server.
	     */
	    if (*line) {
		dsb_update(why, "4.7.0", DSB_DEF_ACTION,
			   DSB_SKIP_RMTA, DSB_DTYPE_SASL, "protocol error",
			   "SASL authentication failed; non-empty initial "
			   "%s challenge from server %s: %s", mechanism,
			   session->namaddr, STR(session->sasl_reply));
		return (-1);
	    }
	} else {
	    result = xsasl_client_next(session->sasl_client, line,
				       session->sasl_reply);
	    if (result != XSASL_AUTH_OK) {
		dsb_update(why, "4.7.0", DSB_DEF_ACTION,	/* Fix 200512 */
		    DSB_SKIP_RMTA, DSB_DTYPE_SASL, STR(session->sasl_reply),
			   "SASL authentication failed; "
			   "cannot authenticate to server %s: %s",
			   session->namaddr, STR(session->sasl_reply));
		return (-1);			/* Fix 200512 */
	    }
	}

	/*
	 * Send a client response.
	 */
	smtp_chat_cmd(session, "%s", STR(session->sasl_reply));
	VSTRING_RESET(session->sasl_reply);	/* clear initial reply */
    }

    /*
     * We completed the authentication protocol.
     */
    if (resp->code / 100 != 2) {
#ifdef HAVE_SASL_AUTH_CACHE
	/* Update the 535 authentication failure cache. */
	if (smtp_sasl_auth_cache && resp->code == 535)
	    smtp_sasl_auth_cache_store(smtp_sasl_auth_cache, session, resp);
#endif
	if (var_smtp_sasl_auth_soft_bounce && resp->code / 100 == 5)
	    STR(resp->dsn_buf)[0] = '4';
	dsb_update(why, resp->dsn, DSB_DEF_ACTION,
		   DSB_MTYPE_DNS, STR(iter->host),
		   var_procname, resp->str,
		   "SASL authentication failed; server %s said: %s",
		   session->namaddr, resp->str);
	return (0);
    }
    return (1);
}

/* smtp_sasl_cleanup - per-session cleanup */

void    smtp_sasl_cleanup(SMTP_SESSION *session)
{
    if (session->sasl_username) {
	myfree(session->sasl_username);
	session->sasl_username = 0;
    }
    if (session->sasl_passwd) {
	myfree(session->sasl_passwd);
	session->sasl_passwd = 0;
    }
    if (session->sasl_mechanism_list) {
	/* allocated in smtp_sasl_helo_auth */
	myfree(session->sasl_mechanism_list);
	session->sasl_mechanism_list = 0;
    }
    if (session->sasl_client) {
	if (msg_verbose)
	    msg_info("disposing SASL state information");
	xsasl_client_free(session->sasl_client);
	session->sasl_client = 0;
    }
    if (session->sasl_reply) {
	vstring_free(session->sasl_reply);
	session->sasl_reply = 0;
    }
}

/* smtp_sasl_passivate - append serialized SASL attributes */

void    smtp_sasl_passivate(SMTP_SESSION *session, VSTRING *buf)
{
}

/* smtp_sasl_activate - de-serialize SASL attributes */

int     smtp_sasl_activate(SMTP_SESSION *session, char *buf)
{
    return (0);
}

#endif
