summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL229
-rw-r--r--configure.in2
-rw-r--r--doc/.cvsignore2
-rw-r--r--doc/Makefile.am3
-rw-r--r--src/.cvsignore4
-rw-r--r--src/proxsmtpd.c872
6 files changed, 784 insertions, 328 deletions
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..54caf7c
--- /dev/null
+++ b/INSTALL
@@ -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;
+}