diff options
Diffstat (limited to 'src/clamsmtpd.c')
-rw-r--r-- | src/clamsmtpd.c | 1312 |
1 files changed, 229 insertions, 1083 deletions
diff --git a/src/clamsmtpd.c b/src/clamsmtpd.c index 4c08919..9f35165 100644 --- a/src/clamsmtpd.c +++ b/src/clamsmtpd.c @@ -33,45 +33,51 @@ * * CONTRIBUTORS * Nate Nielsen <nielsen@memberwebs.com> - * Andreas Steinmetz <ast@domdv.de> */ -#include <sys/time.h> #include <sys/types.h> -#include <sys/socket.h> #include <sys/param.h> -#include <sys/stat.h> +#include <paths.h> #include <ctype.h> #include <stdio.h> #include <unistd.h> -#include <fcntl.h> #include <syslog.h> -#include <signal.h> #include <errno.h> #include <err.h> #include "usuals.h" -#ifdef LINUX_TRANSPARENT_PROXY -#include <linux/netfilter_ipv4.h> -#endif - #include "compat.h" #include "sock_any.h" -#include "clamsmtpd.h" -#include "util.h" +#include "stringx.h" + +#define SP_LEGACY_OPTIONS +#include "smtppass.h" /* ----------------------------------------------------------------------- * STRUCTURES */ -typedef struct clamsmtp_thread +typedef struct clstate { - pthread_t tid; /* Written to by the main thread */ - int fd; /* The file descriptor or -1 */ + /* Settings ------------------------------- */ + struct sockaddr_any clamaddr; /* Address for connecting to clamd */ + const char* clamname; + const char* header; /* The header to add to email */ + const char* directory; /* The directory for temp files */ + int bounce; /* Send back a reject line */ + int quarantine; /* Leave virus files in temp dir */ + int debug_files; /* Leave all files in temp dir */ } -clamsmtp_thread_t; +clstate_t; + +typedef struct clctx +{ + spctx_t sp; /* The main sp context */ + spio_t clam; /* Connection to clamd */ +} +clctx_t; /* ----------------------------------------------------------------------- * STRINGS @@ -79,43 +85,8 @@ clamsmtp_thread_t; #define CRLF "\r\n" -#define SMTP_TOOLONG "500 Line too long" CRLF -#define SMTP_STARTBUSY "554 Server Busy" CRLF -#define SMTP_STARTFAILED "554 Local Error" CRLF -#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected" CRLF -#define SMTP_DATAINTERMED "354 Start mail input; end with <CRLF>.<CRLF>" CRLF -#define SMTP_FAILED "451 Local Error" CRLF -#define SMTP_NOTSUPP "502 Command not implemented" CRLF #define SMTP_DATAVIRUSOK "250 Virus Detected; Discarded Email" CRLF -#define SMTP_OK "250 Ok" CRLF - -#define SMTP_DATA "DATA" CRLF -#define SMTP_BANNER "220 clamsmtp" CRLF -#define SMTP_HELO_RSP "250 clamsmtp" CRLF -#define SMTP_EHLO_RSP "250-clamsmtp" CRLF -#define SMTP_DELIMS "\r\n\t :" -#define SMTP_MULTI_DELIMS " -" - -#define ESMTP_PIPELINE "PIPELINING" -#define ESMTP_TLS "STARTTLS" -#define ESMTP_CHUNK "CHUNKING" -#define ESMTP_BINARY "BINARYMIME" -#define ESMTP_CHECK "CHECKPOINT" - -#define HELO_CMD "HELO" -#define EHLO_CMD "EHLO" -#define FROM_CMD "MAIL FROM" -#define TO_CMD "RCPT TO" -#define DATA_CMD "DATA" -#define RSET_CMD "RSET" -#define STARTTLS_CMD "STARTTLS" -#define BDAT_CMD "BDAT" - -#define DATA_END_SIG "." CRLF - -#define DATA_RSP "354" -#define OK_RSP "250" -#define START_RSP "220" +#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected" CRLF #define CLAM_OK "OK" #define CLAM_ERROR "ERROR" @@ -129,37 +100,35 @@ clamsmtp_thread_t; #define DEFAULT_CONFIG CONF_PREFIX "/clamsmtpd.conf" +#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 DEFAULT_CLAMAV "/var/run/clamav/clamd" +#define DEFAULT_HEADER "X-Virus-Scanned: ClamAV using ClamSMTP" + /* ----------------------------------------------------------------------- * GLOBALS */ -const clstate_t* g_state = NULL; /* The state and configuration of the daemon */ -unsigned int g_unique_id = 0x00100000; /* For connection ids */ - +clstate_t g_clstate; /* ----------------------------------------------------------------------- * FORWARD DECLARATIONS */ static void usage(); -static void on_quit(int signal); -static void pid_file(const char* pidfile, int write); -static void connection_loop(int sock); -static void* thread_main(void* arg); -static int smtp_passthru(clamsmtp_context_t* ctx); -static int connect_out(clamsmtp_context_t* ctx); -static int connect_clam(clamsmtp_context_t* ctx); -static int disconnect_clam(clamsmtp_context_t* ctx); -static void add_to_logline(char* logline, char* prefix, char* line); -static int avcheck_data(clamsmtp_context_t* ctx, char* logline); -static int quarantine_virus(clamsmtp_context_t* ctx, char* tempname); -static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname); -static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname); -static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename); -static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline); -static int read_server_response(clamsmtp_context_t* ctx); -static void read_junk(clamsmtp_context_t* ctx, int fd); +static int connect_clam(clctx_t* ctx); +static int disconnect_clam(clctx_t* ctx); +static int quarantine_virus(clctx_t* ctx); +static int clam_scan_file(clctx_t* ctx); +/* ----------------------------------------------------------------------- + * SIMPLE MACROS + */ /* ---------------------------------------------------------------------------------- * STARTUP ETC... @@ -169,44 +138,62 @@ int main(int argc, char* argv[]) { const char* configfile = DEFAULT_CONFIG; const char* pidfile = NULL; - clstate_t state; + int dbg_level = -1; int warnargs = 0; - int sock; - int true = 1; int ch = 0; + int r; char* t; - clstate_init(&state); - g_state = &state; + /* 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); + + sp_init("clamsmtpd"); + + /* + * We still accept our old arguments for compatibility reasons. + * We fill them into the spstate structure directly + */ /* Parse the arguments nicely */ while((ch = getopt(argc, argv, "bc:d:D:f:h:l:m:p:qt:v")) != -1) { switch(ch) { - /* Actively reject messages */ + /* COMPAT: Actively reject messages */ case 'b': - state.bounce = 1; + if((r = cb_parse_option(CFG_BOUNCE, "on")) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; - /* Change the CLAM socket */ + /* COMPAT: Change the CLAM socket */ case 'c': - state.clamname = optarg; + if((r = cb_parse_option(CFG_CLAMADDR, "on")) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; /* Don't daemonize */ case 'd': - state.debug_level = strtol(optarg, &t, 10); + dbg_level = strtol(optarg, &t, 10); if(*t) /* parse error */ errx(1, "invalid debug log level"); - state.debug_level += LOG_ERR; + dbg_level += LOG_ERR; break; - /* The directory for the files */ + /* COMPAT: The directory for the files */ case 'D': - state.directory = optarg; + if((r = sp_parse_option(CFG_DIRECTORY, optarg)) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; @@ -215,26 +202,27 @@ int main(int argc, char* argv[]) configfile = optarg; break; - /* The header to add */ + /* COMPAT: The header to add */ case 'h': - if(strlen(optarg) == 0) - state.header = NULL; - else - state.header = optarg; + if((r = cb_parse_option(CFG_HEADER, optarg)) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; - /* Change our listening port */ + /* COMPAT: Change our listening port */ case 'l': - state.listenname = optarg; + if((r = sp_parse_option("Listen", optarg)) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; - /* The maximum number of threads */ + /* COMPAT: The maximum number of threads */ case 'm': - state.max_threads = strtol(optarg, &t, 10); - if(*t) /* parse error */ - errx(1, "invalid max threads"); + if((r = sp_parse_option("MaxConnections", optarg)) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; @@ -243,28 +231,34 @@ int main(int argc, char* argv[]) pidfile = optarg; break; - /* The timeout */ + /* COMPAT: The timeout */ case 't': - state.timeout.tv_sec = strtol(optarg, &t, 10); - if(*t) /* parse error */ - errx(1, "invalid timeout"); + if((r = sp_parse_option("TimeOut", optarg)) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; - /* Leave virus files in directory */ + /* COMPAT: Leave virus files in directory */ case 'q': - state.quarantine = 1; + 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); + printf(" (config: %s)\n", DEFAULT_CONFIG); exit(0); break; - /* Leave all files in the tmp directory */ + /* COMPAT: Leave all files in the tmp directory */ case 'X': - state.debug_files = 1; + if((r = cb_parse_option(CFG_DEBUGFILES, "on")) < 0) + usage(); + ASSERT(r == 1); warnargs = 1; break; @@ -283,106 +277,21 @@ int main(int argc, char* argv[]) usage(); if(argc == 1) { - state.outname = argv[0]; + /* 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); - /* Now parse the configuration file */ - if(clstate_parse_config(&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); - } - - clstate_validate(&state); - - messagex(NULL, LOG_DEBUG, "starting up..."); - - /* When set to this we daemonize */ - if(g_state->debug_level == -1) - { - /* Fork a daemon nicely here */ - if(daemon(0, 0) == -1) - { - message(NULL, LOG_ERR, "couldn't run as daemon"); - exit(1); - } - - messagex(NULL, LOG_DEBUG, "running as a daemon"); - state.daemonized = 1; - - /* Open the system log */ - openlog("clamsmtpd", 0, LOG_MAIL); - } - - /* Create the socket */ - sock = socket(SANY_TYPE(g_state->listenaddr), SOCK_STREAM, 0); - if(sock < 0) - { - 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(g_state->listenaddr) == AF_UNIX) - unlink(g_state->listenname); - - 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) - { - message(NULL, LOG_CRIT, "couldn't listen on socket"); - exit(1); - } - - messagex(NULL, LOG_DEBUG, "created socket: %s", g_state->listenname); - - /* Handle some signals */ - signal(SIGPIPE, 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); - - messagex(NULL, LOG_DEBUG, "accepting connections"); - - connection_loop(sock); - - if(pidfile) - pid_file(pidfile, 0); + r = sp_run(configfile, pidfile, dbg_level); - messagex(NULL, LOG_DEBUG, "stopped"); + sp_done(); - /* - * We have to do this at the very end because even printing - * messages requires that g_state is valid. - */ - clstate_cleanup(&state); - return 0; -} - -static void on_quit(int signal) -{ - ((clstate_t*)g_state)->quit = 1; - /* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */ + return r; } static void usage() @@ -392,633 +301,23 @@ static void usage() exit(2); } -static void pid_file(const char* pidfile, int write) -{ - if(write) - { - FILE* f = fopen(pidfile, "w"); - if(f == NULL) - { - message(NULL, LOG_ERR, "couldn't open pid file: %s", pidfile); - } - else - { - fprintf(f, "%d\n", (int)getpid()); - - if(ferror(f)) - message(NULL, LOG_ERR, "couldn't write to pid file: %s", pidfile); - - fclose(f); - } - - messagex(NULL, LOG_DEBUG, "wrote pid file: %s", pidfile); - } - - else - { - unlink(pidfile); - messagex(NULL, LOG_DEBUG, "removed pid file: %s", pidfile); - } -} - - -/* ---------------------------------------------------------------------------------- - * CONNECTION HANDLING - */ - -static void connection_loop(int sock) -{ - clamsmtp_thread_t* threads = NULL; - int fd, i, x, r; - - /* Create the thread buffers */ - threads = (clamsmtp_thread_t*)calloc(g_state->max_threads, sizeof(clamsmtp_thread_t)); - if(!threads) - errx(1, "out of memory"); - - /* Now loop and accept the connections */ - while(!g_state->quit) - { - fd = accept(sock, NULL, NULL); - if(fd == -1) - { - switch(errno) - { - case EINTR: - case EAGAIN: - break; - - case ECONNABORTED: - message(NULL, LOG_ERR, "couldn't accept a connection"); - break; - - default: - message(NULL, LOG_ERR, "couldn't accept a connection"); - ((clstate_t*)g_state)->quit = 1; - break; - }; - - if(g_state->quit) - break; - - continue; - } - - /* Set timeouts on client */ - 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_state->max_threads; i++) - { - /* Find a thread to run or clean up old threads */ - if(threads[i].tid != 0) - { - plock(); - x = threads[i].fd; - punlock(); - - if(x == -1) - { - messagex(NULL, LOG_DEBUG, "cleaning up completed thread"); - pthread_join(threads[i].tid, NULL); - threads[i].tid = 0; - } -#ifdef _DEBUG - else - { - /* For debugging connection problems: */ - messagex(NULL, LOG_DEBUG, "active connection thread: %x", (int)threads[i].tid); - } -#endif - } - - /* Start a new thread if neccessary */ - if(fd != -1 && threads[i].tid == 0) - { - threads[i].fd = fd; - r = pthread_create(&(threads[i].tid), NULL, thread_main, - (void*)(threads + i)); - if(r != 0) - { - threads[i].fd = -1; - threads[i].tid = 0; - - errno = r; - message(NULL, LOG_ERR, "couldn't create thread for connection"); - write(fd, SMTP_STARTFAILED, KL(SMTP_STARTFAILED)); - - shutdown(fd, SHUT_RDWR); - close(fd); - fd = -1; - - break; - } - - messagex(NULL, LOG_DEBUG, "created thread for connection"); - fd = -1; - break; - } - } - - /* 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_state->max_threads); - - write(fd, SMTP_STARTBUSY, KL(SMTP_STARTBUSY)); - shutdown(fd, SHUT_RDWR); - close(fd); - fd = -1; - } - } - - messagex(NULL, LOG_DEBUG, "waiting for threads to quit"); - - /* Quit all threads here */ - for(i = 0; i < g_state->max_threads; i++) - { - /* Clean up quit threads */ - if(threads[i].tid != 0) - { - if(threads[i].fd != -1) - { - plock(); - fd = threads[i].fd; - threads[i].fd = -1; - punlock(); - - shutdown(fd, SHUT_RDWR); - close(fd); - } - - pthread_join(threads[i].tid, NULL); - } - } -} - -static void* thread_main(void* arg) -{ - clamsmtp_thread_t* thread = (clamsmtp_thread_t*)arg; - clamsmtp_context_t* ctx = NULL; - int processing = 0; - int ret = 0; - int fd; - - ASSERT(thread); - - siginterrupt(SIGINT, 1); - siginterrupt(SIGTERM, 1); - - plock(); - /* Get the client socket */ - fd = thread->fd; - punlock(); - - ctx = (clamsmtp_context_t*)calloc(1, sizeof(clamsmtp_context_t)); - if(!ctx) - { - /* Special case. We don't have a context so clean up descriptor */ - close(fd); - - messagex(NULL, LOG_CRIT, "out of memory"); - RETURN(-1); - } - - memset(ctx, 0, sizeof(*ctx)); - - clio_init(&(ctx->server), "SERVER"); - clio_init(&(ctx->client), "CLIENT"); - clio_init(&(ctx->clam), "CLAM "); - - plock(); - /* Assign a unique id to the connection */ - ctx->id = g_unique_id++; - - /* We don't care about wraps, but we don't want zero */ - if(g_unique_id == 0) - g_unique_id++; - punlock(); - - ctx->client.fd = fd; - ASSERT(ctx->client.fd != -1); - messagex(ctx, LOG_DEBUG, "processing %d on thread %x", ctx->client.fd, (int)pthread_self()); - - /* Connect to the outgoing server ... */ - if(connect_out(ctx) == -1) - RETURN(-1); - - /* ... and to the AV daemon */ - if(connect_clam(ctx) == -1) - RETURN(-1); - - /* call the processor */ - processing = 1; - ret = smtp_passthru(ctx); - -cleanup: - - if(ctx) - { - disconnect_clam(ctx); - - /* Let the client know about fatal errors */ - if(!processing && ret == -1 && clio_valid(&(ctx->client))) - clio_write_data(ctx, &(ctx->client), SMTP_STARTFAILED); - - clio_disconnect(ctx, &(ctx->client)); - clio_disconnect(ctx, &(ctx->server)); - } - - /* mark this as done */ - plock(); - thread->fd = -1; - punlock(); - - return (void*)(ret == 0 ? 0 : 1); -} - -static int connect_out(clamsmtp_context_t* ctx) -{ - struct sockaddr_any peeraddr; - struct sockaddr_any addr; - const struct sockaddr_any* outaddr; - char buf[MAXPATHLEN]; - const char* outname; - - memset(&peeraddr, 0, sizeof(peeraddr)); - SANY_LEN(peeraddr) = sizeof(peeraddr); - - /* Get the peer name */ - if(getpeername(ctx->client.fd, &SANY_ADDR(peeraddr), &SANY_LEN(peeraddr)) == -1 || - sock_any_ntop(&peeraddr, buf, MAXPATHLEN, SANY_OPT_NOPORT) == -1) - message(ctx, LOG_WARNING, "couldn't get peer address"); - else - messagex(ctx, LOG_INFO, "accepted connection from: %s", buf); - - /* Create the server connection address */ - outaddr = &(g_state->outaddr); - outname = g_state->outname; - - /* For transparent proxying we have to discover the address to connect to */ - if(g_state->transparent) - { - memset(&addr, 0, sizeof(addr)); - SANY_LEN(addr) = sizeof(addr); - -#ifdef LINUX_TRANSPARENT_PROXY - if(getsockopt(ctx->client.fd, SOL_IP, SO_ORIGINAL_DST, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1) -#else - if(getsockname(ctx->client.fd, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1) -#endif - { - message(ctx, LOG_ERR, "couldn't get source address for transparent proxying"); - return -1; - } - - /* Check address types */ - if(sock_any_cmp(&addr, &peeraddr, SANY_OPT_NOPORT) == 0) - { - messagex(ctx, LOG_ERR, "loop detected in transparent proxying"); - return -1; - } - - outaddr = &addr; - } - - /* No transparent proxy but check for loopback option */ - else - { - if(SANY_TYPE(*outaddr) == AF_INET && - outaddr->s.in.sin_addr.s_addr == 0) - { - /* Use the incoming IP as the default */ - memcpy(&addr, &(g_state->outaddr), sizeof(addr)); - memcpy(&(addr.s.in.sin_addr), &(peeraddr.s.in.sin_addr), sizeof(addr.s.in.sin_addr)); - outaddr = &addr; - } -#ifdef HAVE_INET6 - else if(SANY_TYPE(*outaddr) == AF_INET6 && - outaddr->s.in.in6.sin_addr.s_addr == 0) - { - /* Use the incoming IP as the default */ - memcpy(&addr, &(g_state->outaddr), sizeof(addr)); - memcpy(&(addr.s.in.sin6_addr), &(peeraddr.s.in.sin6_addr), sizeof(addr.s.in.sin6_addr)); - outaddr = &addr; - } -#endif - } - - /* Reparse name if possible */ - if(outaddr != &(g_state->outaddr)) - { - if(sock_any_ntop(outaddr, buf, MAXPATHLEN, 0) != -1) - outname = buf; - else - outname = "unknown"; - } - - /* Connect to the server */ - if(clio_connect(ctx, &(ctx->server), outaddr, outname) == -1) - return -1; - - return 0; -} - /* ---------------------------------------------------------------------------------- - * SMTP HANDLING + * SP CALLBACKS */ -static int smtp_passthru(clamsmtp_context_t* ctx) +int cb_check_data(spctx_t* sp) { - clio_t* io = NULL; - char logline[LINE_LENGTH]; - int r, ret = 0; - int neterror = 0; + int r = 0; + clctx_t* ctx = (clctx_t*)sp; - int first_rsp = 1; /* The first 220 response from server to be filtered */ - int filter_ehlo = 0; /* Filtering parts of an EHLO extensions response */ - int filter_host = 0; /* Next response is 250 hostname, which we change */ + /* Connect to clamav */ + if(!spio_valid(&(ctx->clam))) + r = connect_clam(ctx); - ASSERT(clio_valid(&(ctx->clam)) && - clio_valid(&(ctx->clam))); - logline[0] = 0; + if(r != -1 && (r = sp_cache_data(sp)) > 0) - for(;;) - { - if(clio_select(ctx, &io) == -1) - { - neterror = 1; - RETURN(-1); - } - - /* Client has data available, read a line and process */ - if(io == &(ctx->client)) - { - if(clio_read_line(ctx, &(ctx->client), CLIO_DISCARD) == -1) - RETURN(-1); - - /* Client disconnected, we're done */ - if(ctx->linelen == 0) - RETURN(0); - - /* We don't let clients send really long lines */ - if(LINE_TOO_LONG(ctx)) - { - if(clio_write_data(ctx, &(ctx->client), SMTP_TOOLONG) == -1) - RETURN(-1); - - continue; - } - - /* Only valid after EHLO or HELO commands */ - filter_ehlo = 0; - filter_host = 0; - - /* Handle the DATA section via our AV checker */ - if(is_first_word(ctx->line, DATA_CMD, KL(DATA_CMD))) - { - /* Send back the intermediate response to the client */ - if(clio_write_data(ctx, &(ctx->client), SMTP_DATAINTERMED) == -1) - RETURN(-1); - - /* - * Now go into avcheck mode. This also handles the eventual - * sending of the data to the server, making the av check - * transparent - */ - if(avcheck_data(ctx, logline) == -1) - RETURN(-1); - - /* Print the log out for this email */ - messagex(ctx, LOG_INFO, "%s", logline); - - /* Reset log line */ - logline[0] = 0; - - /* Command handled */ - continue; - } - - /* - * We filter out features that we can't support in - * the EHLO response (ESMTP). See below - */ - else if(is_first_word(ctx->line, EHLO_CMD, KL(EHLO_CMD))) - { - messagex(ctx, LOG_DEBUG, "filtering EHLO response"); - filter_ehlo = 1; - filter_host = 1; - - /* A new message */ - logline[0] = 0; - } - - /* - * We need our response to HELO to be modified in order - * to prevent complaints about mail loops - */ - else if(is_first_word(ctx->line, HELO_CMD, KL(HELO_CMD))) - { - filter_host = 1; - - /* A new message line */ - logline[0] = 0; - } - - /* - * We don't like these commands. Filter them out. We should have - * filtered out their service extensions earlier in the EHLO response. - * This is just for errant clients. - */ - else if(is_first_word(ctx->line, STARTTLS_CMD, KL(STARTTLS_CMD)) || - is_first_word(ctx->line, BDAT_CMD, KL(BDAT_CMD))) - { - messagex(ctx, LOG_DEBUG, "ESMTP feature not supported"); - - if(clio_write_data(ctx, &(ctx->client), SMTP_NOTSUPP) == -1) - RETURN(-1); - - /* Command handled */ - continue; - } - - /* Append recipients to log line */ - else if((r = check_first_word(ctx->line, FROM_CMD, KL(FROM_CMD), SMTP_DELIMS)) > 0) - add_to_logline(logline, "from=", ctx->line + r); - - /* Append sender to log line */ - else if((r = check_first_word(ctx->line, TO_CMD, KL(TO_CMD), SMTP_DELIMS)) > 0) - add_to_logline(logline, "to=", ctx->line + r); - - /* Reset log line */ - else if(is_first_word(ctx->line, RSET_CMD, KL(RSET_CMD))) - logline[0] = 0; - - /* All other commands just get passed through to server */ - if(clio_write_data(ctx, &(ctx->server), ctx->line) == -1) - RETURN(-1); - - continue; - } - - /* Server has data available, read a line and forward */ - if(io == &(ctx->server)) - { - if(clio_read_line(ctx, &(ctx->server), CLIO_DISCARD) == -1) - RETURN(-1); - - if(ctx->linelen == 0) - RETURN(0); - - if(LINE_TOO_LONG(ctx)) - messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); - - /* - * We intercept the first response we get from the server. - * This allows us to change header so that it doesn't look - * to the client server that we're in a wierd loop. - * - * In different situations using the local hostname or - * 'localhost' don't work because the receiving mail server - * expects one of those to be its own name. We use 'clamsmtp' - * instead. No properly configured server would have this - * as their domain name, and RFC 2821 allows us to use - * an arbitrary but identifying string. - */ - if(first_rsp) - { - first_rsp = 0; - - if(is_first_word(ctx->line, START_RSP, KL(START_RSP))) - { - messagex(ctx, LOG_DEBUG, "intercepting initial response"); - - if(clio_write_data(ctx, &(ctx->client), SMTP_BANNER) == -1) - RETURN(-1); - - /* Command handled */ - continue; - } - } - - /* - * Certain mail servers (Postfix 1.x in particular) do a loop check - * on the 250 response after a EHLO or HELO. This is where we - * filter that to prevent loopback errors. - */ - if(filter_host) - { - filter_host = 0; - - /* Check for a simple '250 xxxx' */ - if(is_first_word(ctx->line, OK_RSP, KL(OK_RSP))) - { - messagex(ctx, LOG_DEBUG, "intercepting host response"); - - if(clio_write_data(ctx, &(ctx->client), SMTP_HELO_RSP) == -1) - RETURN(-1); - - continue; - } - - /* Check for the continued response '250-xxxx' */ - if(check_first_word(ctx->line, OK_RSP, KL(OK_RSP), SMTP_MULTI_DELIMS) > 0) - { - messagex(ctx, LOG_DEBUG, "intercepting host response"); - - if(clio_write_data(ctx, &(ctx->client), SMTP_EHLO_RSP) == -1) - RETURN(-1); - - continue; - } - } - - /* - * Filter out any EHLO responses that we can't or don't want - * to support. For example pipelining or TLS. - */ - if(filter_ehlo) - { - if((r = check_first_word(ctx->line, OK_RSP, KL(OK_RSP), SMTP_MULTI_DELIMS)) > 0) - { - char* p = ctx->line + r; - if(is_first_word(p, ESMTP_PIPELINE, KL(ESMTP_PIPELINE)) || - is_first_word(p, ESMTP_TLS, KL(ESMTP_TLS)) || - is_first_word(p, ESMTP_CHUNK, KL(ESMTP_CHUNK)) || - is_first_word(p, ESMTP_BINARY, KL(ESMTP_BINARY)) || - is_first_word(p, ESMTP_CHECK, KL(ESMTP_CHECK))) - { - messagex(ctx, LOG_DEBUG, "filtered ESMTP feature: %s", trim_space(p)); - continue; - } - } - } - - if(clio_write_data(ctx, &(ctx->client), ctx->line) == -1) - RETURN(-1); - - continue; - } - } - -cleanup: - - if(!neterror && ret == -1 && clio_valid(&(ctx->client))) - clio_write_data(ctx, &(ctx->client), SMTP_FAILED); - - return ret; -} - -static void add_to_logline(char* logline, char* prefix, char* line) -{ - int l = strlen(logline); - char* t = logline; - - /* Simple optimization */ - logline += l; - l = LINE_LENGTH - l; - - ASSERT(l >= 0); - - if(t[0] != 0) - strlcat(logline, ", ", l); - - strlcat(logline, prefix, l); - - /* Skip initial white space */ - line = trim_start(line); - - strlcat(logline, line, l); - - /* Skip later white space */ - trim_end(logline); -} - -static int avcheck_data(clamsmtp_context_t* ctx, char* logline) -{ - /* - * Note that most failures are non fatal in this function. - * We only return -1 for data connection errors and the like, - * For most others we actually send a response back to the - * client letting them know what happened and let the SMTP - * connection continue. - */ - - char buf[MAXPATHLEN]; - int havefile = 0; - int r, ret = 0; - - strlcpy(buf, g_state->directory, MAXPATHLEN); - strlcat(buf, "/clamsmtpd.XXXXXX", MAXPATHLEN); - - /* transfer_to_file deletes the temp file on failure */ - if((r = transfer_to_file(ctx, buf)) > 0) - { - havefile = 1; - r = clam_scan_file(ctx, buf, logline); - } + /* ClamAV doesn't like empty files */ + r = clam_scan_file(ctx); switch(r) { @@ -1028,8 +327,8 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) * the server about any of this yet */ case -1: - if(clio_write_data(ctx, &(ctx->client), SMTP_FAILED)) - RETURN(-1); + if(sp_fail_data(sp, NULL) == -1) + return -1; break; /* @@ -1037,8 +336,8 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) * and transfer the file to it. */ case 0: - if(complete_data_transfer(ctx, buf) == -1) - RETURN(-1); + if(sp_done_data(sp, g_clstate.header) == -1) + return -1; break; /* @@ -1048,12 +347,12 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) * choose to reset the connection to reuse it if it wants. */ case 1: - if(clio_write_data(ctx, &(ctx->client), - g_state->bounce ? SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1) - RETURN(-1); - /* Any special post operation actions on the virus */ - quarantine_virus(ctx, buf); + quarantine_virus(ctx); + + if(sp_fail_data(sp, g_clstate.bounce ? + SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1) + return -1; break; default: @@ -1061,109 +360,114 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline) break; }; -cleanup: - if(havefile && !g_state->debug_files) - { - messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf); - unlink(buf); - } - - return ret; + return 0; } -static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname) +int cb_parse_option(const char* name, const char* value) { - ASSERT(ctx); - ASSERT(tempname); - - /* Ask the server for permission to send data */ - if(clio_write_data(ctx, &(ctx->server), SMTP_DATA) == -1) - return -1; - - if(read_server_response(ctx) == -1) - return -1; + 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; + } - /* If server returns an error then tell the client */ - if(!is_first_word(ctx->line, DATA_RSP, KL(DATA_RSP))) + else if(strcasecmp(CFG_HEADER, name) == 0) { - if(clio_write_data(ctx, &(ctx->client), ctx->line) == -1) - return -1; + g_clstate.header = (const char*)trim_space((char*)value); - messagex(ctx, LOG_DEBUG, "server refused data transfer"); + if(strlen(g_clstate.header) == 0) + g_clstate.header = NULL; - return 0; + return 1; } - /* Now pull up the file and send it to the server */ - if(transfer_from_file(ctx, tempname) == -1) + else if(strcasecmp(CFG_DIRECTORY, name) == 0) { - /* Tell the client it went wrong */ - clio_write_data(ctx, &(ctx->client), SMTP_FAILED); - return -1; + g_clstate.directory = value; + return 1; } - /* Okay read the response from the server and echo it to the client */ - if(read_server_response(ctx) == -1) - return -1; + else if(strcasecmp(CFG_BOUNCE, name) == 0) + { + if((g_clstate.bounce = strtob(value)) == -1) + errx(2, "invalid value for " CFG_BOUNCE); + return 1; + } - if(clio_write_data(ctx, &(ctx->client), ctx->line) == -1) - return -1; + else if(strcasecmp(CFG_QUARANTINE, 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); + return 1; + } return 0; } -static int read_server_response(clamsmtp_context_t* ctx) +spctx_t* cb_new_context() { - /* Read response line from the server */ - if(clio_read_line(ctx, &(ctx->server), CLIO_DISCARD) == -1) - return -1; - - if(ctx->linelen == 0) + clctx_t* ctx = (clctx_t*)calloc(1, sizeof(clctx_t)); + if(!ctx) { - messagex(ctx, LOG_ERR, "server disconnected unexpectedly"); - - /* Tell the client it went wrong */ - clio_write_data(ctx, &(ctx->client), SMTP_FAILED); - return 0; + sp_messagex(NULL, LOG_CRIT, "out of memory"); + return NULL; } - if(LINE_TOO_LONG(ctx)) - messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); - - return 0; + /* Initial preparation of the structure */ + spio_init(&(ctx->clam), "CLAMAV"); + return &(ctx->sp); } +void cb_del_context(spctx_t* sp) +{ + clctx_t* ctx = (clctx_t*)sp; + ASSERT(sp); + + disconnect_clam(ctx); + free(ctx); +} /* ---------------------------------------------------------------------------------- * CLAM AV */ -static int connect_clam(clamsmtp_context_t* ctx) +static int connect_clam(clctx_t* ctx) { int ret = 0; + spctx_t* sp = &(ctx->sp); ASSERT(ctx); - ASSERT(!clio_valid(&(ctx->clam))); + ASSERT(!spio_valid(&(ctx->clam))); - if(clio_connect(ctx, &(ctx->clam), &(g_state->clamaddr), g_state->clamname) == -1) + if(spio_connect(sp, &(ctx->clam), &(g_clstate.clamaddr), g_clstate.clamname) == -1) RETURN(-1); - read_junk(ctx, ctx->clam.fd); + spio_read_junk(sp, &(ctx->clam)); /* Send a session and a check header to ClamAV */ - if(clio_write_data(ctx, &(ctx->clam), "SESSION\n") == -1) + if(spio_write_data(sp, &(ctx->clam), "SESSION\n") == -1) RETURN(-1); - read_junk(ctx, ctx->clam.fd); + spio_read_junk(sp, &(ctx->clam)); + /* - if(clio_write_data(ctx, &(ctx->clam), "PING\n") == -1 || - clio_read_line(ctx, &(ctx->clam), CLIO_DISCARD | CLIO_TRIM) == -1) + if(spio_write_data(sp, &(ctx->clam), "PING\n") == -1 || + spio_read_line(sp, &(ctx->clam), CLIO_DISCARD | CLIO_TRIM) == -1) RETURN(-1); - if(strcmp(ctx->line, CONNECT_RESPONSE) != 0) + if(strcmp(sp->line, CONNECT_RESPONSE) != 0) { - message(ctx, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); + sp_message(sp, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); RETURN(-1); } */ @@ -1171,89 +475,93 @@ static int connect_clam(clamsmtp_context_t* ctx) cleanup: if(ret < 0) - clio_disconnect(ctx, &(ctx->clam)); + spio_disconnect(sp, &(ctx->clam)); return ret; } -static int disconnect_clam(clamsmtp_context_t* ctx) +static int disconnect_clam(clctx_t* ctx) { - if(!clio_valid(&(ctx->clam))) + spctx_t* sp = &(ctx->sp); + + if(!spio_valid(&(ctx->clam))) return 0; - if(clio_write_data(ctx, &(ctx->clam), CLAM_DISCONNECT) != -1) - read_junk(ctx, ctx->clam.fd); + if(spio_write_data(sp, &(ctx->clam), CLAM_DISCONNECT) != -1) + spio_read_junk(sp, &(ctx->clam)); - clio_disconnect(ctx, &(ctx->clam)); + spio_disconnect(sp, &(ctx->clam)); return 0; } -static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline) +static int clam_scan_file(clctx_t* ctx) { int len; + spctx_t* sp = &(ctx->sp); - ASSERT(LINE_LENGTH > MAXPATHLEN + 32); + /* Needs to be long enough to hold path names */ + ASSERT(SP_LINE_LENGTH > MAXPATHLEN + 32); - strcpy(ctx->line, CLAM_SCAN); - strcat(ctx->line, tempname); - strcat(ctx->line, "\n"); + strcpy(sp->line, CLAM_SCAN); + strcat(sp->line, sp->cachename); + strcat(sp->line, "\n"); - if(clio_write_data(ctx, &(ctx->clam), ctx->line) == -1) + if(spio_write_data(sp, &(ctx->clam), sp->line) == -1) return -1; - len = clio_read_line(ctx, &(ctx->clam), CLIO_DISCARD | CLIO_TRIM); + len = spio_read_line(sp, &(ctx->clam), SPIO_DISCARD | SPIO_TRIM); if(len == 0) { - messagex(ctx, LOG_ERR, "clamd disconnected unexpectedly"); + sp_messagex(sp, LOG_ERR, "clamd disconnected unexpectedly"); return -1; } - if(is_last_word(ctx->line, CLAM_OK, KL(CLAM_OK))) + if(is_last_word(sp->line, CLAM_OK, KL(CLAM_OK))) { - add_to_logline(logline, "status=", "CLEAN"); - messagex(ctx, LOG_DEBUG, "no virus"); + sp_add_log(sp, "status=", "CLEAN"); + sp_messagex(sp, LOG_DEBUG, "no virus"); return 0; } - if(is_last_word(ctx->line, CLAM_FOUND, KL(CLAM_FOUND))) + if(is_last_word(sp->line, CLAM_FOUND, KL(CLAM_FOUND))) { - len = strlen(tempname); + len = strlen(sp->cachename); - if(ctx->linelen > len) - add_to_logline(logline, "status=VIRUS:", ctx->line + len + 1); + if(sp->linelen > len) + sp_add_log(sp, "status=VIRUS:", sp->line + len + 1); else - add_to_logline(logline, "status=", "VIRUS"); + sp_add_log(sp, "status=", "VIRUS"); - messagex(ctx, LOG_DEBUG, "found virus"); + sp_messagex(sp, LOG_DEBUG, "found virus"); return 1; } - if(is_last_word(ctx->line, CLAM_ERROR, KL(CLAM_ERROR))) + if(is_last_word(sp->line, CLAM_ERROR, KL(CLAM_ERROR))) { - messagex(ctx, LOG_ERR, "clamav error: %s", ctx->line); - add_to_logline(logline, "status=", "CLAMAV-ERROR"); + sp_messagex(sp, LOG_ERR, "clamav error: %s", sp->line); + sp_add_log(sp, "status=", "CLAMAV-ERROR"); return -1; } - add_to_logline(logline, "status=", "CLAMAV-ERROR"); - messagex(ctx, LOG_ERR, "unexepected response from clamd: %s", ctx->line); + sp_add_log(sp, "status=", "CLAMAV-ERROR"); + sp_messagex(sp, LOG_ERR, "unexepected response from clamd: %s", sp->line); return -1; } - /* ---------------------------------------------------------------------------------- * TEMP FILE HANDLING */ -static int quarantine_virus(clamsmtp_context_t* ctx, char* tempname) +static int quarantine_virus(clctx_t* ctx) { char buf[MAXPATHLEN]; + spctx_t* sp = &(ctx->sp); char* t; - if(!g_state->quarantine) + if(!g_clstate.quarantine) return 0; - strlcpy(buf, g_state->directory, MAXPATHLEN); + strlcpy(buf, g_clstate.directory, MAXPATHLEN); strlcat(buf, "/virus.", MAXPATHLEN); /* Points to null terminator */ @@ -1272,191 +580,29 @@ static int quarantine_virus(clamsmtp_context_t* ctx, char* tempname) if(!mktemp(buf)) { - message(ctx, LOG_ERR, "couldn't create quarantine file name"); + sp_message(sp, LOG_ERR, "couldn't create quarantine file name"); return -1; } /* Try to link the file over to the temp */ - if(link(tempname, buf) == -1) + if(link(sp->cachename, buf) == -1) { /* We don't want to allow race conditions */ if(errno == EEXIST) { - message(ctx, LOG_WARNING, "race condition when quarantining virus file: %s", buf); + sp_message(sp, LOG_WARNING, "race condition when quarantining virus file: %s", buf); continue; } - message(ctx, LOG_ERR, "couldn't quarantine virus file"); + sp_message(sp, LOG_ERR, "couldn't quarantine virus file"); return -1; } break; } - messagex(ctx, LOG_INFO, "quarantined virus file as: %s", buf); + sp_messagex(sp, LOG_INFO, "quarantined virus file as: %s", buf); return 0; } -static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname) -{ - FILE* tfile = NULL; - int tfd = -1; - int ended_crlf = 1; /* If the last line ended with a CRLF */ - int ret = 0; - int count = 0; - - if((tfd = mkstemp(tempname)) == -1 || - (tfile = fdopen(tfd, "w")) == NULL) - { - message(ctx, LOG_ERR, "couldn't open temp file"); - RETURN(-1); - } - - messagex(ctx, LOG_DEBUG, "created temporary file: %s", tempname); - - for(;;) - { - switch(clio_read_line(ctx, &(ctx->client), CLIO_QUIET)) - { - case 0: - messagex(ctx, LOG_ERR, "unexpected end of data from client"); - RETURN(-1); - - case -1: - /* Message already printed */ - RETURN(-1); - }; - - if(ended_crlf && strcmp(ctx->line, DATA_END_SIG) == 0) - break; - - /* We check errors on this later */ - fwrite(ctx->line, 1, ctx->linelen, tfile); - count += ctx->linelen; - - /* Check if this line ended with a CRLF */ - ended_crlf = (strcmp(CRLF, ctx->line + (ctx->linelen - KL(CRLF))) == 0); - } - - if(ferror(tfile)) - { - message(ctx, LOG_ERR, "error writing to temp file: %s", tempname); - RETURN(-1); - } - ret = count; - messagex(ctx, LOG_DEBUG, "wrote %d bytes to temp file", count); - -cleanup: - - if(tfile) - fclose(tfile); - - if(tfd != -1) - { - /* Only close this if not opened as a stream */ - if(tfile == NULL) - close(tfd); - - if(ret <= 0) - { - messagex(ctx, LOG_DEBUG, "discarding temporary file"); - unlink(tempname); - } - } - - return ret; -} - -static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename) -{ - FILE* file = NULL; - int header = 0; - int ret = 0; - - file = fopen(filename, "r"); - if(file == NULL) - { - message(ctx, LOG_ERR, "couldn't open temporary file: %s", filename); - RETURN(-1); - } - - messagex(ctx, LOG_DEBUG, "sending from temporary file: %s", filename); - - while(fgets(ctx->line, LINE_LENGTH, file) != NULL) - { - if(g_state->header && !header) - { - /* - * The first blank line we see means the headers are done. - * At this point we add in our virus checked header. - */ - if(is_blank_line(ctx->line)) - { - 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); - - header = 1; - } - } - - if(clio_write_data_raw(ctx, &(ctx->server), ctx->line, strlen(ctx->line)) == -1) - RETURN(-1); - } - - if(ferror(file)) - { - message(ctx, LOG_ERR, "error reading temporary file: %s", filename); - RETURN(-1); - } - - if(clio_write_data(ctx, &(ctx->server), DATA_END_SIG) == -1) - RETURN(-1); - - messagex(ctx, LOG_DEBUG, "sent email data"); - -cleanup: - - if(file != NULL) - fclose(file); - - return ret; -} - - -/* ---------------------------------------------------------------------------------- - * NETWORKING - */ - -static void read_junk(clamsmtp_context_t* ctx, int fd) -{ - char buf[16]; - const char* t; - int said = 0; - int l; - - if(fd == -1) - return; - - /* Make it non blocking */ - fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); - - for(;;) - { - l = read(fd, buf, sizeof(buf) - 1); - if(l <= 0) - break; - - buf[l] = 0; - t = trim_start(buf); - - if(!said && *t) - { - messagex(ctx, LOG_DEBUG, "received junk data from daemon"); - said = 1; - } - } - - fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); -} |