/*
* Copyright (c) 2004, Stefan Walter
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and
* the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * The names of contributors to this software may not be
* used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
*
* CONTRIBUTORS
* Stef Walter <stef@memberwebs.com>
*/
#include <sys/types.h>
#include <sys/param.h>
#include <sys/wait.h>
#include <ctype.h>
#include <stdio.h>
#include <unistd.h>
#include <syslog.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include "usuals.h"
#include "compat.h"
#include "sock_any.h"
#include "stringx.h"
#include "smtppass.h"
/* -----------------------------------------------------------------------
* STRUCTURES
*/
typedef struct pxstate
{
/* Settings ------------------------------- */
const char* command; /* The command to pipe email through */
struct timeval timeout; /* The command timeout */
int pipe_cmd; /* Whether command is a pipe or not */
const char* directory; /* The directory for temp files */
const char* header; /* Header to include in output */
}
pxstate_t;
/* -----------------------------------------------------------------------
* STRINGS
*/
#define REJECTED "Content Rejected"
#define DEFAULT_CONFIG CONF_PREFIX "/proxsmtpd.conf"
#define DEFAULT_TIMEOUT 30
#define CFG_FILTERCMD "FilterCommand"
#define CFG_FILTERTYPE "FilterType"
#define CFG_DIRECTORY "TempDirectory"
#define CFG_DEBUGFILES "DebugFiles"
#define CFG_CMDTIMEOUT "FilterTimeout"
#define CFG_HEADER "Header"
#define TYPE_PIPE "pipe"
#define TYPE_FILE "file"
/* Poll time for waiting operations in milli seconds */
#define POLL_TIME 20
/* read & write ends of a pipe */
#define READ_END 0
#define WRITE_END 1
/* pre-set file descriptors */
#define STDIN 0
#define STDOUT 1
#define STDERR 2
/* -----------------------------------------------------------------------
* GLOBALS
*/
pxstate_t g_pxstate;
/* -----------------------------------------------------------------------
* FORWARD DECLARATIONS
*/
static void usage();
static int process_file_command(spctx_t* sp);
static int process_pipe_command(spctx_t* sp);
static void final_reject_message(char* buf, int buflen);
static void buffer_reject_message(char* data, char* buf, int buflen);
static int kill_process(spctx_t* sp, pid_t pid);
static int wait_process(spctx_t* sp, pid_t pid, int* status);
/* ----------------------------------------------------------------------------------
* STARTUP ETC...
*/
#ifndef HAVE___ARGV
char** __argv;
#endif
int main(int argc, char* argv[])
{
const char* configfile = DEFAULT_CONFIG;
const char* pidfile = NULL;
int dbg_level = -1;
int ch = 0;
int r;
char* t;
#ifndef HAVE___ARGV
__argv = argv;
#endif
/* Setup some defaults */
memset(&g_pxstate, 0, sizeof(g_pxstate));
g_pxstate.directory = _PATH_TMP;
g_pxstate.pipe_cmd = 1;
g_pxstate.timeout.tv_sec = DEFAULT_TIMEOUT;
sp_init("proxsmtpd");
/*
* We still accept our old arguments for compatibility reasons.
* We fill them into the spstate structure directly
*/
/* Parse the arguments nicely */
while((ch = getopt(argc, argv, "d:f:p:v")) != -1)
{
switch(ch)
{
/* Don't daemonize */
case 'd':
dbg_level = strtol(optarg, &t, 10);
if(*t) /* parse error */
errx(1, "invalid debug log level");
dbg_level += LOG_ERR;
break;
/* The configuration file */
case 'f':
configfile = optarg;
break;
/* Write out a pid file */
case 'p':
pidfile = optarg;
break;
/* Print version number */
case 'v':
printf("proxsmtpd (version %s)\n", VERSION);
printf(" (config: %s)\n", DEFAULT_CONFIG);
exit(0);
break;
/* Usage information */
case '?':
default:
usage();
break;
}
}
argc -= optind;
argv += optind;
if(argc > 0)
usage();
r = sp_run(configfile, pidfile, dbg_level);
sp_done();
return r;
}
static void usage()
{
fprintf(stderr, "usage: proxsmtpd [-d debuglevel] [-f configfile] [-p pidfile]\n");
fprintf(stderr, " proxsmtpd -v\n");
exit(2);
}
/* ----------------------------------------------------------------------------------
* SP CALLBACKS
*/
int cb_check_data(spctx_t* ctx)
{
int r = 0;
if(!g_pxstate.command)
{
sp_messagex(ctx, LOG_WARNING, "no filter command specified. passing message through");
if(sp_cache_data(ctx) == -1 ||
sp_done_data(ctx, g_pxstate.header) == -1)
return -1; /* Message already printed */
return 0;
}
/* Cleanup any old filters hanging around */
while(waitpid(-1, &r, WNOHANG) > 0)
;
if(g_pxstate.pipe_cmd)
r = process_pipe_command(ctx);
else
r = process_file_command(ctx);
if(r == -1)
{
if(sp_fail_data(ctx, NULL) == -1)
return -1;
}
return 0;
}
int cb_parse_option(const char* name, const char* value)
{
char* t;
if(strcasecmp(CFG_FILTERCMD, name) == 0)
{
g_pxstate.command = value;
return 1;
}
else if(strcasecmp(CFG_DIRECTORY, name) == 0)
{
g_pxstate.directory = value;
return 1;
}
else if(strcasecmp(CFG_CMDTIMEOUT, name) == 0)
{
g_pxstate.timeout.tv_sec = strtol(value, &t, 10);
if(*t || g_pxstate.timeout.tv_sec <= 0)
errx(2, "invalid setting: " CFG_CMDTIMEOUT);
return 1;
}
else if(strcasecmp(CFG_FILTERTYPE, name) == 0)
{
if(strcasecmp(value, TYPE_PIPE) == 0)
g_pxstate.pipe_cmd = 1;
else if(strcasecmp(value, TYPE_FILE) == 0)
g_pxstate.pipe_cmd = 0;
else
errx(2, "invalid value for " CFG_FILTERTYPE " (must specify 'pipe' or 'file')");
return 1;
}
else if(strcasecmp(CFG_HEADER, name) == 0)
{
g_pxstate.header = trim_start(value);
if(strlen(g_pxstate.header) == 0)
g_pxstate.header = NULL;
return 1;
}
return 0;
}
spctx_t* cb_new_context()
{
spctx_t* ctx = (spctx_t*)calloc(1, sizeof(spctx_t));
if(!ctx)
sp_messagex(NULL, LOG_CRIT, "out of memory");
return ctx;
}
void cb_del_context(spctx_t* ctx)
{
free(ctx);
}
/* -----------------------------------------------------------------------------
* IMPLEMENTATION
*/
static void kill_myself()
{
while (1) {
kill(getpid(), SIGKILL);
sleep(1);
}
}
static pid_t fork_filter(spctx_t* sp, int* infd, int* outfd, int* errfd)
{
pid_t pid;
int ret = 0;
int r = 0;
/* Pipes for input, output, err */
int pipe_i[2];
int pipe_o[2];
int pipe_e[2];
memset(pipe_i, ~0, sizeof(pipe_i));
memset(pipe_o, ~0, sizeof(pipe_o));
memset(pipe_e, ~0, sizeof(pipe_e));
ASSERT(g_pxstate.command);
/* Create the pipes we need */
if((infd && pipe(pipe_i) == -1) ||
(outfd && pipe(pipe_o) == -1) ||
(errfd && pipe(pipe_e) == -1))
{
sp_message(sp, LOG_ERR, "couldn't create pipe for filter command");
RETURN(-1);
}
/* Now fork the pipes across processes */
switch(pid = fork())
{
case -1:
sp_message(sp, LOG_ERR, "couldn't fork for filter command");
RETURN(-1);
/* The child process */
case 0:
if(r >= 0 && infd)
{
close(pipe_i[WRITE_END]);
r = dup2(pipe_i[READ_END], STDIN);
close(pipe_i[READ_END]);
}
if(r >= 0 && outfd)
{
close(pipe_o[READ_END]);
r = dup2(pipe_o[WRITE_END], STDOUT);
close(pipe_o[WRITE_END]);
}
if(r >= 0 && errfd)
{
close(pipe_e[READ_END]);
r = dup2(pipe_e[WRITE_END], STDERR);
close(pipe_e[WRITE_END]);
}
if(r < 0)
{
sp_message(sp, LOG_ERR, "couldn't dup descriptors for filter command");
kill_myself();
}
/* All the necessary environment vars */
sp_setup_forked(sp, 1);
/* Now run the filter command */
execl("/bin/sh", "sh", "-c", g_pxstate.command, NULL);
/* If that returned then there was an error */
sp_message(sp, LOG_ERR, "error executing the shell for filter command");
kill_myself();
break;
};
/* The parent process */
sp_messagex(sp, LOG_DEBUG, "executed filter command: %s (pid: %d)", g_pxstate.command, (int)pid);
/* Setup all our return values */
if(infd)
{
*infd = pipe_i[WRITE_END];
pipe_i[WRITE_END] = -1;
fcntl(*infd, F_SETFL, fcntl(*infd, F_GETFL, 0) | O_NONBLOCK);
}
if(outfd)
{
*outfd = pipe_o[READ_END];
pipe_o[READ_END] = -1;
fcntl(*outfd, F_SETFL, fcntl(*outfd, F_GETFL, 0) | O_NONBLOCK);
}
if(errfd)
{
*errfd = pipe_e[READ_END];
pipe_e[READ_END] = -1;
fcntl(*errfd, F_SETFL, fcntl(*errfd, F_GETFL, 0) | O_NONBLOCK);
}
cleanup:
if(pipe_i[READ_END] != -1)
close(pipe_i[READ_END]);
if(pipe_i[WRITE_END] != -1)
close(pipe_i[WRITE_END]);
if(pipe_o[READ_END] != -1)
close(pipe_o[READ_END]);
if(pipe_o[WRITE_END] != -1)
close(pipe_o[WRITE_END]);
if(pipe_e[READ_END] != -1)
close(pipe_e[READ_END]);
if(pipe_e[WRITE_END] != -1)
close(pipe_e[WRITE_END]);
return ret >= 0 ? pid : (pid_t)-1;
}
static int process_file_command(spctx_t* sp)
{
pid_t pid;
int ret = 0, status, r;
struct timeval timeout;
/* For reading data from the process */
int errfd;
fd_set rmask;
char obuf[1024];
char ebuf[256];
memset(ebuf, 0, sizeof(ebuf));
if(sp_cache_data(sp) == -1)
RETURN(-1); /* message already printed */
pid = fork_filter(sp, NULL, NULL, &errfd);
if(pid == (pid_t)-1)
RETURN(-1);
/* Main read write loop */
while(errfd != -1)
{
FD_ZERO(&rmask);
FD_SET(errfd, &rmask);
/* Select can modify the timeout argument so we copy */
memcpy(&timeout, &(g_pxstate.timeout), sizeof(timeout));
r = select(FD_SETSIZE, &rmask, NULL, NULL, &timeout);
switch(r)
{
case -1:
sp_message(sp, LOG_ERR, "couldn't select while listening to filter command");
RETURN(-1);
case 0:
sp_messagex(sp, LOG_ERR, "timeout while listening to filter command");
RETURN(-1);
};
ASSERT(FD_ISSET(errfd, &rmask));
/* Note because we handle as string we save one byte for null-termination */
r = read(errfd, obuf, sizeof(obuf) - 1);
if(r < 0)
{
if(errno != EINTR && errno != EAGAIN)
{
sp_message(sp, LOG_ERR, "couldn't read data from filter command");
RETURN(-1);
}
continue;
}
if(r == 0)
{
close(errfd);
errfd = -1;
break;
}
/* Null terminate */
obuf[r] = 0;
/* And process */
buffer_reject_message(obuf, ebuf, sizeof(ebuf));
if(sp_is_quit())
RETURN(-1);
}
/* exit the process if not completed */
if(wait_process(sp, pid, &status) == -1)
{
sp_messagex(sp, LOG_ERR, "timeout waiting for filter command to exit");
RETURN(-1);
}
pid = 0;
/* We only trust well behaved programs */
if(!WIFEXITED(status))
{
sp_messagex(sp, LOG_ERR, "filter command terminated abnormally");
RETURN(-1);
}
sp_messagex(sp, LOG_DEBUG, "filter exit code: %d", (int)WEXITSTATUS(status));
/* A successful response */
if(WEXITSTATUS(status) == 0)
{
if(sp_done_data(sp, g_pxstate.header) == -1)
RETURN(-1); /* message already printed */
sp_add_log(sp, "status=", "FILTERED");
}
/* Check code and use stderr if bad code */
else
{
final_reject_message(ebuf, sizeof(ebuf));
if(sp_fail_data(sp, ebuf) == -1)
RETURN(-1); /* message already printed */
sp_add_log(sp, "status=", ebuf);
}
ret = 0;
cleanup:
if(pid != 0)
{
sp_messagex(sp, LOG_WARNING, "killing filter process (pid %d)", (int)pid);
kill_process(sp, pid);
}
if(errfd != -1)
close(errfd);
if(ret < 0)
sp_add_log(sp, "status=", "FILTER-ERROR");
return ret;
}
static int process_pipe_command(spctx_t* sp)
{
pid_t pid;
int ret = 0, status, r;
struct timeval timeout;
/* For sending data to the process */
const char* ibuf = NULL;
int ilen = 0;
int infd;
int icount = 0;
fd_set wmask;
/* For reading data from the process */
int outfd;
int errfd;
fd_set rmask;
char obuf[1024];
char ebuf[256];
int ocount = 0;
ASSERT(g_pxstate.command);
memset(ebuf, 0, sizeof(ebuf));
pid = fork_filter(sp, &infd, &outfd, &errfd);
if(pid == (pid_t)-1)
RETURN(-1);
/* Opens cache file */
if(sp_write_data(sp, obuf, 0) == -1)
RETURN(-1); /* message already printed */
/* Main read write loop */
while(infd != -1 || outfd != -1 || errfd != -1)
{
FD_ZERO(&rmask);
FD_ZERO(&wmask);
/* We only select on those that are still open */
if(infd != -1)
FD_SET(infd, &wmask);
if(outfd != -1)
FD_SET(outfd, &rmask);
if(errfd != -1)
FD_SET(errfd, &rmask);
/* Select can modify the timeout argument so we copy */
memcpy(&timeout, &(g_pxstate.timeout), sizeof(timeout));
r = select(FD_SETSIZE, &rmask, &wmask, NULL, &timeout);
switch(r)
{
case -1:
sp_message(sp, LOG_ERR, "couldn't select while listening to filter command");
RETURN(-1);
case 0:
sp_messagex(sp, LOG_WARNING, "timeout while listening to filter command");
RETURN(-1);
};
/* Handling of process's stdin */
if(infd != -1 && FD_ISSET(infd, &wmask))
{
if(ilen <= 0)
{
/* Read some more data into buffer */
switch(r = sp_read_data(sp, &ibuf))
{
case -1:
RETURN(-1); /* Message already printed */
case 0:
close(infd); /* Done with the input */
infd = -1;
break;
default:
ASSERT(r > 0);
ilen = r;
break;
};
}
if(ilen > 0)
{
/* Write data from buffer */
r = write(infd, ibuf, ilen);
if(r == -1)
{
if(errno == EPIPE)
{
sp_messagex(sp, LOG_INFO, "filter command closed input early");
/* Eat up the rest of the data */
while(sp_read_data(sp, &ibuf) > 0)
;
close(infd);
infd = -1;
}
else if(errno != EAGAIN && errno != EINTR)
{
/* Otherwise it's a normal error */
sp_message(sp, LOG_ERR, "couldn't write to filter command");
RETURN(-1);
}
}
/* A good normal write */
else
{
icount += r;
ilen -= r;
ibuf += r;
}
}
}
/* Handling of stdout, which should be email data */
if(outfd != -1 && FD_ISSET(outfd, &rmask))
{
r = read(outfd, obuf, sizeof(obuf));
if(r > 0)
{
if(sp_write_data(sp, obuf, r) == -1)
RETURN(-1); /* message already printed */
ocount += r;
}
else if(r == 0)
{
close(outfd);
outfd = -1;
}
else if(r < 0)
{
if(errno != EINTR && errno != EAGAIN)
{
sp_message(sp, LOG_ERR, "couldn't read data from filter command");
RETURN(-1);
}
}
}
/* Handling of stderr, the last line of which we use as an err message*/
if(errfd != -1 && FD_ISSET(errfd, &rmask))
{
/* Note because we handle as string we save one byte for null-termination */
r = read(errfd, obuf, sizeof(obuf) - 1);
if(r < 0)
{
if(errno != EINTR && errno != EAGAIN)
{
sp_message(sp, LOG_ERR, "couldn't read data from filter command");
RETURN(-1);
}
}
else if(r == 0)
{
close(errfd);
errfd = -1;
}
else if(r > 0)
{
/* Null terminate */
obuf[r] = 0;
/* And process */
buffer_reject_message(obuf, ebuf, sizeof(ebuf));
}
}
if(sp_is_quit())
RETURN(-1);
}
sp_messagex(sp, LOG_DEBUG, "wrote %d bytes to filter, read %d bytes", icount, ocount);
/* Close the cache file */
if(sp_write_data(sp, NULL, 0) == -1)
RETURN(-1); /* message already printed */
if(wait_process(sp, pid, &status) == -1)
{
sp_messagex(sp, LOG_ERR, "timeout waiting for filter command to exit");
RETURN(-1);
}
pid = 0;
/* We only trust well behaved programs */
if(!WIFEXITED(status))
{
sp_messagex(sp, LOG_ERR, "filter command terminated abnormally");
RETURN(-1);
}
sp_messagex(sp, LOG_DEBUG, "filter exit code: %d", (int)WEXITSTATUS(status));
/* A successful response */
if(WEXITSTATUS(status) == 0)
{
if(sp_done_data(sp, g_pxstate.header) == -1)
RETURN(-1); /* message already printed */
sp_add_log(sp, "status=", "FILTERED");
}
/* Check code and use stderr if bad code */
else
{
final_reject_message(ebuf, sizeof(ebuf));
if(sp_fail_data(sp, ebuf) == -1)
RETURN(-1); /* message already printed */
sp_add_log(sp, "status=", ebuf);
}
ret = 0;
cleanup:
if(infd != -1)
close(infd);
if(outfd != -1)
close(outfd);
if(errfd != -1)
close(errfd);
if(pid != 0)
{
sp_messagex(sp, LOG_WARNING, "killing filter process (pid %d)", (int)pid);
kill_process(sp, pid);
}
if(ret < 0)
sp_add_log(sp, "status=", "FILTER-ERROR");
return ret;
}
static void final_reject_message(char* buf, int buflen)
{
if(buf[0] == 0)
strlcpy(buf, REJECTED, buflen);
else
trim_end(buf);
}
static void buffer_reject_message(char* data, char* buf, int buflen)
{
int len = strlen(data);
char* t = data + len;
int newline = 0;
while(t > data && isspace(*(t - 1)))
{
t--;
if(*t == '\n')
newline = 1;
}
/* No valid line */
if(t > data)
{
if(newline)
*t = 0;
t = strrchr(data, '\n');
if(t == NULL)
{
t = trim_start(data);
/*
* Basically if we already have a newline at the end
* then we need to start a new line
*/
if(buf[strlen(buf) - 1] == '\n')
buf[0] = 0;
}
else
{
t = trim_start(t);
/* Start a new line */
buf[0] = 0;
}
/* t points to a valid line */
strlcat(buf, t, buflen);
}
/* Always append if we found a newline */
if(newline)
strlcat(buf, "\n", buflen);
}
static int wait_process(spctx_t* sp, pid_t pid, int* status)
{
/* We poll x times a second */
int waits = g_pxstate.timeout.tv_sec * (1000 / POLL_TIME);
*status = 0;
while(waits > 0)
{
switch(waitpid(pid, status, WNOHANG))
{
case 0:
break;
case -1:
if(errno != ECHILD && errno != ESRCH)
{
sp_message(sp, LOG_CRIT, "error waiting on process");
return -1;
}
/* fall through */
default:
return 0;
}
usleep(POLL_TIME * 1000);
waits--;
}
return -1;
}
static int kill_process(spctx_t* sp, pid_t pid)
{
int status;
if(kill(pid, SIGTERM) == -1)
{
if(errno == ESRCH)
return 0;
sp_message(sp, LOG_ERR, "couldn't send signal to process");
return -1;
}
if(wait_process(sp, pid, &status) == -1)
{
if(kill(pid, SIGKILL) == -1)
{
if(errno == ESRCH)
return 0;
sp_message(sp, LOG_ERR, "couldn't send signal to process");
return -1;
}
sp_messagex(sp, LOG_ERR, "process wouldn't quit. forced termination");
}
return 0;
}
syntax highlighted by Code2HTML, v. 0.9.1