Blob Blame History Raw
/*
 * Soft:        Keepalived is a failover program for the LVS project
 *              <www.linuxvirtualserver.org>. It monitor & manipulate
 *              a loadbalanced server pool using multi-layer checks.
 *
 * Part:        SMTP WRAPPER connect to a specified smtp server and send mail
 *              using the smtp protocol according to the RFC 821. A non blocking
 *              timeouted connection is used to handle smtp protocol.
 *
 * Author:      Alexandre Cassen, <acassen@linux-vs.org>
 *
 *              This program is distributed in the hope that it will be useful,
 *              but WITHOUT ANY WARRANTY; without even the implied warranty of
 *              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *              See the GNU General Public License for more details.
 *
 *              This program is free software; you can redistribute it and/or
 *              modify it under the terms of the GNU General Public License
 *              as published by the Free Software Foundation; either version
 *              2 of the License, or (at your option) any later version.
 *
 * Copyright (C) 2001-2017 Alexandre Cassen, <acassen@gmail.com>
 */

#include "config.h"

#include <errno.h>
#include <unistd.h>
#include <time.h>

#include "smtp.h"
#include "memory.h"
#include "layer4.h"
#include "logger.h"
#include "utils.h"
#if !HAVE_DECL_SOCK_CLOEXEC
#include "old_socket.h"
#endif
#ifdef _WITH_LVS_
#include "check_api.h"
#endif
#ifdef THREAD_DUMP
#include "scheduler.h"
#endif
#ifdef _SMTP_ALERT_DEBUG_
bool do_smtp_alert_debug;
#endif

/* SMTP FSM definition */
static int connection_error(thread_t *);
static int connection_in_progress(thread_t *);
static int connection_timeout(thread_t *);
static int connection_success(thread_t *);
static int helo_cmd(thread_t *);
static int mail_cmd(thread_t *);
static int rcpt_cmd(thread_t *);
static int data_cmd(thread_t *);
static int body_cmd(thread_t *);
static int quit_cmd(thread_t *);

static int connection_code(thread_t *, int);
static int helo_code(thread_t *, int);
static int mail_code(thread_t *, int);
static int rcpt_code(thread_t *, int);
static int data_code(thread_t *, int);
static int body_code(thread_t *, int);
static int quit_code(thread_t *, int);

static int smtp_read_thread(thread_t *);
static int smtp_send_thread(thread_t *);

struct {
	int (*send) (thread_t *);
	int (*read) (thread_t *, int);
} SMTP_FSM[SMTP_MAX_FSM_STATE] = {
/*      Stream Write Handlers    |   Stream Read handlers   *
 *-------------------------------+--------------------------*/
	{connection_error,		NULL},			/* connect_error */
	{connection_in_progress,	NULL},			/* connect_in_progress */
	{connection_timeout,		NULL},			/* connect_timeout */
	{connection_success,		connection_code},	/* connect_success */
	{helo_cmd,			helo_code},		/* HELO */
	{mail_cmd,			mail_code},		/* MAIL */
	{rcpt_cmd,			rcpt_code},		/* RCPT */
	{data_cmd,			data_code},		/* DATA */
	{body_cmd,			body_code},		/* BODY */
	{quit_cmd,			quit_code}		/* QUIT */
};

static void
free_smtp_all(smtp_t * smtp)
{
	FREE(smtp->buffer);
	FREE(smtp->subject);
	FREE(smtp->body);
	FREE(smtp->email_to);
	FREE(smtp);
}

static char *
fetch_next_email(smtp_t * smtp)
{
	return list_element(global_data->email, smtp->email_it);
}

/* layer4 connection handlers */
static int
connection_error(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	log_message(LOG_INFO, "SMTP connection ERROR to %s."
			    , FMT_SMTP_HOST());
	free_smtp_all(smtp);
	return 0;
}
static int
connection_timeout(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	log_message(LOG_INFO, "Timeout connecting SMTP server %s."
			    , FMT_SMTP_HOST());
	free_smtp_all(smtp);
	return 0;
}
static int
connection_in_progress(thread_t * thread)
{
	int status;

	DBG("SMTP connection to %s now IN_PROGRESS.",
	    FMT_SMTP_HOST());

	/*
	 * Here we use the propriety of a union structure,
	 * each element of the structure have the same value.
	 */
	status = tcp_socket_state(thread, connection_in_progress);

	if (status != connect_in_progress)
		SMTP_FSM_SEND(status, thread);

	return 0;
}
static int
connection_success(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	log_message(LOG_INFO, "Remote SMTP server %s connected."
			    , FMT_SMTP_HOST());

	smtp->stage = connect_success;
	thread_add_read(thread->master, smtp_read_thread, smtp,
			smtp->fd, global_data->smtp_connection_to);
	return 0;
}

/* SMTP protocol handlers */
static int
smtp_read_thread(thread_t * thread)
{
	smtp_t *smtp;
	char *buffer;
	char *reply;
	ssize_t rcv_buffer_size;
	int status = -1;

	smtp = THREAD_ARG(thread);

	if (thread->type == THREAD_READ_TIMEOUT) {
		log_message(LOG_INFO, "Timeout reading data to remote SMTP server %s."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
		return -1;
	}

	buffer = smtp->buffer;

	rcv_buffer_size = read(thread->u.fd, buffer + smtp->buflen,
			       SMTP_BUFFER_LENGTH - smtp->buflen);

	if (rcv_buffer_size == -1) {
		if (errno == EAGAIN)
			goto end;
		log_message(LOG_INFO, "Error reading data from remote SMTP server %s."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
		return 0;
	} else if (rcv_buffer_size == 0) {
		log_message(LOG_INFO, "Remote SMTP server %s has closed the connection."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
		return 0;
	}

	/* received data overflow buffer size ? */
	if (smtp->buflen >= SMTP_BUFFER_MAX) {
		log_message(LOG_INFO, "Received buffer from remote SMTP server %s"
				      " overflow our get read buffer length."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
		return 0;
	} else {
		smtp->buflen += (size_t)rcv_buffer_size;
		buffer[smtp->buflen] = 0;	/* NULL terminate */
	}

      end:

	/* parse the buffer, finding the last line of the response for the code */
	reply = buffer;
	while (reply < buffer + smtp->buflen) {
		char *p;

		p = strstr(reply, "\r\n");
		if (!p) {
			memmove(buffer, reply,
				smtp->buflen - (size_t)(reply - buffer));
			smtp->buflen -= (size_t)(reply - buffer);
			buffer[smtp->buflen] = 0;

			thread_add_read(thread->master, smtp_read_thread,
					smtp, thread->u.fd,
					global_data->smtp_connection_to);
			return 0;
		}

		if (reply[3] == '-') {
			/* Skip over the \r\n */
			reply = p + 2;
			continue;
		}

		status = ((reply[0] - '0') * 100) + ((reply[1] - '0') * 10) + (reply[2] - '0');

		reply = p + 2;
		break;
	}

	memmove(buffer, reply, smtp->buflen - (size_t)(reply - buffer));
	smtp->buflen -= (size_t)(reply - buffer);
	buffer[smtp->buflen] = 0;

	if (status == -1) {
		thread_add_read(thread->master, smtp_read_thread, smtp,
				thread->u.fd, global_data->smtp_connection_to);
		return 0;
	}

	SMTP_FSM_READ(smtp->stage, thread, status);

	/* Registering next smtp command processing thread */
	if (smtp->stage != ERROR) {
		thread_add_write(thread->master, smtp_send_thread, smtp,
				 smtp->fd, global_data->smtp_connection_to);
	} else {
		log_message(LOG_INFO, "Can not read data from remote SMTP server %s."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
	}

	return 0;
}

static int
smtp_send_thread(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (thread->type == THREAD_WRITE_TIMEOUT) {
		log_message(LOG_INFO, "Timeout sending data to remote SMTP server %s."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
		return 0;
	}

	SMTP_FSM_SEND(smtp->stage, thread);

	/* Handle END command */
	if (smtp->stage == END) {
		SMTP_FSM_READ(QUIT, thread, 0);
		return 0;
	}

	/* Registering next smtp command processing thread */
	if (smtp->stage != ERROR) {
		thread_add_read(thread->master, smtp_read_thread, smtp,
				thread->u.fd, global_data->smtp_connection_to);
		thread_del_write(thread);
	} else {
		log_message(LOG_INFO, "Can not send data to remote SMTP server %s."
				    , FMT_SMTP_HOST());
		SMTP_FSM_READ(QUIT, thread, 0);
	}

	return 0;
}

static int
connection_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (status == 220) {
		smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error connecting SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* HELO command processing */
static int
helo_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);
	char *buffer;

	buffer = (char *) MALLOC(SMTP_BUFFER_MAX);
	snprintf(buffer, SMTP_BUFFER_MAX, SMTP_HELO_CMD, (global_data->smtp_helo_name) ? global_data->smtp_helo_name : "localhost");
	if (send(thread->u.fd, buffer, strlen(buffer), 0) == -1)
		smtp->stage = ERROR;
	FREE(buffer);

	return 0;
}
static int
helo_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (status == 250) {
		smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error processing HELO cmd on SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* MAIL command processing */
static int
mail_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);
	char *buffer;

	buffer = (char *) MALLOC(SMTP_BUFFER_MAX);
	snprintf(buffer, SMTP_BUFFER_MAX, SMTP_MAIL_CMD, global_data->email_from);
	if (send(thread->u.fd, buffer, strlen(buffer), 0) == -1)
		smtp->stage = ERROR;
	FREE(buffer);

	return 0;
}
static int
mail_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (status == 250) {
		smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error processing MAIL cmd on SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* RCPT command processing */
static int
rcpt_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);
	char *buffer;
	char *fetched_email;

	buffer = (char *) MALLOC(SMTP_BUFFER_MAX);
	/* We send RCPT TO command multiple time to add all our email receivers.
	 * --rfc821.3.1
	 */
	fetched_email = fetch_next_email(smtp);

	snprintf(buffer, SMTP_BUFFER_MAX, SMTP_RCPT_CMD, fetched_email);
	if (send(thread->u.fd, buffer, strlen(buffer), 0) == -1)
		smtp->stage = ERROR;
	FREE(buffer);

	return 0;
}
static int
rcpt_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);
	char *fetched_email;

	if (status == 250) {
		smtp->email_it++;

		fetched_email = fetch_next_email(smtp);

		if (!fetched_email)
			smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error processing RCPT cmd on SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* DATA command processing */
static int
data_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (send(thread->u.fd, SMTP_DATA_CMD, strlen(SMTP_DATA_CMD), 0) == -1)
		smtp->stage = ERROR;
	return 0;
}
static int
data_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (status == 354) {
		smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error processing DATA cmd on SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* BODY command processing.
 * Do we need to use mutli-thread for multi-part body
 * handling ? Don t really think :)
 */
static int
body_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);
	char *buffer;
	char rfc822[80];
	time_t now;
	struct tm *t;

	buffer = (char *) MALLOC(SMTP_BUFFER_MAX);

	time(&now);
	t = localtime(&now);
	strftime(rfc822, sizeof(rfc822), "%a, %d %b %Y %H:%M:%S %z", t);

	snprintf(buffer, SMTP_BUFFER_MAX, SMTP_HEADERS_CMD,
		 rfc822, global_data->email_from, smtp->subject, smtp->email_to);

	/* send the subject field */
	if (send(thread->u.fd, buffer, strlen(buffer), 0) == -1)
		smtp->stage = ERROR;

	memset(buffer, 0, SMTP_BUFFER_MAX);
	snprintf(buffer, SMTP_BUFFER_MAX, SMTP_BODY_CMD, smtp->body);

	/* send the the body field */
	if (send(thread->u.fd, buffer, strlen(buffer), 0) == -1)
		smtp->stage = ERROR;

	/* send the sending dot */
	if (send(thread->u.fd, SMTP_SEND_CMD, strlen(SMTP_SEND_CMD), 0) == -1)
		smtp->stage = ERROR;

	FREE(buffer);
	return 0;
}
static int
body_code(thread_t * thread, int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (status == 250) {
		log_message(LOG_INFO, "SMTP alert successfully sent.");
		smtp->stage++;
	} else {
		log_message(LOG_INFO, "Error processing DOT cmd on SMTP server %s."
				      " SMTP status code = %d"
				    , FMT_SMTP_HOST()
				    , status);
		smtp->stage = ERROR;
	}

	return 0;
}

/* QUIT command processing */
static int
quit_cmd(thread_t * thread)
{
	smtp_t *smtp = THREAD_ARG(thread);

	if (send(thread->u.fd, SMTP_QUIT_CMD, strlen(SMTP_QUIT_CMD), 0) == -1)
		smtp->stage = ERROR;
	else
		smtp->stage++;
	return 0;
}

static int
quit_code(thread_t * thread, __attribute__((unused)) int status)
{
	smtp_t *smtp = THREAD_ARG(thread);

	/* final state, we are disconnected from the remote host */
	free_smtp_all(smtp);
	thread_close_fd(thread);
	return 0;
}

/* connect remote SMTP server */
static void
smtp_connect(smtp_t * smtp)
{
	enum connect_result status;

	if ((smtp->fd = socket(global_data->smtp_server.ss_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_TCP)) == -1) {
		DBG("SMTP connect fail to create socket.");
		free_smtp_all(smtp);
		return;
	}

#if !HAVE_DECL_SOCK_NONBLOCK
	if (set_sock_flags(smtp->fd, F_SETFL, O_NONBLOCK))
		log_message(LOG_INFO, "Unable to set NONBLOCK on smtp_connect socket - %s (%d)", strerror(errno), errno);
#endif

#if !HAVE_DECL_SOCK_CLOEXEC
	if (set_sock_flags(smtp->fd, F_SETFD, FD_CLOEXEC))
		log_message(LOG_INFO, "Unable to set CLOEXEC on smtp_connect socket - %s (%d)", strerror(errno), errno);
#endif

	status = tcp_connect(smtp->fd, &global_data->smtp_server);

	/* Handle connection status code */
	thread_add_event(master, SMTP_FSM[status].send, smtp, smtp->fd);
}

#ifdef _SMTP_ALERT_DEBUG_
static void
smtp_log_to_file(smtp_t *smtp)
{
	FILE *fp = fopen_safe("/tmp/smtp-alert.log", "a");
	time_t now;
	struct tm tm;
	char time_buf[25];
	int time_buf_len;

	time(&now);
	localtime_r(&now, &tm);
	time_buf_len = strftime(time_buf, sizeof time_buf, "%a %b %e %X %Y", &tm);

	fprintf(fp, "%s: %s -> %s\n"
		    "%*sSubject: %s\n"
		    "%*sBody:    %s\n\n",
		    time_buf, global_data->email_from, smtp->email_to,
		    time_buf_len - 7, "", smtp->subject,
		    time_buf_len - 7, "", smtp->body);

	fclose(fp);

	free_smtp_all(smtp);
}
#endif

/*
 * Build a comma separated string of smtp recipient email addresses
 * for the email message To-header.
 */
static void
build_to_header_rcpt_addrs(smtp_t *smtp)
{
	char *fetched_email;
	char *email_to_addrs;
	size_t bytes_available = SMTP_BUFFER_MAX - 1;
	size_t bytes_to_write;

	if (smtp == NULL)
		return;

	email_to_addrs = smtp->email_to;
	smtp->email_it = 0;

	while (1) {
		fetched_email = fetch_next_email(smtp);
		if (fetched_email == NULL)
			break;

		bytes_to_write = strlen(fetched_email);
		if (!smtp->email_it) {
			if (bytes_available < bytes_to_write)
				break;
		} else {
			if (bytes_available < 2 + bytes_to_write)
				break;

			/* Prepend with a comma and space to all non-first email addresses */
			*email_to_addrs++ = ',';
			*email_to_addrs++ = ' ';
			bytes_available -= 2;
		}

		if (snprintf(email_to_addrs, bytes_to_write + 1, "%s", fetched_email) != (int)bytes_to_write) {
			/* Inconsistent state, no choice but to break here and do nothing */
			break;
		}

		email_to_addrs += bytes_to_write;
		bytes_available -= bytes_to_write;
		smtp->email_it++;
	}

	smtp->email_it = 0;
}

/* Main entry point */
void
smtp_alert(smtp_msg_t msg_type, void* data, const char *subject, const char *body)
{
	smtp_t *smtp;
#ifdef _WITH_VRRP_
	vrrp_t *vrrp;
	vrrp_sgroup_t *vgroup;
#endif
#ifdef _WITH_LVS_
	checker_t *checker;
	virtual_server_t *vs;
	smtp_rs *rs_info;
#endif

	/* Only send mail if email specified */
	if (LIST_ISEMPTY(global_data->email) || !global_data->smtp_server.ss_family)
		return;

	/* allocate & initialize smtp argument data structure */
	smtp = (smtp_t *) MALLOC(sizeof(smtp_t));
	smtp->subject = (char *) MALLOC(MAX_HEADERS_LENGTH);
	smtp->body = (char *) MALLOC(MAX_BODY_LENGTH);
	smtp->buffer = (char *) MALLOC(SMTP_BUFFER_MAX);
	smtp->email_to = (char *) MALLOC(SMTP_BUFFER_MAX);

	/* format subject if rserver is specified */
#ifdef _WITH_LVS_
	if (msg_type == SMTP_MSG_RS) {
		checker = (checker_t *)data;
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] Realserver %s of virtual server %s - %s",
					global_data->router_id,
					FMT_RS(checker->rs, checker->vs),
					FMT_VS(checker->vs),
					checker->rs->alive ? "UP" : "DOWN");
	}
	else if (msg_type == SMTP_MSG_VS) {
		vs = (virtual_server_t *)data;
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] Virtualserver %s - %s",
					global_data->router_id,
					FMT_VS(vs),
					subject);
	}
	else if (msg_type == SMTP_MSG_RS_SHUT) {
		rs_info = (smtp_rs *)data;
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] Realserver %s of virtual server %s - %s",
					global_data->router_id,
					FMT_RS(rs_info->rs, rs_info->vs),
					FMT_VS(rs_info->vs),
					subject);
	}
	else
#endif
#ifdef _WITH_VRRP_
	if (msg_type == SMTP_MSG_VRRP) {
		vrrp = (vrrp_t *)data;
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] VRRP Instance %s - %s",
					global_data->router_id,
					vrrp->iname,
					subject);
	} else if (msg_type == SMTP_MSG_VGROUP) {
		vgroup = (vrrp_sgroup_t *)data;
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] VRRP Group %s - %s",
					global_data->router_id,
					vgroup->gname,
					subject);
	}
	else
#endif
	if (global_data->router_id)
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "[%s] %s"
				      , global_data->router_id
				      , subject);
	else
		snprintf(smtp->subject, MAX_HEADERS_LENGTH, "%s", subject);

	strncpy(smtp->body, body, MAX_BODY_LENGTH - 1);
	smtp->body[MAX_BODY_LENGTH - 1]= '\0';

	build_to_header_rcpt_addrs(smtp);

#ifdef _SMTP_ALERT_DEBUG_
	if (do_smtp_alert_debug)
		smtp_log_to_file(smtp);
	else
#endif
	smtp_connect(smtp);
}

#ifdef THREAD_DUMP
void
register_smtp_addresses(void)
{
	register_thread_address("body_cmd", body_cmd);
	register_thread_address("connection_error", connection_error);
	register_thread_address("connection_in_progress", connection_in_progress);
	register_thread_address("connection_success", connection_success);
	register_thread_address("connection_timeout", connection_timeout);
	register_thread_address("data_cmd", data_cmd);
	register_thread_address("helo_cmd", helo_cmd);
	register_thread_address("mail_cmd", mail_cmd);
	register_thread_address("quit_cmd", quit_cmd);
	register_thread_address("rcpt_cmd", rcpt_cmd);
	register_thread_address("smtp_read_thread", smtp_read_thread);
	register_thread_address("smtp_send_thread", smtp_send_thread);
}
#endif