From 1c4ed8a00cd6c5804055bc72d453591854d8ecf7 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Fri, 3 Sep 2004 01:34:14 +0000 Subject: Configuration file for clamsmtp --- src/Makefile.am | 7 +- src/clamsmtpd.8 | 207 ------------------------------------ src/clamsmtpd.c | 247 +++++++++++++++++++----------------------- src/clamsmtpd.h | 57 ++++++++-- src/clio.c | 12 +-- src/clstate.c | 325 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/usuals.h | 2 + src/util.c | 14 +-- src/util.h | 3 - 9 files changed, 504 insertions(+), 370 deletions(-) delete mode 100644 src/clamsmtpd.8 create mode 100644 src/clstate.c (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am index 5b18c6b..3c8deb6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,11 +2,8 @@ sbin_PROGRAMS = clamsmtpd clamsmtpd_SOURCES = clamsmtpd.c clamsmtpd.h util.c util.h sock_any.h sock_any.c \ - compat.c compat.h usuals.h clio.c - -man_MANS = clamsmtpd.8 -EXTRA_DIST = $(man_MANS) + compat.c compat.h usuals.h clio.c clstate.c all-local: @echo "NOTE: Ignore any warnings about mktemp(). It's used safely in this case." - @echo \ No newline at end of file + @echo diff --git a/src/clamsmtpd.8 b/src/clamsmtpd.8 deleted file mode 100644 index 75a1cae..0000000 --- a/src/clamsmtpd.8 +++ /dev/null @@ -1,207 +0,0 @@ -.\" -.\" Copyright (c) 2004, Nate Nielsen -.\" All rights reserved. -.\" -.\" Redistribution and use in source and binary forms, with or without -.\" modification, are permitted provided that the following conditions -.\" are met: -.\" -.\" * Redistributions of source code must retain the above -.\" copyright notice, this list of conditions and the -.\" following disclaimer. -.\" * Redistributions in binary form must reproduce the -.\" above copyright notice, this list of conditions and -.\" the following disclaimer in the documentation and/or -.\" other materials provided with the distribution. -.\" * The names of contributors to this software may not be -.\" used to endorse or promote products derived from this -.\" software without specific prior written permission. -.\" -.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -.\" FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -.\" COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -.\" INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -.\" BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -.\" OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -.\" AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -.\" OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -.\" THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -.\" DAMAGE. -.\" -.\" -.\" CONTRIBUTORS -.\" Nate Nielsen -.\" -.Dd July, 2004 -.Dt clamsmtpd 8 -.Os clamsmtp -.Sh NAME -.Nm clamsmtpd -.Nd an SMTP server for scanning viruses via clamd -.Sh SYNOPSIS -.Nm -.Op Fl bq -.Op Fl c Ar clamaddr -.Op Fl d Ar level -.Op Fl D Ar tmpdir -.Op Fl h Ar header -.Op Fl l Ar listenaddr -.Op Fl m Ar maxconn -.Op Fl p Ar pidfile -.Op Fl r -.Op Fl t Ar timeout -.Ar serveraddr -.Nm -.Fl v -.Sh DESCRIPTION -.Nm -is an SMTP filter that allows you to check for viruses using the ClamAV -anti-virus software. It accepts SMTP connections and forwards the SMTP commands -and responses to another SMTP server. -.Pp -The DATA email body is intercepted and scanned before forwarding. By default email -with viruses are dropped silently and logged without any additional action taken. -.Pp -.Nm -aims to be lightweight and simple rather than have a myriad of options. Your -basic usage would look like the following (Be sure to see the SECURITY section -below): -.Pp -.Dl clamsmtpd -c /path/to/clam.sock mysmtp.com:25 -.Pp -The above command would start -.Nm -listening on port 10025 (the default) and forward email to mysmtp.com on port 25. -It also specifies the socket where -.Xr clamd 8 -is listening for connections. -.Sh OPTIONS -The options are as follows: -.Bl -tag -width Fl -.It Fl b -When this flag is set -.Nm -actively rejects messages with viruses. This may cause the sender to receive -a message back notifying them of the virus. In most cases this is not a good -idea since many viruses spoof sender addresses. -.It Fl c -.Ar clamaddr -specifies the address to connect to -.XR clamd 8 -on. See syntax of addresses below. -[Default: -.Pa /var/run/clamav/clamd -] -.It Fl d -Don't detach from the console and run as a daemon. In addition the -.Ar level -argument specifies what level of error messages to display. 0 being -the least, 4 the most. -.It Fl D -.Ar tmpdir -is the directory to write temp files too. This directory needs to be -accessible to both -.Xr clamd 8 -and -.Nm -[Default: -.Pa /tmp -] -.It Fl h -.Ar header -is a header to add to scanned messages. Add a blank argument to not add -a header. [Default: 'X-AV-Checked: ClamAV using ClamSMTP'] -.It Fl l -.Ar listenaddr -is the address and port to listen for SMTP connections on. See syntax of -addresses below. [Default: port 10025 on all local IP addresses] -.It Fl m -.Ar maxconn -specifies the maximum number of connections to accept at once. -[Default: 64] -.It Fl p -This option causes -.Nm -to write a file with the daemon's process id, which can be used to stop the -daemon. -.Ar pidfile -is the location of the file. -.It Fl q -Quarantine files that contain viruses by leaving them in the -.Ar tmpdir -directory. The file names look like this (where X is a random -character or number): -.Pa virus.XXXXXX -.It Fl t -.Ar timeout -is the number of seconds to wait while reading data from network connections. -[Default: 180 seconds] -.It Fl v -Prints the clamsmtp version number and exits. -.It serveraddr -The address of the SMTP server to send email to once it's been scanned. This -option must be specified. See syntax of addreses below. -.El -.Sh LOGGING -.Nm -logs to -.Xr syslogd -by default under the 'mail' facility. You can also output logs to the console -using the -.Fl d -option. -.Sh LOOPBACK FEATURE -In some cases it's advantagous to consolidate the virus scanning and filtering -for several mail servers on one machine. -.Nm -allows this by providing a loopback feature to connect back to the IP that an -SMTP connection comes in from. -.Pp -To use this feature specify only a port number (no IP address) for the -.Ar serveraddr -in which case -.Nm -will pass the email back to the said port on the incoming IP address. -.Pp -Make sure the -.Ar maxconn -setting is set high enough to handle the mail from all the servers without refusing -connections. -.Sh SECURITY -There's no reason to run this daemon as root. It is meant as a filter and should -listen on a high TCP port. It's probably a good idea to run it using the same -user as the -.Xr clamd 8 -daemon. This way the temporary files it writes are accessible to -.Xr clamd 8 -.Pp -Care should be taken with the directory that -.Nm -writes its temporary files to. In order to be secure, it should not be a world -writeable location. Specify the directory using the -.Fl t -option. -.Pp -.Nm -should probably not be run on a publicly accessible IP address or without a -firewall. This is especially true if the loopback feature is used (see above). -.Sh ADDRESSES -Addresses can be specified in multiple formats: -.Bl -bullet -.It -Unix local addresses can be specified by specifying their full path. -(ie: '/var/run/clamav/clamd'). -.It -IP addresses can be specified using dotted notation with a colon before -the port number (ie: '127.0.0.1:3310'). -.It -IPv6 addresses can be specified using bracketted notation with a colon -before the port number (ie: '[::1]:3310') -.El -.Sh SEE ALSO -.Xr clamd 8 , -.Xr clamdscan 1 -.Sh AUTHOR -.An Nate Nielsen Aq nielsen@memberwebs.com diff --git a/src/clamsmtpd.c b/src/clamsmtpd.c index e68bdf1..4ffb003 100644 --- a/src/clamsmtpd.c +++ b/src/clamsmtpd.c @@ -43,7 +43,6 @@ #include #include -#include #include #include #include @@ -51,7 +50,6 @@ #include #include #include -#include #include "usuals.h" #include "compat.h" @@ -74,8 +72,6 @@ clamsmtp_thread_t; * STRINGS */ -#define KL(s) ((sizeof(s) - 1) / sizeof(char)) - #define CRLF "\r\n" #define SMTP_TOOLONG "500 Line too long" CRLF @@ -126,44 +122,14 @@ clamsmtp_thread_t; #define CLAM_CONNECT "SESSION\nPING\n" #define CLAM_DISCONNECT "END\n" -/* ----------------------------------------------------------------------- - * DEFAULT SETTINGS - */ - -#define DEFAULT_SOCKET "10025" -#define DEFAULT_PORT 10025 -#define DEFAULT_CLAMAV "/var/run/clamav/clamd" -#define DEFAULT_MAXTHREADS 64 -#define DEFAULT_TIMEOUT 180 -#define DEFAULT_HEADER "X-AV-Checked: ClamAV using ClamSMTP" +#define DEFAULT_CONFIG CONF_PREFIX "/httpauthd.conf" /* ----------------------------------------------------------------------- * GLOBALS */ -int g_daemonized = 0; /* Currently running as a daemon */ -int g_debuglevel = LOG_ERR; /* what gets logged to console */ -int g_maxthreads = DEFAULT_MAXTHREADS; /* The maximum number of threads */ -struct timeval g_timeout = { DEFAULT_TIMEOUT, 0 }; - -struct sockaddr_any g_outaddr; /* The outgoing address */ -const char* g_outname = NULL; -struct sockaddr_any g_clamaddr; /* Address for connecting to clamd */ -const char* g_clamname = DEFAULT_CLAMAV; - -char* g_header = DEFAULT_HEADER; /* The header to add to email */ -const char* g_directory = _PATH_TMP; /* The directory for temp files */ +clstate_t g_state; /* The state and configuration of the daemon */ unsigned int g_unique_id = 0x00100000; /* For connection ids */ -int g_bounce = 0; /* Send back a reject line */ -int g_quarantine = 0; /* Leave virus files in temp dir */ -int g_debugfiles = 0; /* Leave all files in temp dir */ - -/* For main loop and signal handlers */ -int g_quit = 0; - -/* The main mutex and condition variables */ -pthread_mutex_t g_mutex; -pthread_mutexattr_t g_mutexattr; /* ----------------------------------------------------------------------- @@ -172,7 +138,7 @@ pthread_mutexattr_t g_mutexattr; static void usage(); static void on_quit(int signal); -static void pid_file(const char* pid, int write); +static void pid_file(int write); static void connection_loop(int sock); static void* thread_main(void* arg); static int smtp_passthru(clamsmtp_context_t* ctx); @@ -195,86 +161,90 @@ static void read_junk(clamsmtp_context_t* ctx, int fd); int main(int argc, char* argv[]) { - const char* listensock = DEFAULT_SOCKET; - struct sockaddr_any addr; - char* pidfile = NULL; - int daemonize = 1; + const char* configfile = DEFAULT_CONFIG; + int warnargs = 0; int sock; int true = 1; int ch = 0; char* t; + clstate_init(&g_state); + /* Parse the arguments nicely */ - while((ch = getopt(argc, argv, "bc:d:D:h:l:m:p:qt:vX")) != -1) + while((ch = getopt(argc, argv, "bc:d:D:h:l:m:p:qt:v")) != -1) { switch(ch) { /* Actively reject messages */ case 'b': - g_bounce = 1; + g_state.bounce = 1; + warnargs = 1; break; /* Change the CLAM socket */ case 'c': - g_clamname = optarg; + g_state.clamname = optarg; + warnargs = 1; break; /* Don't daemonize */ case 'd': - daemonize = 0; - g_debuglevel = strtol(optarg, &t, 10); - if(*t || g_debuglevel > 4) + g_state.debug_level = strtol(optarg, &t, 10); + if(*t) /* parse error */ errx(1, "invalid debug log level"); - g_debuglevel += LOG_ERR; + g_state.debug_level += LOG_ERR; break; /* The directory for the files */ case 'D': - g_directory = optarg; + g_state.directory = optarg; + warnargs = 1; + break; + + /* The configuration file */ + case 'f': + configfile = optarg; break; /* The header to add */ case 'h': if(strlen(optarg) == 0) - g_header = NULL; + g_state.header = NULL; else - { - g_header = optarg; - - /* Trim off any ending newline chars */ - t = g_header + strlen(g_header); - while(t > g_header && (*(t - 1) == '\r' || *(t - 1) == '\n')) - *(--t) = 0; - } + g_state.header = optarg; + warnargs = 1; break; /* Change our listening port */ case 'l': - listensock = optarg; + g_state.listenname = optarg; + warnargs = 1; break; /* The maximum number of threads */ case 'm': - g_maxthreads = strtol(optarg, &t, 10); - if(*t || g_maxthreads <= 1 || g_maxthreads >= 1024) - errx(1, "invalid max threads (must be between 1 and 1024"); + g_state.max_threads = strtol(optarg, &t, 10); + if(*t) /* parse error */ + errx(1, "invalid max threads"); + warnargs = 1; break; /* Write out a pid file */ case 'p': - pidfile = optarg; + g_state.pidfile = optarg; break; /* The timeout */ case 't': - g_timeout.tv_sec = strtol(optarg, &t, 10); - if(*t || g_timeout.tv_sec <= 0) - errx(1, "invalid timeout: %s", optarg); + g_state.timeout.tv_sec = strtol(optarg, &t, 10); + if(*t) /* parse error */ + errx(1, "invalid timeout"); + warnargs = 1; break; /* Leave virus files in directory */ case 'q': - g_quarantine = 1; + g_state.quarantine = 1; break; /* Print version number */ @@ -285,7 +255,8 @@ int main(int argc, char* argv[]) /* Leave all files in the tmp directory */ case 'X': - g_debugfiles = 1; + g_state.debug_files = 1; + warnargs = 1; break; /* Usage information */ @@ -296,25 +267,33 @@ int main(int argc, char* argv[]) } } + if(warnargs); + warnx("please use configuration file instead of command-line flags: %s", configfile); + argc -= optind; argv += optind; - if(argc != 1) + if(argc > 1) usage(); + if(argc == 1) + g_state.outname = argv[0]; + + /* Now parse the configuration file */ + if(clstate_parse_config(&g_state, configfile) == -1) + { + /* Only error when it was forced */ + if(configfile != DEFAULT_CONFIG) + err(1, "couldn't open config file: %s", configfile); + else + warnx("default configuration file not found: %s", configfile); + } - g_outname = argv[0]; + clstate_validate(&g_state); messagex(NULL, LOG_DEBUG, "starting up..."); - /* Parse all the addresses */ - if(sock_any_pton(listensock, &addr, SANY_OPT_DEFANY | SANY_OPT_DEFPORT(DEFAULT_PORT)) == -1) - errx(1, "invalid listen socket name or ip: %s", listensock); - if(sock_any_pton(g_outname, &g_outaddr, SANY_OPT_DEFPORT(25)) == -1) - errx(1, "invalid connect socket name or ip: %s", g_outname); - if(sock_any_pton(g_clamname, &g_clamaddr, SANY_OPT_DEFLOCAL) == -1) - errx(1, "invalid clam socket name: %s", g_clamname); - - if(daemonize) + /* When set to this we daemonize */ + if(g_state.debug_level == -1) { /* Fork a daemon nicely here */ if(daemon(0, 0) == -1) @@ -324,51 +303,61 @@ int main(int argc, char* argv[]) } messagex(NULL, LOG_DEBUG, "running as a daemon"); - g_daemonized = 1; + g_state.daemonized = 1; /* Open the system log */ openlog("clamsmtpd", 0, LOG_MAIL); } /* Create the socket */ - sock = socket(SANY_TYPE(addr), SOCK_STREAM, 0); + sock = socket(SANY_TYPE(g_state.listenaddr), SOCK_STREAM, 0); if(sock < 0) - err(1, "couldn't open socket"); + { + message(NULL, LOG_CRIT, "couldn't open socket"); + exit(1); + } setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&true, sizeof(true)); /* Unlink the socket file if it exists */ - if(SANY_TYPE(addr) == AF_UNIX) - unlink(listensock); + if(SANY_TYPE(g_state.listenaddr) == AF_UNIX) + unlink(g_state.listenname); - if(bind(sock, &SANY_ADDR(addr), SANY_LEN(addr)) != 0) - err(1, "couldn't bind to address: %s", listensock); + if(bind(sock, &SANY_ADDR(g_state.listenaddr), SANY_LEN(g_state.listenaddr)) != 0) + { + message(NULL, LOG_CRIT, "couldn't bind to address: %s", g_state.listenname); + exit(1); + } /* Let 5 connections queue up */ if(listen(sock, 5) != 0) - err(1, "couldn't listen on socket"); + { + message(NULL, LOG_CRIT, "couldn't listen on socket"); + exit(1); + } - messagex(NULL, LOG_DEBUG, "created socket: %s", listensock); + messagex(NULL, LOG_DEBUG, "created socket: %s", g_state.listenname); /* Handle some signals */ signal(SIGPIPE, SIG_IGN); - signal(SIGHUP, SIG_IGN); + signal(SIGHUP, SIG_IGN); signal(SIGINT, on_quit); signal(SIGTERM, on_quit); siginterrupt(SIGINT, 1); siginterrupt(SIGTERM, 1); - if(pidfile) - pid_file(pidfile, 1); + if(g_state.pidfile) + pid_file(1); messagex(NULL, LOG_DEBUG, "accepting connections"); connection_loop(sock); - if(pidfile) - pid_file(pidfile, 0); + if(g_state.pidfile) + pid_file(0); + clstate_cleanup(&g_state); messagex(NULL, LOG_DEBUG, "stopped"); return 0; @@ -376,45 +365,43 @@ int main(int argc, char* argv[]) static void on_quit(int signal) { - g_quit = 1; - + g_state.quit = 1; /* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */ } static void usage() { - fprintf(stderr, "usage: clamsmtpd [-bq] [-c clamaddr] [-d debuglevel] [-D tmpdir] [-h header] " - "[-l listenaddr] [-m maxconn] [-p pidfile] [-t timeout] serveraddr\n"); + fprintf(stderr, "usage: clamsmtpd [-d debuglevel] [-f configfile] \n"); fprintf(stderr, " clamsmtpd -v\n"); exit(2); } -static void pid_file(const char* pidfile, int write) +static void pid_file(int write) { if(write) { - FILE* f = fopen(pidfile, "w"); + FILE* f = fopen(g_state.pidfile, "w"); if(f == NULL) { - message(NULL, LOG_ERR, "couldn't open pid file: %s", pidfile); + message(NULL, LOG_ERR, "couldn't open pid file: %s", g_state.pidfile); } else { fprintf(f, "%d\n", (int)getpid()); if(ferror(f)) - message(NULL, LOG_ERR, "couldn't write to pid file: %s", pidfile); + message(NULL, LOG_ERR, "couldn't write to pid file: %s", g_state.pidfile); fclose(f); } - messagex(NULL, LOG_DEBUG, "wrote pid file: %s", pidfile); + messagex(NULL, LOG_DEBUG, "wrote pid file: %s", g_state.pidfile); } else { - unlink(pidfile); - messagex(NULL, LOG_DEBUG, "removed pid file: %s", pidfile); + unlink(g_state.pidfile); + messagex(NULL, LOG_DEBUG, "removed pid file: %s", g_state.pidfile); } } @@ -429,18 +416,12 @@ static void connection_loop(int sock) int fd, i, x, r; /* Create the thread buffers */ - threads = (clamsmtp_thread_t*)calloc(g_maxthreads, sizeof(clamsmtp_thread_t)); + threads = (clamsmtp_thread_t*)calloc(g_state.max_threads, sizeof(clamsmtp_thread_t)); if(!threads) errx(1, "out of memory"); - /* Create the main mutex and condition variable */ - if(pthread_mutexattr_init(&g_mutexattr) != 0 || - pthread_mutexattr_settype(&g_mutexattr, MUTEX_TYPE) || - pthread_mutex_init(&g_mutex, &g_mutexattr) != 0) - errx(1, "threading problem. can't create mutex or condition var"); - /* Now loop and accept the connections */ - while(!g_quit) + while(!g_state.quit) { fd = accept(sock, NULL, NULL); if(fd == -1) @@ -457,23 +438,23 @@ static void connection_loop(int sock) default: message(NULL, LOG_ERR, "couldn't accept a connection"); - g_quit = 1; + g_state.quit = 1; break; }; - if(g_quit) + if(g_state.quit) break; continue; } /* Set timeouts on client */ - if(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &g_timeout, sizeof(g_timeout)) < 0 || - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &g_timeout, sizeof(g_timeout)) < 0) + if(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &g_state.timeout, sizeof(g_state.timeout)) < 0 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &g_state.timeout, sizeof(g_state.timeout)) < 0) message(NULL, LOG_WARNING, "couldn't set timeouts on incoming connection"); /* Look for thread and also clean up others */ - for(i = 0; i < g_maxthreads; i++) + for(i = 0; i < g_state.max_threads; i++) { /* Find a thread to run or clean up old threads */ if(threads[i].tid != 0) @@ -507,7 +488,7 @@ static void connection_loop(int sock) { errno = r; message(NULL, LOG_ERR, "couldn't create thread"); - g_quit = 1; + g_state.quit = 1; break; } @@ -520,7 +501,7 @@ static void connection_loop(int sock) /* Check to make sure we have a thread */ if(fd != -1) { - messagex(NULL, LOG_ERR, "too many connections open (max %d). sent 554 response", g_maxthreads); + messagex(NULL, LOG_ERR, "too many connections open (max %d). sent 554 response", g_state.max_threads); write(fd, SMTP_STARTBUSY, KL(SMTP_STARTBUSY)); shutdown(fd, SHUT_RDWR); @@ -532,7 +513,7 @@ static void connection_loop(int sock) messagex(NULL, LOG_DEBUG, "waiting for threads to quit"); /* Quit all threads here */ - for(i = 0; i < g_maxthreads; i++) + for(i = 0; i < g_state.max_threads; i++) { /* Clean up quit threads */ if(threads[i].tid != 0) @@ -551,10 +532,6 @@ static void connection_loop(int sock) pthread_join(threads[i].tid, NULL); } } - - /* Close the mutex */ - pthread_mutex_destroy(&g_mutex); - pthread_mutexattr_destroy(&g_mutexattr); } static void* thread_main(void* arg) @@ -616,15 +593,15 @@ static void* thread_main(void* arg) /* Create the server connection address */ - outaddr = &g_outaddr; - outname = g_outname; + outaddr = &(g_state.outaddr); + outname = g_state.outname; if(SANY_TYPE(*outaddr) == AF_INET && outaddr->s.in.sin_addr.s_addr == 0) { /* Use the incoming IP as the default */ in_addr_t in = addr.s.in.sin_addr.s_addr; - memcpy(&addr, &g_outaddr, sizeof(addr)); + memcpy(&addr, &(g_state.outaddr), sizeof(addr)); addr.s.in.sin_addr.s_addr = in; outaddr = &addr; @@ -954,7 +931,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) int havefile = 0; int r, ret = 0; - strlcpy(buf, g_directory, MAXPATHLEN); + strlcpy(buf, g_state.directory, MAXPATHLEN); strlcat(buf, "/clamsmtpd.XXXXXX", MAXPATHLEN); /* transfer_to_file deletes the temp file on failure */ @@ -993,7 +970,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) */ case 1: if(clio_write_data(ctx, &(ctx->client), - g_bounce ? SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1) + g_state.bounce ? SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1) RETURN(-1); /* Any special post operation actions on the virus */ @@ -1006,7 +983,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) }; cleanup: - if(havefile && !g_debugfiles) + if(havefile && !g_state.debug_files) { messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf); unlink(buf); @@ -1089,7 +1066,7 @@ static int connect_clam(clamsmtp_context_t* ctx) ASSERT(ctx); ASSERT(!clio_valid(&(ctx->clam))); - if(clio_connect(ctx, &(ctx->clam), &g_clamaddr, g_clamname) == -1) + if(clio_connect(ctx, &(ctx->clam), &g_state.clamaddr, g_state.clamname) == -1) RETURN(-1); read_junk(ctx, ctx->clam.fd); @@ -1193,10 +1170,10 @@ static int quarantine_virus(clamsmtp_context_t* ctx, char* tempname) char buf[MAXPATHLEN]; char* t; - if(!g_quarantine) + if(!g_state.quarantine) return 0; - strlcpy(buf, g_directory, MAXPATHLEN); + strlcpy(buf, g_state.directory, MAXPATHLEN); strlcat(buf, "/virus.", MAXPATHLEN); /* Points to null terminator */ @@ -1328,7 +1305,7 @@ static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename) while(fgets(ctx->line, LINE_LENGTH, file) != NULL) { - if(g_header && !header) + if(g_state.header && !header) { /* * The first blank line we see means the headers are done. @@ -1336,7 +1313,7 @@ static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename) */ if(is_blank_line(ctx->line)) { - if(clio_write_data_raw(ctx, &(ctx->server), (char*)g_header, strlen(g_header)) == -1 || + if(clio_write_data_raw(ctx, &(ctx->server), (char*)g_state.header, strlen(g_state.header)) == -1 || clio_write_data_raw(ctx, &(ctx->server), CRLF, KL(CRLF)) == -1) RETURN(-1); diff --git a/src/clamsmtpd.h b/src/clamsmtpd.h index ca3df37..deb2cfb 100644 --- a/src/clamsmtpd.h +++ b/src/clamsmtpd.h @@ -39,6 +39,10 @@ #ifndef __CLAMSMTPD_H__ #define __CLAMSMTPD_H__ +#include + +/* IO Buffers see clio.c ---------------------------------------------------- */ + #define BUF_LEN 256 typedef struct clio @@ -50,6 +54,8 @@ typedef struct clio } clio_t; +/* The main context --------------------------------------------------------- */ + /* * A generous maximum line length. It needs to be longer than * a full path on this system can be, because we pass the file @@ -75,17 +81,12 @@ typedef struct clamsmtp_context } clamsmtp_context_t; -extern int g_daemonized; /* Currently running as a daemon */ -extern int g_debuglevel; /* what gets logged to console */ -extern pthread_mutex_t g_mutex; /* The main mutex */ -extern struct timeval g_timeout; -extern int g_quit; - -struct sockaddr_any; #define LINE_TOO_LONG(ctx) ((ctx)->linelen >= (LINE_LENGTH - 2)) #define RETURN(x) { ret = x; goto cleanup; } +/* Implemented in clio.c ---------------------------------------------------- */ + #define CLIO_TRIM 0x00000001 #define CLIO_DISCARD 0x00000002 #define CLIO_QUIET 0x00000004 @@ -99,4 +100,46 @@ int clio_read_line(clamsmtp_context_t* ctx, clio_t* io, int trim); int clio_write_data(clamsmtp_context_t* ctx, clio_t* io, const char* data); int clio_write_data_raw(clamsmtp_context_t* ctx, clio_t* io, unsigned char* buf, int len); + +/* Implemented in clstate.c ------------------------------------------------ */ + +typedef struct clstate +{ + /* Settings ------------------------------- */ + int debug_level; /* The level to print stuff to console */ + int max_threads; /* Maximum number of threads to process at once */ + struct timeval timeout; /* Timeout for communication */ + + struct sockaddr_any outaddr; /* The outgoing address */ + const char* outname; + struct sockaddr_any clamaddr; /* Address for connecting to clamd */ + const char* clamname; + struct sockaddr_any listenaddr; /* Address to listen on */ + const char* listenname; + + const char* header; /* The header to add to email */ + const char* directory; /* The directory for temp files */ + const char* pidfile; /* The process id file */ + int bounce; /* Send back a reject line */ + int quarantine; /* Leave virus files in temp dir */ + int debug_files; /* Leave all files in temp dir */ + + /* State --------------------------------- */ + int daemonized; /* Whether process is daemonized or not */ + pthread_mutex_t mutex; /* The main mutex */ + int quit; /* Quit the process */ + + /* Internal Use ------------------------- */ + char* _p; + pthread_mutexattr_t _mtxattr; +} +clstate_t; + +extern clstate_t g_state; + +void clstate_init(clstate_t* state); +int clstate_parse_config(clstate_t* state, const char* configfile); +void clstate_validate(clstate_t* state); +void clstate_cleanup(clstate_t* state); + #endif /* __CLAMSMTPD_H__ */ diff --git a/src/clio.c b/src/clio.c index 1794118..a2694bb 100644 --- a/src/clio.c +++ b/src/clio.c @@ -90,7 +90,7 @@ static void log_io_data(clamsmtp_context_t* ctx, clio_t* io, const char* data, i memcpy(buf, data, len); buf[len] = 0; - messagex(ctx, LOG_DEBUG, "%s%s%s", GET_IO_NAME(io), + messagex(0, LOG_DEBUG, "%s%s%s", GET_IO_NAME(io), read ? " < " : " > ", buf); data += pos; @@ -116,8 +116,8 @@ int clio_connect(clamsmtp_context_t* ctx, clio_t* io, struct sockaddr_any* sany, if((io->fd = socket(SANY_TYPE(*sany), SOCK_STREAM, 0)) == -1) RETURN(-1); - if(setsockopt(io->fd, SOL_SOCKET, SO_RCVTIMEO, &g_timeout, sizeof(g_timeout)) == -1 || - setsockopt(io->fd, SOL_SOCKET, SO_SNDTIMEO, &g_timeout, sizeof(g_timeout)) == -1) + if(setsockopt(io->fd, SOL_SOCKET, SO_RCVTIMEO, &g_state.timeout, sizeof(g_state.timeout)) == -1 || + setsockopt(io->fd, SOL_SOCKET, SO_SNDTIMEO, &g_state.timeout, sizeof(g_state.timeout)) == -1) messagex(ctx, LOG_WARNING, "couldn't set timeouts on connection"); if(connect(io->fd, &SANY_ADDR(*sany), SANY_LEN(*sany)) == -1) @@ -183,7 +183,7 @@ int clio_select(clamsmtp_context_t* ctx, clio_t** io) /* Select on the above */ - switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_timeout)) + switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_state.timeout)) { case 0: messagex(ctx, LOG_ERR, "network operation timed out"); @@ -244,7 +244,7 @@ int clio_read_line(clamsmtp_context_t* ctx, clio_t* io, int opts) if(errno == EINTR) { /* When the application is quiting */ - if(g_quit) + if(g_state.quit) return -1; /* For any other signal we go again */ @@ -380,7 +380,7 @@ int clio_write_data_raw(clamsmtp_context_t* ctx, clio_t* io, unsigned char* buf, if(errno == EINTR) { /* When the application is quiting */ - if(g_quit) + if(g_state.quit) return -1; /* For any other signal we go again */ diff --git a/src/clstate.c b/src/clstate.c new file mode 100644 index 0000000..1d5f2af --- /dev/null +++ b/src/clstate.c @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2004, Nate Nielsen + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * * Redistributions in binary form must reproduce the + * above copyright notice, this list of conditions and + * the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * The names of contributors to this software may not be + * used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * + * CONTRIBUTORS + * Nate Nielsen + * Yamamoto Takao + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "usuals.h" +#include "compat.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ----------------------------------------------------------------------- + * DIRECTIONS FOR ADDING A CONFIGURATION OPTION + * + * - Add field to clstate_t structure in clamsmtpd.h + * - Add default and set in clstate_init (below) + * - Add config keyword (below) + * - Parsing of option in clstate_parse_config (below) + * - Validation of option in clstate_validate (below) + * - Document in the sample doc/clamsmtpd.conf + * - Document in doc/clamsmtpd.conf.5 + */ + +/* ----------------------------------------------------------------------- + * DEFAULT SETTINGS + */ + +#define DEFAULT_SOCKET "10025" +#define DEFAULT_PORT 10025 +#define DEFAULT_CLAMAV "/var/run/clamav/clamd" +#define DEFAULT_MAXTHREADS 64 +#define DEFAULT_TIMEOUT 180 +#define DEFAULT_HEADER "X-AV-Checked: ClamAV using ClamSMTP" + +/* ----------------------------------------------------------------------- + * CONFIG KEYWORDS + */ + +#define CFG_MAXTHREADS "MaxConnections" +#define CFG_TIMEOUT "TimeOut" +#define CFG_OUTADDR "OutAddress" +#define CFG_LISTENADDR "Listen" +#define CFG_CLAMADDR "ClamAddress" +#define CFG_HEADER "ScanHeader" +#define CFG_DIRECTORY "TempDirectory" +#define CFG_BOUNCE "Bounce" +#define CFG_QUARANTINE "Quarantine" +#define CFG_DEBUGFILES "DebugFiles" +#define CFG_PIDFILE "PidFile" + +/* The set of delimiters that can be present between config and value */ +#define CFG_DELIMS ": \t" + +/* ----------------------------------------------------------------------- + * CODE + */ + +/* String to bool helper function */ +static int strtob(const char* str) +{ + if(strcasecmp(str, "0") == 0 || + strcasecmp(str, "no") == 0 || + strcasecmp(str, "false") == 0 || + strcasecmp(str, "f") == 0 || + strcasecmp(str, "off") == 0) + return 0; + + if(strcasecmp(str, "1") == 0 || + strcasecmp(str, "yes") == 0 || + strcasecmp(str, "true") == 0 || + strcasecmp(str, "t") == 0 || + strcasecmp(str, "on") == 0) + return 1; + + return -1; +} + +void clstate_init(clstate_t* state) +{ + ASSERT(state); + memset(state, 0, sizeof(*state)); + + /* Setup the defaults */ + state->debug_level = -1; + state->max_threads = DEFAULT_MAXTHREADS; + state->timeout.tv_sec = DEFAULT_TIMEOUT; + state->clamname = DEFAULT_CLAMAV; + state->listenname = DEFAULT_SOCKET; + state->header = DEFAULT_HEADER; + state->directory = _PATH_TMP; + + /* Create the main mutex and condition variable */ + if(pthread_mutexattr_init(&(state->_mtxattr)) != 0 || + pthread_mutexattr_settype(&(state->_mtxattr), MUTEX_TYPE) || + pthread_mutex_init(&(state->mutex), &(state->_mtxattr)) != 0) + errx(1, "threading problem. can't create mutex or condition var"); +} + +int clstate_parse_config(clstate_t* state, const char* configfile) +{ + FILE* f = NULL; + long len; + int r; + char* p; + char* t; + char* n; + + ASSERT(state); + ASSERT(configfile); + ASSERT(!state->_p); + + f = fopen(configfile, "r"); + if(f == NULL) + { + /* Soft errors when default config file and not found */ + if((errno == ENOENT || errno == ENOTDIR)) + return -1; + else + err(1, "couldn't open config file: %s", configfile); + } + + if(fseek(f, 0, SEEK_END) == -1 || (len = ftell(f)) == -1) + err(1, "couldn't seek config file: %s", configfile); + + if((state->_p = (char*)malloc(len + 2)) == NULL) + errx(1, "out of memory"); + + if(fread(state->_p, 1, len, f) != len) + err(1, "couldn't read config file: %s", configfile); + + fclose(f); + messagex(NULL, LOG_DEBUG, "successfully opened config file: %s", configfile); + + /* Double null terminate the data */ + p = state->_p; + p[len] = 0; + p[len + 1] = 0; + + /* Now split string at new lines */ + while((t = strchr(p, '\n')) != NULL) + { + *t = 0; + p = t + 1; + } + + n = state->_p; + + /* Go through lines and process them */ + while(*n != 0) + { + p = n; /* Do this before trimming below */ + n = p + strlen(p) + 1; + + p = trim_space(p); + + /* Comments and empty lines */ + if(*p == 0 || *p == '#') + continue; + + /* Save some code typing below */ + #define PARSE(o) \ + (r = check_first_word(p, (o), KL(o), CFG_DELIMS)) + #define VAL \ + (p + r) + + /* + * Note that we don't validate here. If something's wrong + * set it to an invalid value. + */ + + if(PARSE(CFG_MAXTHREADS)) + { + state->max_threads = strtol(VAL, &t, 10); + if(*t) /* parse failed */ + state->max_threads = -1; + } + + else if(PARSE(CFG_TIMEOUT)) + { + state->timeout.tv_sec = strtol(VAL, &t, 10); + if(*t) /* parse failed */ + state->timeout.tv_sec = -1; + } + + else if(PARSE(CFG_OUTADDR)) + state->outname = VAL; + + else if(PARSE(CFG_LISTENADDR)) + state->listenname = VAL; + + else if(PARSE(CFG_CLAMADDR)) + state->clamname = VAL; + + else if(PARSE(CFG_HEADER)) + state->header = VAL; + + else if(PARSE(CFG_DIRECTORY)) + state->directory = VAL; + + else if(PARSE(CFG_PIDFILE)) + state->pidfile = VAL; + + else if(PARSE(CFG_BOUNCE)) + state->bounce = strtob(VAL); + + else if(PARSE(CFG_QUARANTINE)) + state->quarantine = strtob(VAL); + + else if(PARSE(CFG_DEBUGFILES)) + state->debug_files = strtob(VAL); + + /* Unrecognized option */ + else + errx(2, "unrecognized line in config file: %s", p); + + messagex(NULL, LOG_DEBUG, "successfully parsed line: %s", p); + } + + return 0; +} + +void clstate_validate(clstate_t* state) +{ + ASSERT(state); + messagex(NULL, LOG_DEBUG, "validating configuration options"); + + if(state->debug_level < -1 || state->debug_level > 4) + errx(2, "invalid debug log level (must be between 1 and 4)"); + + if(state->max_threads <= 1 || state->max_threads >= 1024) + errx(2, "invalid " CFG_MAXTHREADS " (must be between 1 and 1024)"); + + if(state->timeout.tv_sec <= 0) + errx(2, "invalid " CFG_TIMEOUT); + + /* This option has no default, but is required */ + if(state->outname == NULL) + errx(2, "no " CFG_OUTADDR " specified in config file."); + + if(state->bounce == -1) + errx(2, "invalid value for " CFG_BOUNCE); + if(state->quarantine == -1) + errx(2, "invalid value for " CFG_QUARANTINE); + if(state->debug_files == -1) + errx(2, "invalid value for " CFG_DEBUGFILES); + + /* Parse all the addresses */ + if(sock_any_pton(state->listenname, &(state->listenaddr), SANY_OPT_DEFANY | SANY_OPT_DEFPORT(DEFAULT_PORT)) == -1) + errx(2, "invalid " CFG_LISTENADDR " socket name or ip: %s", state->listenname); + if(sock_any_pton(state->outname, &(state->outaddr), SANY_OPT_DEFPORT(25)) == -1) + errx(2, "invalid " CFG_OUTADDR " socket name or ip: %s", state->outname); + if(sock_any_pton(state->clamname, &(state->clamaddr), SANY_OPT_DEFLOCAL) == -1) + errx(2, "invalid " CFG_CLAMADDR " socket name: %s", state->clamname); + + if(strlen(state->directory) == 0) + errx(2, "invalid " CFG_DIRECTORY); + if(state->pidfile && strlen(state->pidfile) == 0) + errx(2, "invalid " CFG_PIDFILE); + + if(state->header) + { + /* + * This is for when it comes from the command-line. + * Once command line args are phased out this can be removed + */ + state->header = (const char*)trim_space((char*)state->header); + + if(strlen(state->header) == 0) + state->header = NULL; + } +} + +void clstate_cleanup(clstate_t* state) +{ + if(state->_p) + { + free(state->_p); + memset(state, 0, sizeof(*state)); + messagex(NULL, LOG_DEBUG, "freed configuration option memory"); + } + + /* Close the mutex */ + pthread_mutex_destroy(&(state->mutex)); + pthread_mutexattr_destroy(&(state->_mtxattr)); +} diff --git a/src/usuals.h b/src/usuals.h index 00410aa..385bcf9 100644 --- a/src/usuals.h +++ b/src/usuals.h @@ -71,4 +71,6 @@ #define ASSERT #endif +#define KL(s) ((sizeof(s) - 1) / sizeof(char)) + #endif /* __USUALS_H__ */ diff --git a/src/util.c b/src/util.c index 8b6a816..c0a46bc 100644 --- a/src/util.c +++ b/src/util.c @@ -67,14 +67,14 @@ static void vmessage(clamsmtp_context_t* ctx, int level, int err, char* m; int e = errno; - if(g_daemonized) + if(g_state.daemonized) { if(level >= LOG_DEBUG) return; } else { - if(g_debuglevel < level) + if(g_state.debug_level < level) return; } @@ -102,7 +102,7 @@ static void vmessage(clamsmtp_context_t* ctx, int level, int err, } /* Either to syslog or stderr */ - if(g_daemonized) + if(g_state.daemonized) vsyslog(level, msg, ap); else vwarnx(msg, ap); @@ -244,16 +244,16 @@ void plock() #endif #ifdef _DEBUG - r = pthread_mutex_trylock(&g_mutex); + r = pthread_mutex_trylock(&(g_state.mutex)); if(r == EBUSY) { wait = 1; message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self()); - r = pthread_mutex_lock(&g_mutex); + r = pthread_mutex_lock(&(g_state.mutex)); } #else - r = pthread_mutex_lock(&g_mutex); + r = pthread_mutex_lock(&(g_state.mutex)); #endif @@ -273,7 +273,7 @@ void plock() void punlock() { - int r = pthread_mutex_unlock(&g_mutex); + int r = pthread_mutex_unlock(&(g_state.mutex)); if(r != 0) { errno = r; diff --git a/src/util.h b/src/util.h index 8e6f2f4..37fa245 100644 --- a/src/util.h +++ b/src/util.h @@ -42,9 +42,6 @@ void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...); void message(clamsmtp_context_t* ctx, int level, const char* msg, ...); -void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read); -void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix); - int check_first_word(const char* line, const char* word, int len, char* delims); int is_first_word(const char* line, const char* word, int len); int is_last_word(const char* line, const char* word, int len); -- cgit v1.2.3