From 0dc7245bcee8971a5fc1a79c3b9c9781ca848198 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Tue, 14 Sep 2004 18:08:53 +0000 Subject: Initial build --- src/.cvsignore | 4 + src/proxsmtpd.c | 872 +++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 549 insertions(+), 327 deletions(-) create mode 100644 src/.cvsignore (limited to 'src') diff --git a/src/.cvsignore b/src/.cvsignore new file mode 100644 index 0000000..0564b29 --- /dev/null +++ b/src/.cvsignore @@ -0,0 +1,4 @@ +Makefile +Makefile.in +proxsmtpd +.deps diff --git a/src/proxsmtpd.c b/src/proxsmtpd.c index 897faa9..3540dd2 100644 --- a/src/proxsmtpd.c +++ b/src/proxsmtpd.c @@ -37,6 +37,7 @@ #include #include +#include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include #include "usuals.h" @@ -61,10 +63,9 @@ typedef struct pxstate { /* Settings ------------------------------- */ const char* command; /* The command to pipe email through */ - /* TODO: Timeout for above command */ + struct timeval timeout; /* The command timeout */ + int pipe_cmd; /* Whether command is a pipe or not */ const char* directory; /* The directory for temp files */ - int quarantine; /* Leave failed files in temp dir */ - int debug_files; /* Leave all files in temp dir */ } pxstate_t; @@ -72,39 +73,43 @@ pxstate_t; * STRINGS */ -#define CRLF "\r\n" +#define SMTP_REJECTED "550 Content Rejected\r\n" +#define DEFAULT_CONFIG CONF_PREFIX "/proxsmtpd.conf" -XXXXXXXXXXXXXXXXXxx -#define SMTP_DATAVIRUSOK "250 Virus Detected; Discarded Email" CRLF -#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected" CRLF +#define CFG_FILTERCMD "FilterCommand" +#define CFG_PIPECMD "Pipe" +#define CFG_DIRECTORY "TempDirectory" +#define CFG_DEBUGFILES "DebugFiles" +#define CFG_CMDTIMEOUT "CommandTimeout" -#define DEFAULT_CONFIG CONF_PREFIX "/proxsmtpd.conf" +/* Poll time for waiting operations in milli seconds */ +#define POLL_TIME 20 -#define CFG_FILTER "FilterCommand" -#define CFG_DIRECTORY "TempDirectory" -#define CFG_QUARANTINE "Quarantine" -#define CFG_DEBUGFILES "DebugFiles" +/* 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 */ -clstate_t g_clstate; +pxstate_t g_pxstate; /* ----------------------------------------------------------------------- * FORWARD DECLARATIONS */ static void usage(); -static int connect_clam(clctx_t* ctx); -static int disconnect_clam(clctx_t* ctx); -static int quarantine_virus(clctx_t* ctx); -static int transfer_to_cache(clctx_t* ctx); -static int clam_scan_file(clctx_t* ctx); - -/* ----------------------------------------------------------------------- - * SIMPLE MACROS - */ +static int process_file_command(spctx_t* sp); +static int process_pipe_command(spctx_t* sp); +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... @@ -115,21 +120,16 @@ int main(int argc, char* argv[]) const char* configfile = DEFAULT_CONFIG; const char* pidfile = NULL; int dbg_level = -1; - int warnargs = 0; int ch = 0; int r; char* t; /* Setup some defaults */ - memset(&g_clstate, 0, sizeof(g_clstate)); - g_clstate.header = DEFAULT_HEADER; - g_clstate.directory = _PATH_TMP; - - /* We need the default to parse into a useable form, so we do this: */ - r = cb_parse_option(CFG_CLAMADDR, DEFAULT_CLAMAV); - ASSERT(r == 1); + memset(&g_pxstate, 0, sizeof(g_pxstate)); + g_pxstate.directory = _PATH_TMP; + g_pxstate.pipe_cmd = 1; - sp_init("clamsmtpd"); + sp_init("proxsmtpd"); /* * We still accept our old arguments for compatibility reasons. @@ -137,26 +137,10 @@ int main(int argc, char* argv[]) */ /* Parse the arguments nicely */ - while((ch = getopt(argc, argv, "bc:d:D:f:h:l:m:p:qt:v")) != -1) + while((ch = getopt(argc, argv, "d:f:p:v")) != -1) { switch(ch) { - /* COMPAT: Actively reject messages */ - case 'b': - if((r = cb_parse_option(CFG_BOUNCE, "on")) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - - /* COMPAT: Change the CLAM socket */ - case 'c': - if((r = cb_parse_option(CFG_CLAMADDR, "on")) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - /* Don't daemonize */ case 'd': dbg_level = strtol(optarg, &t, 10); @@ -165,64 +149,16 @@ int main(int argc, char* argv[]) dbg_level += LOG_ERR; break; - /* COMPAT: The directory for the files */ - case 'D': - if((r = sp_parse_option(CFG_DIRECTORY, optarg)) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - /* The configuration file */ case 'f': configfile = optarg; break; - /* COMPAT: The header to add */ - case 'h': - if((r = cb_parse_option(CFG_HEADER, optarg)) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - - /* COMPAT: Change our listening port */ - case 'l': - if((r = sp_parse_option("Listen", optarg)) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - - /* COMPAT: The maximum number of threads */ - case 'm': - if((r = sp_parse_option("MaxConnections", optarg)) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - /* Write out a pid file */ case 'p': pidfile = optarg; break; - /* COMPAT: The timeout */ - case 't': - if((r = sp_parse_option("TimeOut", optarg)) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - - /* COMPAT: Leave virus files in directory */ - case 'q': - if((r = cb_parse_option(CFG_QUARANTINE, "on")) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - /* Print version number */ case 'v': printf("clamsmtpd (version %s)\n", VERSION); @@ -230,14 +166,6 @@ int main(int argc, char* argv[]) exit(0); break; - /* COMPAT: Leave all files in the tmp directory */ - case 'X': - if((r = cb_parse_option(CFG_DEBUGFILES, "on")) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - break; - /* Usage information */ case '?': default: @@ -249,19 +177,8 @@ int main(int argc, char* argv[]) argc -= optind; argv += optind; - if(argc > 1) + if(argc > 0) usage(); - if(argc == 1) - { - /* COMPAT: The out address */ - if((r = sp_parse_option("OutAddress", argv[0])) < 0) - usage(); - ASSERT(r == 1); - warnargs = 1; - } - - if(warnargs) - warnx("please use configuration file instead of command-line flags: %s", configfile); r = sp_run(configfile, pidfile, dbg_level); @@ -272,8 +189,8 @@ int main(int argc, char* argv[]) static void usage() { - fprintf(stderr, "usage: clamsmtpd [-d debuglevel] [-f configfile] [-p pidfile]\n"); - fprintf(stderr, " clamsmtpd -v\n"); + fprintf(stderr, "usage: proxsmtpd [-d debuglevel] [-f configfile] [-p pidfile]\n"); + fprintf(stderr, " proxsmtpd -v\n"); exit(2); } @@ -281,109 +198,61 @@ static void usage() * SP CALLBACKS */ -int cb_check_data(spctx_t* sp) +int cb_check_data(spctx_t* ctx) { int r = 0; - clctx_t* ctx = (clctx_t*)sp; - - /* Connect to clamav */ - if(!spio_valid(&(ctx->clam))) - r = connect_clam(ctx); - /* transfer_to_cache */ - if(r != -1 && (r = transfer_to_cache(ctx)) > 0) - - /* ClamAV doesn't like empty files */ - r = clam_scan_file(ctx); - - switch(r) + if(!g_pxstate.command) { + sp_messagex(ctx, LOG_WARNING, "no filter command specified. passing message through"); - /* - * There was an error tell the client. We haven't notified - * the server about any of this yet - */ - case -1: - if(sp_fail_data(sp, NULL) == -1) - return -1; - break; - - /* - * No virus was found. Now we initiate a connection to the server - * and transfer the file to it. - */ - case 0: - if(sp_done_data(sp, g_clstate.header) == -1) - return -1; - break; + if(sp_cache_data(ctx) == -1 || + sp_done_data(ctx, NULL) == -1) + return -1; /* Message already printed */ + } - /* - * A virus was found, normally we just drop the email. But if - * requested we can send a simple message back to our client. - * The server doesn't know data was ever sent, and the client can - * choose to reset the connection to reuse it if it wants. - */ - case 1: - /* Any special post operation actions on the virus */ - quarantine_virus(ctx); + if(g_pxstate.pipe_cmd) + r = process_pipe_command(ctx); + else + r = process_file_command(ctx); - if(sp_fail_data(sp, g_clstate.bounce ? - SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1) + if(r == -1) + { + if(sp_fail_data(ctx, NULL) == -1) return -1; - break; - - default: - ASSERT(0 && "Invalid clam_scan_file return value"); - break; - }; + } return 0; } int cb_parse_option(const char* name, const char* value) { - if(strcasecmp(CFG_CLAMADDR, name) == 0) - { - if(sock_any_pton(value, &(g_clstate.clamaddr), SANY_OPT_DEFLOCAL) == -1) - errx(2, "invalid " CFG_CLAMADDR " socket name: %s", value); - g_clstate.clamname = value; - return 1; - } + char* t; - else if(strcasecmp(CFG_HEADER, name) == 0) + if(strcasecmp(CFG_FILTERCMD, name) == 0) { - g_clstate.header = (const char*)trim_space((char*)value); - - if(strlen(g_clstate.header) == 0) - g_clstate.header = NULL; - + g_pxstate.command = value; return 1; } else if(strcasecmp(CFG_DIRECTORY, name) == 0) { - g_clstate.directory = value; + g_pxstate.directory = value; return 1; } - else if(strcasecmp(CFG_BOUNCE, name) == 0) + else if(strcasecmp(CFG_CMDTIMEOUT, name) == 0) { - if((g_clstate.bounce = strtob(value)) == -1) - errx(2, "invalid value for " CFG_BOUNCE); + 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_QUARANTINE, name) == 0) + else if(strcasecmp(CFG_PIPECMD, name) == 0) { - if((g_clstate.quarantine = strtob(value)) == -1) - errx(2, "invalid value for " CFG_BOUNCE); - return 1; - } - - else if(strcasecmp(CFG_DEBUGFILES, name) == 0) - { - if((g_clstate.debug_files = strtob(value)) == -1) - errx(2, "invalid value for " CFG_DEBUGFILES); + if((g_pxstate.pipe_cmd = strtob(value)) == -1) + errx(2, "invalid value for " CFG_PIPECMD); return 1; } @@ -392,219 +261,568 @@ int cb_parse_option(const char* name, const char* value) spctx_t* cb_new_context() { - clctx_t* ctx = (clctx_t*)calloc(1, sizeof(clctx_t)); + spctx_t* ctx = (spctx_t*)calloc(1, sizeof(spctx_t)); if(!ctx) - { sp_messagex(NULL, LOG_CRIT, "out of memory"); - return NULL; - } - - /* Initial preparation of the structure */ - spio_init(&(ctx->clam), "CLAMAV"); - return &(ctx->sp); + return ctx; } -void cb_del_context(spctx_t* sp) +void cb_del_context(spctx_t* ctx) { - clctx_t* ctx = (clctx_t*)sp; - ASSERT(sp); - - disconnect_clam(ctx); free(ctx); } -/* ---------------------------------------------------------------------------------- - * CLAM AV +/* ----------------------------------------------------------------------------- + * IMPLEMENTATION */ -static int connect_clam(clctx_t* ctx) +static int process_file_command(spctx_t* sp) { - int ret = 0; - spctx_t* sp = &(ctx->sp); + pid_t pid; + int ret = 0, status, r; - ASSERT(ctx); - ASSERT(!spio_valid(&(ctx->clam))); + /* For reading data from the process */ + int pipe_e[2]; + fd_set rmask; + char obuf[1024]; + char ebuf[256]; - if(spio_connect(sp, &(ctx->clam), &(g_clstate.clamaddr), g_clstate.clamname) == -1) - RETURN(-1); + ASSERT(g_pxstate.command); - spio_read_junk(sp, &(ctx->clam)); + memset(ebuf, 0, sizeof(ebuf)); + memset(pipe_e, ~0, sizeof(pipe_e)); - /* Send a session and a check header to ClamAV */ + if(sp_cache_data(sp) == -1) + RETURN(-1); /* message already printed */ - if(spio_write_data(sp, &(ctx->clam), "SESSION\n") == -1) + /* Create the pipe we need */ + if(pipe(pipe_e) == -1) + { + sp_message(sp, LOG_ERR, "couldn't create pipe for filter command"); RETURN(-1); + } - spio_read_junk(sp, &(ctx->clam)); - -/* - if(spio_write_data(sp, &(ctx->clam), "PING\n") == -1 || - spio_read_line(sp, &(ctx->clam), CLIO_DISCARD | CLIO_TRIM) == -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); - if(strcmp(sp->line, CONNECT_RESPONSE) != 0) + /* The child process */ + case 0: + + /* Fixup our ends of the pipe */ + if(dup2(pipe_e[WRITE_END], STDERR) == -1) + { + sp_message(sp, LOG_ERR, "couldn't dup descriptor for filter command"); + exit(1); + } + + /* Setup environment nicely */ + if(setenv("EMAIL", sp->cachename, 1) == -1 || + setenv("TMP", g_pxstate.directory, 1) == -1) + { + sp_messagex(sp, LOG_ERR, "couldn't setup environment for filter command"); + exit(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"); + exit(1); + break; + }; + + /* The parent process */ + + /* Close our copies of the pipes that we don't need */ + close(pipe_e[WRITE_END]); + pipe_e[WRITE_END] = -1; + + /* Pipe shouldn't be blocking */ + fcntl(pipe_e[READ_END], F_SETFL, fcntl(pipe_e[READ_END], F_GETFL, 0) | O_NONBLOCK); + + /* Main read write loop */ + for(;;) { - sp_message(sp, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); + FD_SET(pipe_e[READ_END], &rmask); + + r = select(FD_SETSIZE, &rmask, NULL, NULL, &(g_pxstate.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); + }; + + for(;;) + { + /* Note because we handle as string we save one byte for null-termination */ + r = read(pipe_e[READ_END], 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) + break; + + /* Null terminate */ + obuf[r] = 0; + + /* And process */ + buffer_reject_message(obuf, ebuf, sizeof(ebuf)); + } + + /* Check if process is still around */ + if(waitpid(pid, &status, WNOHANG) == pid) + { + pid = 0; + break; + } + } + + ASSERT(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, NULL) == -1) + RETURN(-1); /* message already printed */ + } + + /* Check code and use stderr if bad code */ + else + { + if(sp_fail_data(sp, ebuf[0] == 0 ? SMTP_REJECTED : ebuf) == -1) + RETURN(-1); /* message already printed */ + } + + ret = 0; cleanup: - if(ret < 0) - spio_disconnect(sp, &(ctx->clam)); + if(pipe_e[READ_END] != -1) + close(pipe_e[READ_END]); + if(pipe_e[WRITE_END] != -1) + close(pipe_e[WRITE_END]); + + if(pid != 0) + kill_process(sp, pid); return ret; } -static int disconnect_clam(clctx_t* ctx) +static int process_pipe_command(spctx_t* sp) { - spctx_t* sp = &(ctx->sp); + pid_t pid; + int ret = 0, status; + int r, n, done; + + /* For sending data to the process */ + const char* ibuf = NULL; + int ilen = 0; + int pipe_i[2]; + fd_set wmask; + int writing; + + /* For reading data from the process */ + int pipe_o[2]; + int pipe_e[2]; + fd_set rmask; + int reading; + char obuf[1024]; + char ebuf[256]; + + ASSERT(g_pxstate.command); + + memset(ebuf, 0, sizeof(ebuf)); + + memset(pipe_i, ~0, sizeof(pipe_i)); + memset(pipe_o, ~0, sizeof(pipe_o)); + memset(pipe_e, ~0, sizeof(pipe_e)); + + /* Create the pipes we need */ + if(pipe(pipe_i) == -1 || pipe(pipe_o) == -1 || pipe(pipe_e) == -1) + { + sp_message(sp, LOG_ERR, "couldn't create pipes for filter command"); + RETURN(-1); + } - if(!spio_valid(&(ctx->clam))) - return 0; + /* Now fork the pipes across processes */ + switch(pid = fork()) + { + case -1: + sp_message(sp, LOG_ERR, "couldn't fork for filter command"); + RETURN(-1); - if(spio_write_data(sp, &(ctx->clam), CLAM_DISCONNECT) != -1) - spio_read_junk(sp, &(ctx->clam)); + /* The child process */ + case 0: - spio_disconnect(sp, &(ctx->clam)); - return 0; -} + /* Fixup our ends of the pipe */ + if(dup2(pipe_i[READ_END], STDIN) == -1 || + dup2(pipe_o[WRITE_END], STDOUT) == -1 || + dup2(pipe_e[WRITE_END], STDERR) == -1) + { + sp_message(sp, LOG_ERR, "couldn't dup descriptors for filter command"); + exit(1); + } -static int clam_scan_file(clctx_t* ctx) -{ - int len; - spctx_t* sp = &(ctx->sp); + /* Now run the filter command */ + execl("/bin/sh", "sh", "-c", g_pxstate.command, NULL); - /* Needs to be long enough to hold path names */ - ASSERT(SP_LINE_LENGTH > MAXPATHLEN + 32); + /* If that returned then there was an error */ + sp_message(sp, LOG_ERR, "error executing the shell for filter command"); + exit(1); + break; + }; - strcpy(sp->line, CLAM_SCAN); - strcat(sp->line, sp->cachename); - strcat(sp->line, "\n"); + /* The parent process */ - if(spio_write_data(sp, &(ctx->clam), sp->line) == -1) - return -1; + /* Close our copies of the pipes that we don't need */ + close(pipe_i[READ_END]); + pipe_i[READ_END] = -1; + close(pipe_o[WRITE_END]); + pipe_o[WRITE_END] = -1; + close(pipe_e[WRITE_END]); + pipe_e[WRITE_END] = -1; + + /* None of our pipes should be blocking */ + fcntl(pipe_i[WRITE_END], F_SETFL, fcntl(pipe_i[WRITE_END], F_GETFL, 0) | O_NONBLOCK); + fcntl(pipe_o[READ_END], F_SETFL, fcntl(pipe_o[READ_END], F_GETFL, 0) | O_NONBLOCK); + fcntl(pipe_e[READ_END], F_SETFL, fcntl(pipe_e[READ_END], F_GETFL, 0) | O_NONBLOCK); - len = spio_read_line(sp, &(ctx->clam), SPIO_DISCARD | SPIO_TRIM); - if(len == 0) + /* Main read write loop */ + for(;;) { - sp_messagex(sp, LOG_ERR, "clamd disconnected unexpectedly"); - return -1; + reading = 0; + writing = 0; + done = 0; + + FD_ZERO(&rmask); + FD_ZERO(&wmask); + + /* We only select on those that are still open */ + if(pipe_i[WRITE_END] != -1) + { + FD_SET(pipe_i[WRITE_END], &wmask); + writing = 1; + } + if(pipe_o[READ_END] != -1) + { + FD_SET(pipe_o[READ_END], &rmask); + reading = 1; + } + if(pipe_e[READ_END] != -1) + { + FD_SET(pipe_e[READ_END], &rmask); + reading = 1; + } + + /* If nothing open then go away */ + if(!reading && !writing) + break; + + r = select(FD_SETSIZE, reading ? &rmask : NULL, + writing ? &wmask : NULL, NULL, &(g_pxstate.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(FD_ISSET(pipe_i[WRITE_END], &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: + done = 1; + break; + default: + ASSERT(r > 0); + ilen = r; + break; + }; + } + + /* Write data from buffer */ + for(;;) + { + r = write(pipe_i[WRITE_END], ibuf, ilen); + if(r == -1) + { + if(errno == EAGAIN || errno == EINTR) + break; + else if(errno == EPIPE) + { + sp_message(sp, LOG_WARNING, "filter command closed input early"); + + /* Eat up the rest of the data */ + while(sp_read_data(sp, &ibuf) > 0) + ; + done = 1; + break; + } + + /* Otherwise it's a normal error */ + sp_message(sp, LOG_ERR, "couldn't write to filter command"); + RETURN(-1); + } + + else + { + ilen -= r; + ibuf += r; + } + + break; + } + } + + /* Check if process is still around */ + if(!done && waitpid(pid, &status, WNOHANG) == pid) + { + pid = 0; + done = 1; + } + + /* Close output pipes if done */ + if(done) + { + close(pipe_i[WRITE_END]); + pipe_i[WRITE_END] = -1; + + /* Force emptying of these guys */ + FD_SET(pipe_o[READ_END], &rmask); + FD_SET(pipe_e[READ_END], &rmask); + } + + /* + * During normal operation we only read one block of data + * at a time, but once done we make sure to drain the + * output buffers dry. + */ + do + { + /* Handling of stdout, which should be email data */ + if(FD_ISSET(pipe_o[READ_END], &rmask)) + { + r = read(pipe_o[READ_END], obuf, sizeof(obuf)); + if(r > 0) + { + if(sp_write_data(sp, obuf, r) == -1) + RETURN(-1); /* message already printed */ + } + + 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(FD_ISSET(pipe_e[READ_END], &rmask)) + { + /* Note because we handle as string we save one byte for null-termination */ + n = read(pipe_e[READ_END], obuf, sizeof(obuf) - 1); + if(n < 0) + { + if(errno != EINTR || errno != EAGAIN) + { + sp_message(sp, LOG_ERR, "couldn't read data from filter command"); + RETURN(-1); + } + } + + else if(n > 0) + { + /* Null terminate */ + obuf[n] = 0; + + /* And process */ + buffer_reject_message(obuf, ebuf, sizeof(ebuf)); + } + } + + } /* when in 'done' mode we keep reading as long as there's data */ + while(done && !(r == 0 && n == 0)); + + if(done) + break; + + if(sp_is_quit()) + break; } - if(is_last_word(sp->line, CLAM_OK, KL(CLAM_OK))) + /* exit the process if not completed */ + if(pid != 0) { - sp_add_log(sp, "status=", "CLEAN"); - sp_messagex(sp, LOG_DEBUG, "no virus"); - return 0; + if(wait_process(sp, pid, &status) == -1) + { + sp_messagex(sp, LOG_ERR, "timeout waiting for filter command to exit"); + RETURN(-1); + } + + pid = 0; } - if(is_last_word(sp->line, CLAM_FOUND, KL(CLAM_FOUND))) + /* We only trust well behaved programs */ + if(!WIFEXITED(status)) { - len = strlen(sp->cachename); + sp_messagex(sp, LOG_ERR, "filter command terminated abnormally"); + RETURN(-1); + } - if(sp->linelen > len) - sp_add_log(sp, "status=VIRUS:", sp->line + len + 1); - else - sp_add_log(sp, "status=", "VIRUS"); + sp_messagex(sp, LOG_DEBUG, "filter exit code: %d", (int)WEXITSTATUS(status)); - sp_messagex(sp, LOG_DEBUG, "found virus"); - return 1; + /* A successful response */ + if(WEXITSTATUS(status) == 0) + { + if(sp_done_data(sp, NULL) == -1) + RETURN(-1); /* message already printed */ } - if(is_last_word(sp->line, CLAM_ERROR, KL(CLAM_ERROR))) + /* Check code and use stderr if bad code */ + else { - sp_messagex(sp, LOG_ERR, "clamav error: %s", sp->line); - sp_add_log(sp, "status=", "CLAMAV-ERROR"); - return -1; + if(sp_fail_data(sp, ebuf[0] == 0 ? SMTP_REJECTED : ebuf) == -1) + RETURN(-1); /* message already printed */ } - sp_add_log(sp, "status=", "CLAMAV-ERROR"); - sp_messagex(sp, LOG_ERR, "unexepected response from clamd: %s", sp->line); - return -1; -} + ret = 0; -/* ---------------------------------------------------------------------------------- - * TEMP FILE HANDLING - */ +cleanup: -static int quarantine_virus(clctx_t* ctx) -{ - char buf[MAXPATHLEN]; - spctx_t* sp = &(ctx->sp); - char* t; + 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]); + + if(pid != 0) + kill_process(sp, pid); - if(!g_clstate.quarantine) - return 0; + return ret; +} - strlcpy(buf, g_clstate.directory, MAXPATHLEN); - strlcat(buf, "/virus.", MAXPATHLEN); +static void buffer_reject_message(char* data, char* buf, int buflen) +{ + char* t; - /* Points to null terminator */ - t = buf + strlen(buf); + /* Take away all junk at beginning and end */ + data = trim_space(data); /* - * Yes, I know we're using mktemp. And yet we're doing it in - * a safe manner due to the link command below not overwriting - * existing files. + * Look for the last new line in the message. We + * don't care about stuff before that. */ - for(;;) + t = strchr(data, '\n'); + if(t == NULL) + { + t = data; + } + else { - /* Null terminate off the ending, and replace with X's for mktemp */ - *t = 0; - strlcat(buf, "XXXXXX", MAXPATHLEN); + t++; + buf[0] = 0; /* Start a new message */ + } - if(!mktemp(buf)) - { - sp_message(sp, LOG_ERR, "couldn't create quarantine file name"); - return -1; - } + strlcat(buf, t, buflen); +} - /* Try to link the file over to the temp */ - if(link(sp->cachename, buf) == -1) - { - /* We don't want to allow race conditions */ - if(errno == EEXIST) - { - sp_message(sp, LOG_WARNING, "race condition when quarantining virus file: %s", buf); - continue; - } +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); - sp_message(sp, LOG_ERR, "couldn't quarantine virus file"); + while(waits > 0) + { + switch(waitpid(pid, status, WNOHANG)) + { + case 0: + continue; + case -1: + sp_message(sp, LOG_CRIT, "error waiting on process"); return -1; + default: + return 0; } - break; + usleep(POLL_TIME * 1000); + waits--; } - sp_messagex(sp, LOG_INFO, "quarantined virus file as: %s", buf); - return 0; + return -1; } -static int transfer_to_cache(clctx_t* ctx) +static int kill_process(spctx_t* sp, pid_t pid) { - spctx_t* sp = &(ctx->sp); - int r, count = 0; - const char* data; + int status; - while((r = sp_read_data(sp, &data)) != 0) + if(kill(pid, SIGTERM) == -1) { - if(r < 0) - return -1; /* Message already printed */ + if(errno == ESRCH) + return 0; - count += r; - - if((r = sp_write_data(sp, data, r)) < 0) - return -1; /* Message already printed */ + sp_message(sp, LOG_ERR, "couldn't send signal to process"); + return -1; } - /* End the caching */ - if(sp_write_data(sp, NULL, 0) < 0) - return -1; + if(wait_process(sp, pid, &status) == -1) + { + if(kill(pid, SIGKILL) == -1) + { + if(errno == ESRCH) + return 0; - sp_messagex(sp, LOG_DEBUG, "wrote %d bytes to temp file", count); - return count; -} + 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; +} -- cgit v1.2.3