diff options
-rw-r--r-- | INSTALL | 229 | ||||
-rw-r--r-- | configure.in | 2 | ||||
-rw-r--r-- | doc/.cvsignore | 2 | ||||
-rw-r--r-- | doc/Makefile.am | 3 | ||||
-rw-r--r-- | src/.cvsignore | 4 | ||||
-rw-r--r-- | src/proxsmtpd.c | 872 |
6 files changed, 784 insertions, 328 deletions
@@ -0,0 +1,229 @@ +Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002 Free Software +Foundation, Inc. + + This file is free documentation; the Free Software Foundation gives +unlimited permission to copy, distribute and modify it. + +Basic Installation +================== + + These are generic installation instructions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. (Caching is +disabled by default to prevent problems with accidental use of stale +cache files.) + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You only need +`configure.ac' if you want to change it or regenerate `configure' using +a newer version of `autoconf'. + +The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. If you're + using `csh' on an old version of System V, you might need to type + `sh ./configure' instead to prevent `csh' from trying to execute + `configure' itself. + + Running `configure' takes awhile. While running, it prints some + messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package. + + 4. Type `make install' to install the programs and any data files and + documentation. + + 5. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. Run `./configure --help' +for details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c89 CFLAGS=-O2 LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you must use a version of `make' that +supports the `VPATH' variable, such as GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. + + If you have to use a `make' that does not support the `VPATH' +variable, you have to compile the package for one architecture at a +time in the source code directory. After you have installed the +package for one architecture, use `make distclean' before reconfiguring +for another architecture. + +Installation Names +================== + + By default, `make install' will install the package's files in +`/usr/local/bin', `/usr/local/man', etc. You can specify an +installation prefix other than `/usr/local' by giving `configure' the +option `--prefix=PATH'. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +give `configure' the option `--exec-prefix=PATH', the package will use +PATH as the prefix for installing programs and libraries. +Documentation and other data files will still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=PATH' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + +Optional Features +================= + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + +Specifying the System Type +========================== + + There may be some features `configure' cannot figure out +automatically, but needs to determine by the type of machine the package +will run on. Usually, assuming the package is built to be run on the +_same_ architectures, `configure' can figure that out, but if it prints +a message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the `--target=TYPE' option to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + + Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +will cause the specified gcc to be used as the C compiler (unless it is +overridden in the site shell script). + +`configure' Invocation +====================== + + `configure' recognizes the following options to control how it +operates. + +`--help' +`-h' + Print a summary of the options to `configure', and exit. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. + diff --git a/configure.in b/configure.in index 018132b..211fb15 100644 --- a/configure.in +++ b/configure.in @@ -94,7 +94,7 @@ AC_CHECK_DECL(PTHREAD_MUTEX_ERRORCHECK_NP, [AC_DEFINE(HAVE_ERR_MUTEX, 1, "Error [ #include <pthread.h> ])], [ #include <pthread.h> ]) # Required Functions -AC_CHECK_FUNCS([memset strerror malloc realloc getopt strchr tolower getaddrinfo], , +AC_CHECK_FUNCS([memset strerror malloc realloc getopt strchr tolower getaddrinfo usleep], , [echo "ERROR: Required function missing"; exit 1]) AC_CHECK_FUNCS([strlwr strlcat strlcpy strncat strncpy]) diff --git a/doc/.cvsignore b/doc/.cvsignore new file mode 100644 index 0000000..282522d --- /dev/null +++ b/doc/.cvsignore @@ -0,0 +1,2 @@ +Makefile +Makefile.in diff --git a/doc/Makefile.am b/doc/Makefile.am new file mode 100644 index 0000000..e548c86 --- /dev/null +++ b/doc/Makefile.am @@ -0,0 +1,3 @@ + +#man_MANS = proxsmtpd.8 proxsmtpd.conf.5 +#EXTRA_DIST = $(man_MANS) proxsmtpd.conf 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 <sys/types.h> #include <sys/param.h> +#include <sys/wait.h> #include <paths.h> #include <ctype.h> @@ -44,6 +45,7 @@ #include <unistd.h> #include <syslog.h> #include <errno.h> +#include <fcntl.h> #include <err.h> #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; +} |