From ac7e532095160a85ca03476aa707ef80a8a8ce5b Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Thu, 8 Jul 2004 18:27:54 +0000 Subject: Initial import --- .cvsignore | 16 + COPYING | 31 ++ ChangeLog | 8 + Makefile.am | 7 + NEWS | 1 + README | 3 + acsite.m4 | 200 +++++++++ common/compat.c | 77 ++++ common/compat.h | 51 +++ common/smtppass.c | 1215 +++++++++++++++++++++++++++++++++++++++++++++++++++++ common/smtppass.h | 24 ++ common/sock_any.c | 275 ++++++++++++ common/sock_any.h | 33 ++ common/stringx.c | 271 ++++++++++++ common/stringx.h | 19 + common/usuals.h | 38 ++ configure.in | 91 ++++ src/.cvsignore | 5 + src/Makefile.am | 8 + src/clamsmtpd.8 | 132 ++++++ src/clamsmtpd.c | 1215 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/clamsmtpd.h | 24 ++ src/compat.c | 77 ++++ src/compat.h | 51 +++ src/sock_any.c | 275 ++++++++++++ src/sock_any.h | 33 ++ src/usuals.h | 38 ++ src/util.c | 271 ++++++++++++ src/util.h | 19 + 29 files changed, 4508 insertions(+) create mode 100644 .cvsignore create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 acsite.m4 create mode 100644 common/compat.c create mode 100644 common/compat.h create mode 100644 common/smtppass.c create mode 100644 common/smtppass.h create mode 100644 common/sock_any.c create mode 100644 common/sock_any.h create mode 100644 common/stringx.c create mode 100644 common/stringx.h create mode 100644 common/usuals.h create mode 100644 configure.in create mode 100644 src/.cvsignore create mode 100644 src/Makefile.am create mode 100644 src/clamsmtpd.8 create mode 100644 src/clamsmtpd.c create mode 100644 src/clamsmtpd.h create mode 100644 src/compat.c create mode 100644 src/compat.h create mode 100644 src/sock_any.c create mode 100644 src/sock_any.h create mode 100644 src/usuals.h create mode 100644 src/util.c create mode 100644 src/util.h diff --git a/.cvsignore b/.cvsignore new file mode 100644 index 0000000..6bcddb5 --- /dev/null +++ b/.cvsignore @@ -0,0 +1,16 @@ +autom4te.cache +aclocal.m4 +config.h +config.h.in +config.log +config.status +configure +depcomp +install-sh +Makefile +Makefile.in +missing +stamp-* +*.tar.gz +config.guess +config.sub diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..763af15 --- /dev/null +++ b/COPYING @@ -0,0 +1,31 @@ + +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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..0dbca55 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,8 @@ + +0.2 + - Added logging + - Many bug fixes + - Initial public release + +0.1 + - Initial implementation diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..1a5cc97 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,7 @@ + +EXTRA_DIST = config.sub acsite.m4 config.guess +SUBDIRS = src + +dist-hook: + rm -rf `find $(distdir)/ -name CVS` + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..c7ab92a --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +See ChangeLog \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..2aec147 --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +================================================================= + CLAMSMTP README + diff --git a/acsite.m4 b/acsite.m4 new file mode 100644 index 0000000..e8cdb22 --- /dev/null +++ b/acsite.m4 @@ -0,0 +1,200 @@ +dnl Available from the GNU Autoconf Macro Archive at: +dnl http://www.gnu.org/software/ac-archive/htmldoc/acx_pthread.html +dnl +AC_DEFUN([ACX_PTHREAD], [ +AC_REQUIRE([AC_CANONICAL_HOST]) +AC_LANG_SAVE +AC_LANG_C +acx_pthread_ok=no + +# We used to check for pthread.h first, but this fails if pthread.h +# requires special compiler flags (e.g. on True64 or Sequent). +# It gets checked for in the link test anyway. + +# First of all, check if the user has set any of the PTHREAD_LIBS, +# etcetera environment variables, and if threads linking works using +# them: +if test x"$PTHREAD_LIBS$PTHREAD_CFLAGS" != x; then + save_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS $PTHREAD_CFLAGS" + save_LIBS="$LIBS" + LIBS="$PTHREAD_LIBS $LIBS" + AC_MSG_CHECKING([for pthread_join in LIBS=$PTHREAD_LIBS with CFLAGS=$PTHREAD_CFLAGS]) + AC_TRY_LINK_FUNC(pthread_join, acx_pthread_ok=yes) + AC_MSG_RESULT($acx_pthread_ok) + if test x"$acx_pthread_ok" = xno; then + PTHREAD_LIBS="" + PTHREAD_CFLAGS="" + fi + LIBS="$save_LIBS" + CFLAGS="$save_CFLAGS" +fi + +# We must check for the threads library under a number of different +# names; the ordering is very important because some systems +# (e.g. DEC) have both -lpthread and -lpthreads, where one of the +# libraries is broken (non-POSIX). + +# Create a list of thread flags to try. Items starting with a "-" are +# C compiler flags, and other items are library names, except for "none" +# which indicates that we try without any flags at all, and "pthread-config" +# which is a program returning the flags for the Pth emulation library. + +acx_pthread_flags="pthreads none -Kthread -kthread lthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config" + +# The ordering *is* (sometimes) important. Some notes on the +# individual items follow: + +# pthreads: AIX (must check this before -lpthread) +# none: in case threads are in libc; should be tried before -Kthread and +# other compiler flags to prevent continual compiler warnings +# -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h) +# -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) +# lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) +# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads) +# -pthreads: Solaris/gcc +# -mthreads: Mingw32/gcc, Lynx/gcc +# -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it +# doesn't hurt to check since this sometimes defines pthreads too; +# also defines -D_REENTRANT) +# pthread: Linux, etcetera +# --thread-safe: KAI C++ +# pthread-config: use pthread-config program (for GNU Pth library) + +case "${host_cpu}-${host_os}" in + *solaris*) + + # On Solaris (at least, for some versions), libc contains stubbed + # (non-functional) versions of the pthreads routines, so link-based + # tests will erroneously succeed. (We need to link with -pthread or + # -lpthread.) (The stubs are missing pthread_cleanup_push, or rather + # a function called by this macro, so we could check for that, but + # who knows whether they'll stub that too in a future libc.) So, + # we'll just look for -pthreads and -lpthread first: + + acx_pthread_flags="-pthread -pthreads pthread -mt $acx_pthread_flags" + ;; +esac + +if test x"$acx_pthread_ok" = xno; then +for flag in $acx_pthread_flags; do + + case $flag in + none) + AC_MSG_CHECKING([whether pthreads work without any flags]) + ;; + + -*) + AC_MSG_CHECKING([whether pthreads work with $flag]) + PTHREAD_CFLAGS="$flag" + ;; + + pthread-config) + AC_CHECK_PROG(acx_pthread_config, pthread-config, yes, no) + if test x"$acx_pthread_config" = xno; then continue; fi + PTHREAD_CFLAGS="`pthread-config --cflags`" + PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`" + ;; + + *) + AC_MSG_CHECKING([for the pthreads library -l$flag]) + PTHREAD_LIBS="-l$flag" + ;; + esac + + save_LIBS="$LIBS" + save_CFLAGS="$CFLAGS" + LIBS="$PTHREAD_LIBS $LIBS" + CFLAGS="$CFLAGS $PTHREAD_CFLAGS" + + # Check for various functions. We must include pthread.h, + # since some functions may be macros. (On the Sequent, we + # need a special flag -Kthread to make this header compile.) + # We check for pthread_join because it is in -lpthread on IRIX + # while pthread_create is in libc. We check for pthread_attr_init + # due to DEC craziness with -lpthreads. We check for + # pthread_cleanup_push because it is one of the few pthread + # functions on Solaris that doesn't have a non-functional libc stub. + # We try pthread_create on general principles. + AC_TRY_LINK([#include ], + [pthread_t th; pthread_join(th, 0); + pthread_attr_init(0); pthread_cleanup_push(0, 0); + pthread_create(0,0,0,0); pthread_cleanup_pop(0); ], + [acx_pthread_ok=yes]) + + LIBS="$save_LIBS" + CFLAGS="$save_CFLAGS" + + AC_MSG_RESULT($acx_pthread_ok) + if test "x$acx_pthread_ok" = xyes; then + break; + fi + + PTHREAD_LIBS="" + PTHREAD_CFLAGS="" +done +fi + +# Various other checks: +if test "x$acx_pthread_ok" = xyes; then + save_LIBS="$LIBS" + LIBS="$PTHREAD_LIBS $LIBS" + save_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS $PTHREAD_CFLAGS" + + # Detect AIX lossage: threads are created detached by default + # and the JOINABLE attribute has a nonstandard name (UNDETACHED). + AC_MSG_CHECKING([for joinable pthread attribute]) + AC_TRY_LINK([#include ], + [int attr=PTHREAD_CREATE_JOINABLE;], + ok=PTHREAD_CREATE_JOINABLE, ok=unknown) + if test x"$ok" = xunknown; then + AC_TRY_LINK([#include ], + [int attr=PTHREAD_CREATE_UNDETACHED;], + ok=PTHREAD_CREATE_UNDETACHED, ok=unknown) + fi + if test x"$ok" != xPTHREAD_CREATE_JOINABLE; then + AC_DEFINE(PTHREAD_CREATE_JOINABLE, $ok, + [Define to the necessary symbol if this constant + uses a non-standard name on your system.]) + fi + AC_MSG_RESULT(${ok}) + if test x"$ok" = xunknown; then + AC_MSG_WARN([we do not know how to create joinable pthreads]) + fi + + AC_MSG_CHECKING([if more special flags are required for pthreads]) + flag=no + case "${host_cpu}-${host_os}" in + *-aix* | *-freebsd*) flag="-D_THREAD_SAFE";; + *solaris* | *-osf* | *-hpux*) flag="-D_REENTRANT";; + esac + AC_MSG_RESULT(${flag}) + if test "x$flag" != xno; then + PTHREAD_CFLAGS="$flag $PTHREAD_CFLAGS" + fi + + LIBS="$save_LIBS" + CFLAGS="$save_CFLAGS" + + # More AIX lossage: must compile with cc_r + AC_CHECK_PROG(PTHREAD_CC, cc_r, cc_r, ${CC}) +else + PTHREAD_CC="$CC" +fi + +AC_SUBST(PTHREAD_LIBS) +AC_SUBST(PTHREAD_CFLAGS) +AC_SUBST(PTHREAD_CC) + +# Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND: +if test x"$acx_pthread_ok" = xyes; then + ifelse([$1],,AC_DEFINE(HAVE_PTHREAD,1,[Define if you have POSIX threads libraries and header files.]),[$1]) + : +else + acx_pthread_ok=no + $2 +fi +AC_LANG_RESTORE +])dnl ACX_PTHREAD + diff --git a/common/compat.c b/common/compat.c new file mode 100644 index 0000000..baf1e34 --- /dev/null +++ b/common/compat.c @@ -0,0 +1,77 @@ + +#include "usuals.h" +#include "compat.h" + +#ifndef HAVE_REALLOCF + +void* reallocf(void* ptr, size_t size) +{ + void* ret = realloc(ptr, size); + + if(!ret && size) + free(ptr); + + return ret; +} + +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s) +{ + char* t = s; + while(*t) + { + *t = tolower(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s) +{ + char* t = s; + while(*t) + { + *t = toupper(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRLCPY + +#ifndef HAVE_STRNCPY +#error neither strncpy or strlcpy found +#endif + +void strlcpy(char* dest, const char* src, size_t count) +{ + if(count > 0) + { + strncpy(dest, src, count); + dest[count - 1] = 0; + } +} +#endif + +#ifndef HAVE_STRLCAT + +#ifndef HAVE_STRNCAT +#error neither strncat or strlcat found +#endif + +void strlcat(char* dest, const char* src, size_t count) +{ + if(count > 0) + { + strncat(dest, src, count); + dest[count - 1] = 0; + } +} +#endif + + diff --git a/common/compat.h b/common/compat.h new file mode 100644 index 0000000..6c20ae9 --- /dev/null +++ b/common/compat.h @@ -0,0 +1,51 @@ + + +#ifndef _COMPAT_H_ +#define _COMPAT_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#ifndef HAVE_STDARG_H +#error ERROR: Must have a working stdarg.h header +#else +#include +#endif + +#ifndef HAVE_REALLOCF +void* reallocf(void* p, size_t sz); +#endif + +#include + +/* TODO: Move this logic to configure */ +#if HAVE_ERR_MUTEX == 1 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK_NP +#else +# if HAVE_ERR_MUTEX == 2 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK +# else +# error "Need error checking mutex functionality" +# endif +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s); +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s); +#endif + +#ifndef HAVE_STRLCAT +void strlcat(char *dst, const char *src, size_t size); +#endif + +#ifndef HAVE_STRLCPY +void strlcpy(char *dst, const char *src, size_t size); +#endif + +#endif /* _COMPAT_H_ */ diff --git a/common/smtppass.c b/common/smtppass.c new file mode 100644 index 0000000..71e5a3f --- /dev/null +++ b/common/smtppass.c @@ -0,0 +1,1215 @@ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "usuals.h" +#include "compat.h" +#include "sock_any.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ----------------------------------------------------------------------- + * Structures + */ + +typedef struct clamsmtp_thread +{ + pthread_t tid; /* Written to by the main thread */ + int fd; /* The file descriptor or -1 */ +} +clamsmtp_thread_t; + +#define LINE_TOO_LONG(ctx) ((ctx)->linelen >= (LINE_LENGTH - 2)) +#define RETURN(x) { ret = x; goto cleanup; } + +/* ----------------------------------------------------------------------- + * Strings + */ + +#define KL(s) ((sizeof(s) - 1) / sizeof(char)) + +#define SMTP_TOOLONG "500 Line too long\r\n" +#define SMTP_STARTBUSY "554 Server Busy\r\n" +#define SMTP_STARTFAILED "554 Local Error\r\n" +#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected\r\n" +#define SMTP_DATAINTERMED "354 Start mail input; end with .\r\n" +#define SMTP_FAILED "451 Local Error\r\n" + +#define SMTP_DATA "DATA\r\n" +#define SMTP_DELIMS "\r\n\t :" + +#define FROM_CMD "MAIL FROM" +#define TO_CMD "RCPT TO" +#define DATA_CMD "DATA" +#define RSET_CMD "RSET" + +#define DATA_END_SIG "\r\n.\r\n" + +#define DATA_RSP "354" + +#define CLAM_OK "OK" +#define CLAM_ERROR "ERROR" +#define CLAM_FOUND "FOUND" + +#define CONNECT_RSP "PONG" +#define CLAM_SCAN "SCAN " + +#define CLAM_CONNECT "SESSION\nPING\n" +#define CLAM_DISCONNECT "END\n" + +/* ----------------------------------------------------------------------- + * Default Settings + */ + +#define DEFAULT_SOCKET "0.0.0.0: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\r\n" + + +/* ----------------------------------------------------------------------- + * 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; + +const char* g_header = DEFAULT_HEADER; /* The header to add to email */ +const char* g_directory = _PATH_TMP; /* The directory for temp files */ +unsigned int g_unique_id = 0x00001000; /* For connection ids */ + +/* 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; + + +/* ----------------------------------------------------------------------- + * Forward Declarations + */ + +static usage(); +static void on_quit(int signal); +static void write_pid(const char* pid); +static void connection_loop(int sock); +static void* thread_main(void* arg); +static int smtp_passthru(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 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 read_line(clamsmtp_context_t* ctx, int* fd, int trim); +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf); +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len); + + +int main(int argc, char* argv[]) +{ + const char* listensock = DEFAULT_SOCKET; + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + char* pidfile = NULL; + int daemonize = 1; + int sock; + int true = 1; + int ch = 0; + char* t; + + /* Parse the arguments nicely */ + while((ch = getopt(argc, argv, "c:d:D:h:l:m:p:t:")) != -1) + { + switch(ch) + { + /* Change the CLAM socket */ + case 'c': + g_clamname = optarg; + break; + + /* Don't daemonize */ + case 'd': + daemonize = 0; + g_debuglevel = strtol(optarg, &t, 10); + if(*t || g_debuglevel > 4) + errx(1, "invalid debug log level"); + g_debuglevel += LOG_ERR; + break; + + /* The directory for the files */ + case 'D': + g_directory = optarg; + break; + + /* The header to add */ + case 'h': + if(strlen(optarg) == 0) + g_header = NULL; + else + g_header = optarg; + break; + + /* Change our listening port */ + case 'l': + listensock = optarg; + 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"); + break; + + /* Write out a pid file */ + case 'p': + 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); + break; + + /* Usage information */ + case '?': + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if(argc != 1) + usage(); + + g_outname = argv[0]; + + messagex(NULL, LOG_DEBUG, "starting up..."); + + /* Parse all the addresses */ + if(sock_any_pton(listensock, &addr, DEFAULT_PORT) == -1) + errx(1, "invalid listen socket name or ip: %s", listensock); + if(sock_any_pton(g_outname, &g_outaddr, 25) == -1) + errx(1, "invalid connect socket name or ip: %s", g_outname); + if(sock_any_pton(g_clamname, &g_clamaddr, 0) == -1) + errx(1, "invalid clam socket name: %s", g_clamname); + + /* Create the socket */ + sock = socket(SANY_TYPE(addr), SOCK_STREAM, 0); + if(sock < 0) + err(1, "couldn't open socket"); + + 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(bind(sock, &SANY_ADDR(addr), SANY_LEN(addr)) != 0) + err(1, "couldn't bind to address: %s", listensock); + + /* Let 5 connections queue up */ + if(listen(sock, 5) != 0) + err(1, "couldn't listen on socket"); + + messagex(NULL, LOG_DEBUG, "created socket: %s", listensock); + + if(daemonize) + { + /* 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"); + g_daemonized = 1; + + /* Open the system log */ + openlog("clamsmtp", 0, LOG_MAIL); + } + + if(pidfile) + write_pid(pidfile); + + /* 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); + + messagex(NULL, LOG_DEBUG, "accepting connections"); + + connection_loop(sock); + + messagex(NULL, LOG_DEBUG, "stopped"); + + return 0; +} + +static void connection_loop(int sock) +{ + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + int fd, i, x, r; + + /* Create the thread buffers */ + threads = (clamsmtp_thread_t*)calloc(g_maxthreads, 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) + { + 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"); + g_quit = 1; + break; + }; + + if(g_quit) + break; + + continue; + } + + /* Look for thread and also clean up others */ + for(i = 0; i < g_maxthreads; 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; + } + } + + /* 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) + { + errno = r; + message(NULL, LOG_ERR, "couldn't create thread"); + g_quit = 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)", g_maxthreads); + + /* TODO: Respond with a too many connections message */ + write_data(NULL, &fd, SMTP_STARTBUSY); + shutdown(fd, SHUT_RDWR); + } + } + + messagex(NULL, LOG_INFO, "waiting for threads to quit"); + + /* Quit all threads here */ + for(i = 0; i < g_maxthreads; i++) + { + /* Clean up quit threads */ + if(threads[i].tid != 0) + { + if(threads[i].fd != -1) + shutdown(threads[i].fd, SHUT_RDWR); + + pthread_join(threads[i].tid, NULL); + } + } + + /* Close the mutex */ + pthread_mutex_destroy(&g_mutex); + pthread_mutexattr_destroy(&g_mutexattr); +} + +static void on_quit(int signal) +{ + g_quit = 1; + + /* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */ +} + +static int usage() +{ + fprintf(stderr, "clamsmtp [-c clamaddr] [-d debuglevel] [-D tmpdir] [-h header]" + "[-l listenaddr] [-m maxconn] [-p pidfile] [-t timeout] serveraddr\n"); + return 2; +} + +static void write_pid(const char* pidfile) +{ + 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); + } +} + +static void* thread_main(void* arg) +{ + clamsmtp_thread_t* thread = (clamsmtp_thread_t*)arg; + char peername[MAXPATHLEN]; + struct sockaddr_any addr; + clamsmtp_context_t ctx; + int r; + + ASSERT(thread); + + siginterrupt(SIGINT, 1); + siginterrupt(SIGTERM, 1); + + memset(&ctx, 0, sizeof(ctx)); + + plock(); + ctx.client = thread->fd; + punlock(); + + ctx.server = -1; + ctx.clam = -1; + + ASSERT(ctx.client != -1); + + /* Assign a unique id to the connection */ + ctx.id = g_unique_id++; + + /* Get the peer name */ + if(getpeername(ctx.client, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1 || + sock_any_ntop(&addr, peername, MAXPATHLEN) == -1) + messagex(&ctx, LOG_WARNING, "couldn't get peer address"); + else + messagex(&ctx, LOG_INFO, "accepted connection from: %s", peername); + + /* call the processor */ + r = smtp_passthru(&ctx); + + /* Close the incoming connection if neccessary */ + if(ctx.client != -1) + shutdown(ctx.client, SHUT_RDWR); + + messagex(&ctx, LOG_INFO, "closed client connection"); + + /* mark this as done */ + plock(); + thread->fd = -1; + punlock(); + + return (void*)(r == 0 ? 0 : 1); +} + +static int smtp_passthru(clamsmtp_context_t* ctx) +{ + char logline[LINE_LENGTH]; + int processing = 0; + int r, ret = 0; + fd_set mask; + + ASSERT(ctx->server == -1); + + if((ctx->server = socket(SANY_TYPE(g_outaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->server, &SANY_ADDR(g_outaddr), SANY_LEN(g_outaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to %s", g_outname); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "connected to server: %s", g_outname); + + if(connect_clam(ctx) == -1) + RETURN(-1); + + /* This changes the error code sent to the client when an + * error occurs. See cleanup below */ + processing = 1; + logline[0] = 0; + + for(;;) + { + FD_ZERO(&mask); + + FD_SET(ctx->client, &mask); + FD_SET(ctx->server, &mask); + + switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_timeout)) + { + case 0: + message(ctx, LOG_ERR, "network operation timed out"); + RETURN(-1); + case -1: + message(ctx, LOG_ERR, "couldn't select on sockets"); + RETURN(-1); + }; + + /* Client has data available, read a line and process */ + if(FD_ISSET(ctx->client, &mask)) + { + if(read_line(ctx, &(ctx->client), 0) == -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(write_data(ctx, &(ctx->server), SMTP_TOOLONG) == -1) + RETURN(-1); + } + + else + { + if(is_first_word(ctx->line, DATA_CMD, KL(DATA_CMD))) + { + /* Send back the intermediate response to the client */ + if(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; + } + + /* All other commands just get passed through to server */ + else + { + + /* Append recipients to log line */ + 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; + + if(write_data(ctx, &(ctx->server), ctx->line) == -1) + RETURN(-1); + } + } + + continue; + } + + /* Server has data available, read a line and forward */ + if(FD_ISSET(ctx->server, &mask)) + { + if(read_line(ctx, &(ctx->server), 0) == -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"); + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + RETURN(-1); + + continue; + } + } + +cleanup: + + disconnect_clam(ctx); + + if(ret == -1 && ctx->client != -1) + { + write_data(ctx, &(ctx->client), + processing ? SMTP_FAILED : SMTP_STARTFAILED); + } + + if(ctx->server != -1) + { + shutdown(ctx->server, SHUT_RDWR); + messagex(ctx, LOG_DEBUG, "closed server connection"); + } + + 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 */ + while(*line && isspace(*line)) + *line++; + + strlcat(logline, line, l); + t = logline + strlen(logline); + + /* Skip later white space */ + while(t > logline && isspace(*(t - 1))) + *(--t) = 0; +} + +static int connect_clam(clamsmtp_context_t* ctx) +{ + int r, len = -1; + int ret = 0; + + ASSERT(ctx); + ASSERT(ctx->clam == -1); + + if((ctx->clam = socket(SANY_TYPE(g_clamaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->clam, &SANY_ADDR(g_clamaddr), SANY_LEN(g_clamaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to clamd at %s", g_clamname); + RETURN(-1); + } + + read_junk(ctx, ctx->clam); + + /* Send a session and a check header to ClamAV */ + + if(write_data(ctx, &(ctx->clam), "SESSION\n") == -1) + RETURN(-1); + + read_junk(ctx, ctx->clam); +/* + if(write_data(ctx, &(ctx->clam), "PING\n") == -1 || + read_line(ctx, &(ctx->clam), 1) == -1) + RETURN(-1); + + if(strcmp(ctx->line, CONNECT_RESPONSE) != 0) + { + message(ctx, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); + RETURN(-1); + } +*/ + messagex(ctx, LOG_DEBUG, "connected to clamd: %s", g_clamname); + +cleanup: + + if(ret < 0) + { + if(ctx->clam != -1) + { + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam == -1; + } + } + + return ret; +} + +static int disconnect_clam(clamsmtp_context_t* ctx) +{ + if(ctx->clam == -1) + return 0; + + if(write_data(ctx, &(ctx->clam), CLAM_DISCONNECT) != -1) + read_junk(ctx, ctx->clam); + + messagex(ctx, LOG_DEBUG, "disconnected from clamd"); + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam = -1; + return 0; +} + +static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline) +{ + int len; + + ASSERT(LINE_LENGTH < MAXPATHLEN + 32); + + strcpy(ctx->line, CLAM_SCAN); + strcat(ctx->line, tempname); + strcat(ctx->line, "\n"); + + if(write_data(ctx, &(ctx->clam), ctx->line) == -1) + return -1; + + len = read_line(ctx, &(ctx->clam), 1); + if(len == 0) + { + messagex(ctx, LOG_ERR, "clamd disconnected unexpectedly"); + return -1; + } + + if(is_last_word(ctx->line, CLAM_OK, KL(CLAM_OK))) + { + add_to_logline(logline, "status=", "CLEAN"); + messagex(ctx, LOG_DEBUG, "no virus"); + return 0; + } + + if(is_last_word(ctx->line, CLAM_FOUND, KL(CLAM_FOUND))) + { + len = strlen(tempname); + + if(ctx->linelen > len) + add_to_logline(logline, "status=VIRUS:", ctx->line + len + 1); + else + add_to_logline(logline, "status=", "VIRUS"); + + messagex(ctx, LOG_DEBUG, "found virus"); + return 1; + } + + if(is_last_word(ctx->line, CLAM_ERROR, KL(CLAM_ERROR))) + { + messagex(ctx, LOG_ERR, "clamav error: %s", ctx->line); + return -1; + } + + messagex(ctx, LOG_ERR, "unexepected response from clamd: %s", ctx->line); + return -1; +} + +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_directory, MAXPATHLEN); + strlcat(buf, "/clamsmtp.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); + } + + switch(r) + { + + /* + * There was an error tell the client. We haven't notified + * the server about any of this yet + */ + case -1: + if(write_data(ctx, &(ctx->client), SMTP_FAILED)) + RETURN(-1); + break; + + /* + * No virus was found. Now we initiate a connection to the server + * and transfer the file to it. + */ + case 0: + if(complete_data_transfer(ctx, buf) == -1) + RETURN(-1); + break; + + /* + * A virus was found, just send back a simple message to the 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: + if(write_data(ctx, &(ctx->client), SMTP_DATAVIRUS) == -1) + RETURN(-1); + break; + + default: + ASSERT(0 && "Invalid clam_scan_file return value"); + break; + }; + +cleanup: + if(havefile) + { + messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf); + unlink(buf); + } + + return ret; +} + +static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname) +{ + ASSERT(ctx); + ASSERT(tempname); + + /* Ask the server for permission to send data */ + if(write_data(ctx, &(ctx->server), SMTP_DATA) == -1) + return -1; + + if(read_server_response(ctx) == -1) + return -1; + + /* If server returns an error then tell the client */ + if(!is_first_word(ctx->line, DATA_RSP, KL(DATA_RSP))) + { + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + messagex(ctx, LOG_DEBUG, "server refused data transfer"); + + return 0; + } + + /* Now pull up the file and send it to the server */ + if(transfer_from_file(ctx, tempname) == -1) + { + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return -1; + } + + /* Okay read the response from the server and echo it to the client */ + if(read_server_response(ctx) == -1) + return -1; + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + return 0; +} + +static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname) +{ + /* If there aren't any lines in the message and just an + end signature then start at the dot. */ + const char* topsig = strchr(DATA_END_SIG, '.'); + const char* cursig = topsig; + FILE* tfile = NULL; + int tfd = -1; + int ret = 0; + char ch; + int count = 0; + + ASSERT(topsig != NULL); + + 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(read(ctx->client, &ch, 1)) + { + case 0: + messagex(ctx, LOG_ERR, "unexpected end of data from client"); + RETURN(-1); + + case -1: + message(ctx, LOG_ERR, "error reading from client"); + RETURN(-1); + }; + + if((char)ch != *cursig) + { + /* Write out the part of the sig we kept back */ + if(cursig != topsig) + { + /* We check errors on this later */ + fwrite(topsig, 1, cursig - topsig, tfile); + count += (cursig - topsig); + } + + /* We've seen at least one char not in the sig */ + cursig = topsig = DATA_END_SIG; + } + + /* The sig may have been reset above so check again */ + if((char)ch == *cursig) + { + cursig++; + + if(!*cursig) + { + /* We found end of data */ + break; + } + } + + else + { + fputc(ch, tfile); + count++; + } + } + + 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 == -1) + { + 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; + const char* t; + const char* e; + int header = 0; + int ret = 0; + int len, r; + + file = fopen(filename, "r"); + if(file == NULL) + { + message(ctx, LOG_ERR, "couldn't open temporary file: %s", filename); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "opened temporary file: %s", filename); + + while(fgets(ctx->line, LINE_LENGTH, file) != NULL) + { + if(g_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(write_data_raw(ctx, &(ctx->server), g_header, strlen(g_header)) == -1) + RETURN(-1); + } + + header = 1; + } + + if(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(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; +} + +static int read_server_response(clamsmtp_context_t* ctx) +{ + /* Read response line from the server */ + if(read_line(ctx, &(ctx->server), 0) == -1) + return -1; + + if(ctx->linelen == 0) + { + messagex(ctx, LOG_ERR, "server disconnected unexpectedly"); + + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return 0; + } + + if(LINE_TOO_LONG(ctx)) + messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); + + return 0; +} + +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 = buf; + + while(*t && isspace(*t)) + t++; + + if(!said && *t) + { + messagex(ctx, LOG_WARNING, "received junk data from daemon"); + said = 1; + } + } + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); +} + +static int read_line(clamsmtp_context_t* ctx, int* fd, int trim) +{ + int l; + char* t; + const char* e; + + if(*fd == -1) + { + messagex(ctx, LOG_WARNING, "tried to read from a closed connection"); + return 0; + } + + ctx->line[0] = 0; + e = ctx->line + (LINE_LENGTH - 1); + + for(t = ctx->line; t < e; ++t) + { + l = read(*fd, (void*)t, sizeof(char)); + + /* We got a character */ + if(l == 1) + { + /* End of line */ + if(*t == '\n') + { + ++t; + break; + } + + /* We skip spaces at the beginning if trimming */ + if(trim && t == ctx->line && isspace(*t)) + continue; + } + + /* If it's the end of file then return that */ + else if(l == 0) + { + /* Put in an extra line if there was anything */ + if(t > ctx->line && !trim) + { + *t = '\n'; + ++t; + } + + break; + } + + /* Transient errors */ + else if(l == -1 && errno == EAGAIN) + continue; + + /* Fatal errors */ + else if(l == -1) + { + message(ctx, LOG_ERR, "couldn't read data"); + return -1; + } + } + + *t = 0; + + if(trim) + { + while(t > ctx->line && isspace(*(t - 1))) + { + --t; + *t = 0; + } + } + + ctx->linelen = t - ctx->line; + log_fd_data(ctx, ctx->line, fd, 1); + + return ctx->linelen; +} + +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len) +{ + int r; + + while(len > 0) + { + r = write(*fd, buf, len); + + if(r > 0) + { + buf += r; + len -= r; + } + + else if(r == -1) + { + if(errno == EAGAIN) + continue; + + if(errno == EPIPE) + { + shutdown(*fd, SHUT_RDWR); + *fd = -1; + } + + message(ctx, LOG_ERR, "couldn't write data to socket"); + return -1; + } + } + + return 0; +} + +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf) +{ + int len = strlen(buf); + + if(*fd == -1) + { + message(ctx, LOG_ERR, "connection closed. can't write data."); + return -1; + } + + log_fd_data(ctx, buf, fd, 0); + return write_data_raw(ctx, fd, buf, len); +} diff --git a/common/smtppass.h b/common/smtppass.h new file mode 100644 index 0000000..4931e5e --- /dev/null +++ b/common/smtppass.h @@ -0,0 +1,24 @@ +#ifndef __CLAMSMTPD_H__ +#define __CLAMSMTPD_H__ + +/* A generous maximum line length. */ +#define LINE_LENGTH 2000 + +typedef struct clamsmtp_context +{ + unsigned int id; /* Identifier for the connection */ + + int client; /* Connection to client */ + int server; /* Connection to server */ + int clam; /* Connection to clamd */ + + char line[LINE_LENGTH]; /* Working buffer */ + int linelen; /* Length of valid data in above */ +} +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 */ + +#endif /* __CLAMSMTPD_H__ */ diff --git a/common/sock_any.c b/common/sock_any.c new file mode 100644 index 0000000..acac8ee --- /dev/null +++ b/common/sock_any.c @@ -0,0 +1,275 @@ + +#include +#include +#include +#include +#include +#include + +#include "sock_any.h" + +#include + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport) +{ + size_t l; + char buf[256]; /* TODO: Use a constant */ + char* t; + char* t2; + + memset(any, 0, sizeof(*any)); + + /* Just a port? */ + do + { + #define PORT_CHARS "0123456789" + #define PORT_MIN 1 + #define PORT_MAX 5 + + int port = 0; + + l = strspn(addr, PORT_CHARS); + if(l < PORT_MIN || l > PORT_MAX || addr[l] != 0) + break; + + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + + any->s.in.sin_family = AF_INET; + any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port)); + any->s.in.sin_addr.s_addr = 0; + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + + /* Look and see if we can parse an ipv4 address */ + do + { + #define IPV4_PORT_CHARS + #define IPV4_CHARS "0123456789." + #define IPV4_MIN 3 + #define IPV4_MAX 18 + + int port = 0; + t = NULL; + + l = strlen(addr); + if(l < IPV4_MIN || l > IPV4_MAX) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV4_CHARS); + if(l < IPV4_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0 && buf[l] != ':') + break; + + /* Get the port out */ + if(buf[l] != 0) + { + t = buf + l + 1; + buf[l] = 0; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + any->s.in.sin_family = AF_INET; + any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port)); + + if(inet_pton(AF_INET, buf, &(any->s.in.sin_addr)) <= 0) + break; + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + +#ifdef HAVE_INET6 + do + { + #define IPV6_CHARS "0123456789:" + #define IPV6_MIN 3 + #define IPV6_MAX 48 + + int port = -1; + t = NULL; + + l = strlen(addr); + if(l < IPV6_MIN || l > IPV6_MAX) + break; + + /* If it starts with a '[' then we can get port */ + if(buf[0] == '[') + { + port = 0; + addr++; + } + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV6_CHARS); + if(l < IPV6_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0) + { + /* If had bracket, then needs to end with a bracket */ + if(port != 0 || buf[l] != ']') + break; + + /* Get the port out */ + t = buf + l + 1; + + if(*t = ':') + t++; + } + + if(t) + { + port = strtol(t, &t, 10); + if(*t || port <= 0 || port >= 65536) + break; + } + + any->s.in6.sin6_family = AF_INET6; + any->s.in6.sin6_port = htons((unsigned short)port <= 0 : defport : port); + + if(inet_pton(AF_INET6, buf, &(any->s.in6.sin6_addr)) >= 0) + break; + + any->namelen = sizeof(any->s.in6); + return AF_INET6; + } + while(0); +#endif + + /* A unix socket path */ + do + { + /* No colon and must have a path component */ + if(strchr(addr, ':') || !strchr(addr, '/')) + break; + + l = strlen(addr); + if(l >= sizeof(any->s.un.sun_path)) + break; + + any->s.un.sun_family = AF_UNIX; + strcpy(any->s.un.sun_path, addr); + + any->namelen = sizeof(any->s.un) - (sizeof(any->s.un.sun_path) - l); + return AF_UNIX; + } + while(0); + + /* A DNS name and a port? */ + do + { + struct addrinfo* res; + int port = 0; + t = NULL; + + l = strlen(addr); + if(l >= 255 || !isalpha(addr[0])) + break; + + /* Some basic illegal character checks */ + if(strcspn(addr, " /\\") != l) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + t = strchr(buf, ':'); + if(t) + { + *t = 0; + t++; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + /* Try and resolve the domain name */ + if(getaddrinfo(buf, NULL, NULL, &res) != 0 || !res) + break; + + memcpy(&(any->s.a), res->ai_addr, sizeof(struct sockaddr)); + any->namelen = res->ai_addrlen; + freeaddrinfo(res); + + port = htons((unsigned short)(port <= 0 ? defport : port)); + + switch(any->s.a.sa_family) + { + case PF_INET: + any->s.in.sin_port = port; + break; +#ifdef HAVE_INET6 + case PF_INET6: + any->s.in6.sin6_port = port; + break; +#endif + }; + + return any->s.a.sa_family; + } + while(0); + + return -1; +} + +int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen) +{ + int len = 0; + + switch(any->s.a.sa_family) + { + case AF_UNIX: + len = strlen(any->s.un.sun_path); + if(addrlen < len + 1) + { + errno = ENOSPC; + return -1; + } + + strcpy(addr, any->s.un.sun_path); + break; + + case AF_INET: + if(inet_ntop(any->s.a.sa_family, &(any->s.in.sin_addr), addr, addrlen) == NULL) + return -1; + break; + +#ifdef HAVE_INET6 + case AF_INET6: + if(inet_ntop(any->s.a.sa_family, &(any->s.in6.sin6_addr), addr, addrlen) == NULL) + return -1; + break; +#endif + + default: + errno = EAFNOSUPPORT; + return -1; + } + + return 0; +} diff --git a/common/sock_any.h b/common/sock_any.h new file mode 100644 index 0000000..693bd2a --- /dev/null +++ b/common/sock_any.h @@ -0,0 +1,33 @@ + +#ifndef __SOCK_ANY_H__ +#define __SOCK_ANY_H__ + +#include +#include +#include + +struct sockaddr_any +{ + union _sockaddr_any + { + /* The header */ + struct sockaddr a; + + /* The different types */ + struct sockaddr_un un; + struct sockaddr_in in; +#ifdef HAVE_INET6 + struct sockaddr_in6 in6; +#endif + } s; + size_t namelen; +}; + +#define SANY_ADDR(any) ((any).s.a) +#define SANY_LEN(any) ((any).namelen) +#define SANY_TYPE(any) ((any).s.a.sa_family) + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport); +int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen); + +#endif /* __SOCK_ANY_H__ */ diff --git a/common/stringx.c b/common/stringx.c new file mode 100644 index 0000000..f0dea56 --- /dev/null +++ b/common/stringx.c @@ -0,0 +1,271 @@ + +#include + +#include +#include +#include +#include +#include + +#include "usuals.h" +#include "compat.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ---------------------------------------------------------------------------------- + * Logging + */ + +const char kMsgDelimiter[] = ": "; +#define MAX_MSGLEN 256 + +static void vmessage(clamsmtp_context_t* ctx, int level, int err, + const char* msg, va_list ap) +{ + size_t len; + char* m; + int e = errno; + + if(g_daemonized) + { + if(level >= LOG_DEBUG) + return; + } + else + { + if(g_debuglevel < level) + return; + } + + ASSERT(msg); + + len = strlen(msg) + 20 + MAX_MSGLEN; + m = (char*)alloca(len); + + if(m) + { + if(ctx) + snprintf(m, len, "%06X: %s%s", ctx->id, msg, err ? ": " : ""); + else + snprintf(m, len, "%s%s", msg, err ? ": " : ""); + + if(err) + strerror_r(e, m + strlen(m), MAX_MSGLEN); + + m[len - 1] = 0; + msg = m; + } + + /* Either to syslog or stderr */ + if(g_daemonized) + vsyslog(level, msg, ap); + else + vwarnx(msg, ap); +} + +void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vmessage(ctx, level, 0, msg, ap); + va_end(ap); +} + +void message(clamsmtp_context_t* ctx, int level, const char* msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vmessage(ctx, level, 1, msg, ap); + va_end(ap); +} + +#define MAX_LOG_LINE 79 + +void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read) +{ + #define offsetof(s, m) ((size_t)&(((s*)0)->m)) + #define ismember(o, m) (((char*)(m)) < (((char*)(o)) + sizeof(*(o)))) + #define ptrdiff(o, t) + + char prefix[16]; + const char* t; + + ASSERT(ctx); + ASSERT(ismember(ctx, fd)); + + switch((char*)fd - (char*)ctx) + { + case offsetof(clamsmtp_context_t, client): + strcpy(prefix, "CLIENT "); + break; + case offsetof(clamsmtp_context_t, server): + strcpy(prefix, "SERVER "); + break; + case offsetof(clamsmtp_context_t, clam): + strcpy(prefix, "CLAM "); + break; + default: + strcpy(prefix, "???? "); + break; + } + + strcat(prefix, read ? "< " : "> "); + log_data(ctx, data, prefix); +} + + +void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix) +{ + char buf[MAX_LOG_LINE + 1]; + int pos, len; + + for(;;) + { + data += strspn(data, "\r\n"); + + if(!*data) + break; + + pos = strcspn(data, "\r\n"); + + len = pos < MAX_LOG_LINE ? pos : MAX_LOG_LINE; + memcpy(buf, data, len); + buf[len] = 0; + + messagex(ctx, LOG_DEBUG, "%s%s", prefix, buf); + + data += pos; + } +} + +/* ---------------------------------------------------------------------------------- + * Parsing + */ + +int is_first_word(const char* line, const char* word, int len) +{ + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + while(*line && isspace(*line)) + line++; + + if(strncasecmp(line, word, len) != 0) + return 0; + + line += len; + return !*line || isspace(*line); +} + +int check_first_word(const char* line, const char* word, int len, char* delims) +{ + const char* t; + int found = 0; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line; + + while(*t && strchr(delims, *t)) + t++; + + if(strncasecmp(t, word, len) != 0) + return 0; + + t += len; + + while(*t && strchr(delims, *t)) + { + found = 1; + t++; + } + + return (!*t || found) ? t - line : 0; +} + +int is_last_word(const char* line, const char* word, int len) +{ + const char* t; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line + strlen(line); + + while(t > line && isspace(*(t - 1))) + --t; + + if(t - len < line) + return 0; + + return strncasecmp(t - len, word, len) == 0; +} + +int is_blank_line(const char* line) +{ + /* Small optimization */ + if(!*line) + return 1; + + while(*line && isspace(*line)) + line++; + + return *line == 0; +} + +/* ----------------------------------------------------------------------- + * Locking + */ + +void plock() +{ + int r; + +#ifdef _DEBUG + int wait = 0; +#endif + +#ifdef _DEBUG + r = pthread_mutex_trylock(&g_mutex); + if(r == EBUSY) + { + wait = 1; + message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self()); + r = pthread_mutex_lock(&g_mutex); + } + +#else + r = pthread_mutex_lock(&g_mutex); + +#endif + + if(r != 0) + { + errno = r; + message(NULL, LOG_CRIT, "threading problem. couldn't lock mutex"); + } + +#ifdef _DEBUG + else if(wait) + { + message(NULL, LOG_DEBUG, "thread unblocked: %d", pthread_self()); + } +#endif +} + +void punlock() +{ + int r = pthread_mutex_unlock(&g_mutex); + if(r != 0) + { + errno = r; + message(NULL, LOG_CRIT, "threading problem. couldn't unlock mutex"); + } +} + diff --git a/common/stringx.h b/common/stringx.h new file mode 100644 index 0000000..54b8ea6 --- /dev/null +++ b/common/stringx.h @@ -0,0 +1,19 @@ + +#ifndef __UTIL_H__ +#define __UTIL_H__ + +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); +int is_blank_line(const char* line); + +void plock(); +void punlock(); + +#endif /* __UTIL_H__ */ diff --git a/common/usuals.h b/common/usuals.h new file mode 100644 index 0000000..e14ecf5 --- /dev/null +++ b/common/usuals.h @@ -0,0 +1,38 @@ + + +#ifndef __USUALS_H__ +#define __USUALS_H__ + +#include + +#include "config.h" + +#include +#include +#include +#include + +#include "compat.h" + +#ifndef NULL +#define NULL 0 +#endif + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#define countof(x) (sizeof(x) / sizeof(x[0])) + +#ifdef _DEBUG + #include "assert.h" + #define ASSERT assert +#else + #define ASSERT +#endif + +#endif /* __USUALS_H__ */ diff --git a/configure.in b/configure.in new file mode 100644 index 0000000..79c997a --- /dev/null +++ b/configure.in @@ -0,0 +1,91 @@ +dnl +dnl Copyright (c) 2004, Nate Nielsen +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions +dnl are met: +dnl +dnl * Redistributions of source code must retain the above +dnl copyright notice, this list of conditions and the +dnl following disclaimer. +dnl * Redistributions in binary form must reproduce the +dnl above copyright notice, this list of conditions and +dnl the following disclaimer in the documentation and/or +dnl other materials provided with the distribution. +dnl * The names of contributors to this software may not be +dnl used to endorse or promote products derived from this +dnl software without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +dnl FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +dnl COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +dnl INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +dnl BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +dnl OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +dnl AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +dnl OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +dnl THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +dnl DAMAGE. +dnl +dnl +dnl CONTRIBUTORS +dnl Nate Nielsen +dnl + +dnl Process this file with autoconf to produce a configure script. +AC_INIT(clamsmtp, 0.2, nielsen@memberwebs.com) +AM_INIT_AUTOMAKE(clamsmtp, 0.2) + +LDFLAGS="$LDFLAGS -L/usr/local/lib" +CFLAGS="$CFLAGS -I/usr/local/include" + +AC_CONFIG_SRCDIR([src/clamsmtpd.c]) +AM_CONFIG_HEADER([config.h]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_LN_S +AC_PROG_MAKE_SET + +# Debug mode +AC_ARG_ENABLE(debug, + AC_HELP_STRING([--enable-debug], + [Compile binaries in debug mode])) + +if test "$enable_debug" = "yes"; then + CFLAGS="$CFLAGS -g -O0" + AC_DEFINE_UNQUOTED(_DEBUG, 1, [In debug mode]) + echo "enabling debug compile mode" +fi + +# TODO: Figure out why we need this wierd hack +ACX_PTHREAD( , [echo "ERROR: Pthread support not found."; exit 1] ) + +LIBS="$PTHREAD_LIBS $LIBS" +CFLAGS="$CFLAGS $PTHREAD_CFLAGS" + +# Checks for header files. +AC_HEADER_STDC +AC_CHECK_HEADERS([unistd.h stdio.h stddef.h fcntl.h stdlib.h assert.h errno.h stdarg.h err.h string.h], , + [echo "ERROR: Required C header missing"; exit 1]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_SIZE_T + +AC_CHECK_DECL(PTHREAD_MUTEX_ERRORCHECK_NP, [AC_DEFINE(HAVE_ERR_MUTEX, 1, "Error Mutex Type")], + [AC_CHECK_DECL(PTHREAD_MUTEX_ERRORCHECK, [AC_DEFINE(HAVE_ERR_MUTEX, 2)], + [echo "ERROR: Missing error checking mutex functionality in pthread.h"], + [ #include ])], [ #include ]) + +# Required Functions +AC_CHECK_FUNCS([memset strerror malloc realloc getopt strchr tolower getaddrinfo], , + [echo "ERROR: Required function missing"; exit 1]) +AC_CHECK_FUNCS([strlwr strlcat strlcpy strncat strncpy]) + +AC_CONFIG_FILES([Makefile src/Makefile]) +AC_OUTPUT diff --git a/src/.cvsignore b/src/.cvsignore new file mode 100644 index 0000000..1682adb --- /dev/null +++ b/src/.cvsignore @@ -0,0 +1,5 @@ +clamsmtpd +*.o +Makefile +Makefile.in +.deps diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..c8efa41 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,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 + +man_MANS = clamsmtpd.8 +EXTRA_DIST = $(man_MANS) diff --git a/src/clamsmtpd.8 b/src/clamsmtpd.8 new file mode 100644 index 0000000..4d3b55c --- /dev/null +++ b/src/clamsmtpd.8 @@ -0,0 +1,132 @@ +.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 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 t Ar timeout +.Ar serveraddr +.Sh DESCRIPTION +.Nm +is an SMTP filter that allows you to check for viruses via using ClamAV +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. Email with +viruses are rejected 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 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 25 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 t +.Ar timeout +is the number of seconds to wait while reading data from network connections. +[Default: 180 seconds] +.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 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. +.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 new file mode 100644 index 0000000..71e5a3f --- /dev/null +++ b/src/clamsmtpd.c @@ -0,0 +1,1215 @@ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "usuals.h" +#include "compat.h" +#include "sock_any.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ----------------------------------------------------------------------- + * Structures + */ + +typedef struct clamsmtp_thread +{ + pthread_t tid; /* Written to by the main thread */ + int fd; /* The file descriptor or -1 */ +} +clamsmtp_thread_t; + +#define LINE_TOO_LONG(ctx) ((ctx)->linelen >= (LINE_LENGTH - 2)) +#define RETURN(x) { ret = x; goto cleanup; } + +/* ----------------------------------------------------------------------- + * Strings + */ + +#define KL(s) ((sizeof(s) - 1) / sizeof(char)) + +#define SMTP_TOOLONG "500 Line too long\r\n" +#define SMTP_STARTBUSY "554 Server Busy\r\n" +#define SMTP_STARTFAILED "554 Local Error\r\n" +#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected\r\n" +#define SMTP_DATAINTERMED "354 Start mail input; end with .\r\n" +#define SMTP_FAILED "451 Local Error\r\n" + +#define SMTP_DATA "DATA\r\n" +#define SMTP_DELIMS "\r\n\t :" + +#define FROM_CMD "MAIL FROM" +#define TO_CMD "RCPT TO" +#define DATA_CMD "DATA" +#define RSET_CMD "RSET" + +#define DATA_END_SIG "\r\n.\r\n" + +#define DATA_RSP "354" + +#define CLAM_OK "OK" +#define CLAM_ERROR "ERROR" +#define CLAM_FOUND "FOUND" + +#define CONNECT_RSP "PONG" +#define CLAM_SCAN "SCAN " + +#define CLAM_CONNECT "SESSION\nPING\n" +#define CLAM_DISCONNECT "END\n" + +/* ----------------------------------------------------------------------- + * Default Settings + */ + +#define DEFAULT_SOCKET "0.0.0.0: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\r\n" + + +/* ----------------------------------------------------------------------- + * 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; + +const char* g_header = DEFAULT_HEADER; /* The header to add to email */ +const char* g_directory = _PATH_TMP; /* The directory for temp files */ +unsigned int g_unique_id = 0x00001000; /* For connection ids */ + +/* 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; + + +/* ----------------------------------------------------------------------- + * Forward Declarations + */ + +static usage(); +static void on_quit(int signal); +static void write_pid(const char* pid); +static void connection_loop(int sock); +static void* thread_main(void* arg); +static int smtp_passthru(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 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 read_line(clamsmtp_context_t* ctx, int* fd, int trim); +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf); +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len); + + +int main(int argc, char* argv[]) +{ + const char* listensock = DEFAULT_SOCKET; + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + char* pidfile = NULL; + int daemonize = 1; + int sock; + int true = 1; + int ch = 0; + char* t; + + /* Parse the arguments nicely */ + while((ch = getopt(argc, argv, "c:d:D:h:l:m:p:t:")) != -1) + { + switch(ch) + { + /* Change the CLAM socket */ + case 'c': + g_clamname = optarg; + break; + + /* Don't daemonize */ + case 'd': + daemonize = 0; + g_debuglevel = strtol(optarg, &t, 10); + if(*t || g_debuglevel > 4) + errx(1, "invalid debug log level"); + g_debuglevel += LOG_ERR; + break; + + /* The directory for the files */ + case 'D': + g_directory = optarg; + break; + + /* The header to add */ + case 'h': + if(strlen(optarg) == 0) + g_header = NULL; + else + g_header = optarg; + break; + + /* Change our listening port */ + case 'l': + listensock = optarg; + 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"); + break; + + /* Write out a pid file */ + case 'p': + 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); + break; + + /* Usage information */ + case '?': + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if(argc != 1) + usage(); + + g_outname = argv[0]; + + messagex(NULL, LOG_DEBUG, "starting up..."); + + /* Parse all the addresses */ + if(sock_any_pton(listensock, &addr, DEFAULT_PORT) == -1) + errx(1, "invalid listen socket name or ip: %s", listensock); + if(sock_any_pton(g_outname, &g_outaddr, 25) == -1) + errx(1, "invalid connect socket name or ip: %s", g_outname); + if(sock_any_pton(g_clamname, &g_clamaddr, 0) == -1) + errx(1, "invalid clam socket name: %s", g_clamname); + + /* Create the socket */ + sock = socket(SANY_TYPE(addr), SOCK_STREAM, 0); + if(sock < 0) + err(1, "couldn't open socket"); + + 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(bind(sock, &SANY_ADDR(addr), SANY_LEN(addr)) != 0) + err(1, "couldn't bind to address: %s", listensock); + + /* Let 5 connections queue up */ + if(listen(sock, 5) != 0) + err(1, "couldn't listen on socket"); + + messagex(NULL, LOG_DEBUG, "created socket: %s", listensock); + + if(daemonize) + { + /* 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"); + g_daemonized = 1; + + /* Open the system log */ + openlog("clamsmtp", 0, LOG_MAIL); + } + + if(pidfile) + write_pid(pidfile); + + /* 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); + + messagex(NULL, LOG_DEBUG, "accepting connections"); + + connection_loop(sock); + + messagex(NULL, LOG_DEBUG, "stopped"); + + return 0; +} + +static void connection_loop(int sock) +{ + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + int fd, i, x, r; + + /* Create the thread buffers */ + threads = (clamsmtp_thread_t*)calloc(g_maxthreads, 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) + { + 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"); + g_quit = 1; + break; + }; + + if(g_quit) + break; + + continue; + } + + /* Look for thread and also clean up others */ + for(i = 0; i < g_maxthreads; 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; + } + } + + /* 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) + { + errno = r; + message(NULL, LOG_ERR, "couldn't create thread"); + g_quit = 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)", g_maxthreads); + + /* TODO: Respond with a too many connections message */ + write_data(NULL, &fd, SMTP_STARTBUSY); + shutdown(fd, SHUT_RDWR); + } + } + + messagex(NULL, LOG_INFO, "waiting for threads to quit"); + + /* Quit all threads here */ + for(i = 0; i < g_maxthreads; i++) + { + /* Clean up quit threads */ + if(threads[i].tid != 0) + { + if(threads[i].fd != -1) + shutdown(threads[i].fd, SHUT_RDWR); + + pthread_join(threads[i].tid, NULL); + } + } + + /* Close the mutex */ + pthread_mutex_destroy(&g_mutex); + pthread_mutexattr_destroy(&g_mutexattr); +} + +static void on_quit(int signal) +{ + g_quit = 1; + + /* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */ +} + +static int usage() +{ + fprintf(stderr, "clamsmtp [-c clamaddr] [-d debuglevel] [-D tmpdir] [-h header]" + "[-l listenaddr] [-m maxconn] [-p pidfile] [-t timeout] serveraddr\n"); + return 2; +} + +static void write_pid(const char* pidfile) +{ + 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); + } +} + +static void* thread_main(void* arg) +{ + clamsmtp_thread_t* thread = (clamsmtp_thread_t*)arg; + char peername[MAXPATHLEN]; + struct sockaddr_any addr; + clamsmtp_context_t ctx; + int r; + + ASSERT(thread); + + siginterrupt(SIGINT, 1); + siginterrupt(SIGTERM, 1); + + memset(&ctx, 0, sizeof(ctx)); + + plock(); + ctx.client = thread->fd; + punlock(); + + ctx.server = -1; + ctx.clam = -1; + + ASSERT(ctx.client != -1); + + /* Assign a unique id to the connection */ + ctx.id = g_unique_id++; + + /* Get the peer name */ + if(getpeername(ctx.client, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1 || + sock_any_ntop(&addr, peername, MAXPATHLEN) == -1) + messagex(&ctx, LOG_WARNING, "couldn't get peer address"); + else + messagex(&ctx, LOG_INFO, "accepted connection from: %s", peername); + + /* call the processor */ + r = smtp_passthru(&ctx); + + /* Close the incoming connection if neccessary */ + if(ctx.client != -1) + shutdown(ctx.client, SHUT_RDWR); + + messagex(&ctx, LOG_INFO, "closed client connection"); + + /* mark this as done */ + plock(); + thread->fd = -1; + punlock(); + + return (void*)(r == 0 ? 0 : 1); +} + +static int smtp_passthru(clamsmtp_context_t* ctx) +{ + char logline[LINE_LENGTH]; + int processing = 0; + int r, ret = 0; + fd_set mask; + + ASSERT(ctx->server == -1); + + if((ctx->server = socket(SANY_TYPE(g_outaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->server, &SANY_ADDR(g_outaddr), SANY_LEN(g_outaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to %s", g_outname); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "connected to server: %s", g_outname); + + if(connect_clam(ctx) == -1) + RETURN(-1); + + /* This changes the error code sent to the client when an + * error occurs. See cleanup below */ + processing = 1; + logline[0] = 0; + + for(;;) + { + FD_ZERO(&mask); + + FD_SET(ctx->client, &mask); + FD_SET(ctx->server, &mask); + + switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_timeout)) + { + case 0: + message(ctx, LOG_ERR, "network operation timed out"); + RETURN(-1); + case -1: + message(ctx, LOG_ERR, "couldn't select on sockets"); + RETURN(-1); + }; + + /* Client has data available, read a line and process */ + if(FD_ISSET(ctx->client, &mask)) + { + if(read_line(ctx, &(ctx->client), 0) == -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(write_data(ctx, &(ctx->server), SMTP_TOOLONG) == -1) + RETURN(-1); + } + + else + { + if(is_first_word(ctx->line, DATA_CMD, KL(DATA_CMD))) + { + /* Send back the intermediate response to the client */ + if(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; + } + + /* All other commands just get passed through to server */ + else + { + + /* Append recipients to log line */ + 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; + + if(write_data(ctx, &(ctx->server), ctx->line) == -1) + RETURN(-1); + } + } + + continue; + } + + /* Server has data available, read a line and forward */ + if(FD_ISSET(ctx->server, &mask)) + { + if(read_line(ctx, &(ctx->server), 0) == -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"); + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + RETURN(-1); + + continue; + } + } + +cleanup: + + disconnect_clam(ctx); + + if(ret == -1 && ctx->client != -1) + { + write_data(ctx, &(ctx->client), + processing ? SMTP_FAILED : SMTP_STARTFAILED); + } + + if(ctx->server != -1) + { + shutdown(ctx->server, SHUT_RDWR); + messagex(ctx, LOG_DEBUG, "closed server connection"); + } + + 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 */ + while(*line && isspace(*line)) + *line++; + + strlcat(logline, line, l); + t = logline + strlen(logline); + + /* Skip later white space */ + while(t > logline && isspace(*(t - 1))) + *(--t) = 0; +} + +static int connect_clam(clamsmtp_context_t* ctx) +{ + int r, len = -1; + int ret = 0; + + ASSERT(ctx); + ASSERT(ctx->clam == -1); + + if((ctx->clam = socket(SANY_TYPE(g_clamaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->clam, &SANY_ADDR(g_clamaddr), SANY_LEN(g_clamaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to clamd at %s", g_clamname); + RETURN(-1); + } + + read_junk(ctx, ctx->clam); + + /* Send a session and a check header to ClamAV */ + + if(write_data(ctx, &(ctx->clam), "SESSION\n") == -1) + RETURN(-1); + + read_junk(ctx, ctx->clam); +/* + if(write_data(ctx, &(ctx->clam), "PING\n") == -1 || + read_line(ctx, &(ctx->clam), 1) == -1) + RETURN(-1); + + if(strcmp(ctx->line, CONNECT_RESPONSE) != 0) + { + message(ctx, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); + RETURN(-1); + } +*/ + messagex(ctx, LOG_DEBUG, "connected to clamd: %s", g_clamname); + +cleanup: + + if(ret < 0) + { + if(ctx->clam != -1) + { + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam == -1; + } + } + + return ret; +} + +static int disconnect_clam(clamsmtp_context_t* ctx) +{ + if(ctx->clam == -1) + return 0; + + if(write_data(ctx, &(ctx->clam), CLAM_DISCONNECT) != -1) + read_junk(ctx, ctx->clam); + + messagex(ctx, LOG_DEBUG, "disconnected from clamd"); + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam = -1; + return 0; +} + +static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline) +{ + int len; + + ASSERT(LINE_LENGTH < MAXPATHLEN + 32); + + strcpy(ctx->line, CLAM_SCAN); + strcat(ctx->line, tempname); + strcat(ctx->line, "\n"); + + if(write_data(ctx, &(ctx->clam), ctx->line) == -1) + return -1; + + len = read_line(ctx, &(ctx->clam), 1); + if(len == 0) + { + messagex(ctx, LOG_ERR, "clamd disconnected unexpectedly"); + return -1; + } + + if(is_last_word(ctx->line, CLAM_OK, KL(CLAM_OK))) + { + add_to_logline(logline, "status=", "CLEAN"); + messagex(ctx, LOG_DEBUG, "no virus"); + return 0; + } + + if(is_last_word(ctx->line, CLAM_FOUND, KL(CLAM_FOUND))) + { + len = strlen(tempname); + + if(ctx->linelen > len) + add_to_logline(logline, "status=VIRUS:", ctx->line + len + 1); + else + add_to_logline(logline, "status=", "VIRUS"); + + messagex(ctx, LOG_DEBUG, "found virus"); + return 1; + } + + if(is_last_word(ctx->line, CLAM_ERROR, KL(CLAM_ERROR))) + { + messagex(ctx, LOG_ERR, "clamav error: %s", ctx->line); + return -1; + } + + messagex(ctx, LOG_ERR, "unexepected response from clamd: %s", ctx->line); + return -1; +} + +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_directory, MAXPATHLEN); + strlcat(buf, "/clamsmtp.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); + } + + switch(r) + { + + /* + * There was an error tell the client. We haven't notified + * the server about any of this yet + */ + case -1: + if(write_data(ctx, &(ctx->client), SMTP_FAILED)) + RETURN(-1); + break; + + /* + * No virus was found. Now we initiate a connection to the server + * and transfer the file to it. + */ + case 0: + if(complete_data_transfer(ctx, buf) == -1) + RETURN(-1); + break; + + /* + * A virus was found, just send back a simple message to the 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: + if(write_data(ctx, &(ctx->client), SMTP_DATAVIRUS) == -1) + RETURN(-1); + break; + + default: + ASSERT(0 && "Invalid clam_scan_file return value"); + break; + }; + +cleanup: + if(havefile) + { + messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf); + unlink(buf); + } + + return ret; +} + +static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname) +{ + ASSERT(ctx); + ASSERT(tempname); + + /* Ask the server for permission to send data */ + if(write_data(ctx, &(ctx->server), SMTP_DATA) == -1) + return -1; + + if(read_server_response(ctx) == -1) + return -1; + + /* If server returns an error then tell the client */ + if(!is_first_word(ctx->line, DATA_RSP, KL(DATA_RSP))) + { + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + messagex(ctx, LOG_DEBUG, "server refused data transfer"); + + return 0; + } + + /* Now pull up the file and send it to the server */ + if(transfer_from_file(ctx, tempname) == -1) + { + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return -1; + } + + /* Okay read the response from the server and echo it to the client */ + if(read_server_response(ctx) == -1) + return -1; + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + return 0; +} + +static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname) +{ + /* If there aren't any lines in the message and just an + end signature then start at the dot. */ + const char* topsig = strchr(DATA_END_SIG, '.'); + const char* cursig = topsig; + FILE* tfile = NULL; + int tfd = -1; + int ret = 0; + char ch; + int count = 0; + + ASSERT(topsig != NULL); + + 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(read(ctx->client, &ch, 1)) + { + case 0: + messagex(ctx, LOG_ERR, "unexpected end of data from client"); + RETURN(-1); + + case -1: + message(ctx, LOG_ERR, "error reading from client"); + RETURN(-1); + }; + + if((char)ch != *cursig) + { + /* Write out the part of the sig we kept back */ + if(cursig != topsig) + { + /* We check errors on this later */ + fwrite(topsig, 1, cursig - topsig, tfile); + count += (cursig - topsig); + } + + /* We've seen at least one char not in the sig */ + cursig = topsig = DATA_END_SIG; + } + + /* The sig may have been reset above so check again */ + if((char)ch == *cursig) + { + cursig++; + + if(!*cursig) + { + /* We found end of data */ + break; + } + } + + else + { + fputc(ch, tfile); + count++; + } + } + + 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 == -1) + { + 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; + const char* t; + const char* e; + int header = 0; + int ret = 0; + int len, r; + + file = fopen(filename, "r"); + if(file == NULL) + { + message(ctx, LOG_ERR, "couldn't open temporary file: %s", filename); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "opened temporary file: %s", filename); + + while(fgets(ctx->line, LINE_LENGTH, file) != NULL) + { + if(g_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(write_data_raw(ctx, &(ctx->server), g_header, strlen(g_header)) == -1) + RETURN(-1); + } + + header = 1; + } + + if(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(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; +} + +static int read_server_response(clamsmtp_context_t* ctx) +{ + /* Read response line from the server */ + if(read_line(ctx, &(ctx->server), 0) == -1) + return -1; + + if(ctx->linelen == 0) + { + messagex(ctx, LOG_ERR, "server disconnected unexpectedly"); + + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return 0; + } + + if(LINE_TOO_LONG(ctx)) + messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); + + return 0; +} + +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 = buf; + + while(*t && isspace(*t)) + t++; + + if(!said && *t) + { + messagex(ctx, LOG_WARNING, "received junk data from daemon"); + said = 1; + } + } + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); +} + +static int read_line(clamsmtp_context_t* ctx, int* fd, int trim) +{ + int l; + char* t; + const char* e; + + if(*fd == -1) + { + messagex(ctx, LOG_WARNING, "tried to read from a closed connection"); + return 0; + } + + ctx->line[0] = 0; + e = ctx->line + (LINE_LENGTH - 1); + + for(t = ctx->line; t < e; ++t) + { + l = read(*fd, (void*)t, sizeof(char)); + + /* We got a character */ + if(l == 1) + { + /* End of line */ + if(*t == '\n') + { + ++t; + break; + } + + /* We skip spaces at the beginning if trimming */ + if(trim && t == ctx->line && isspace(*t)) + continue; + } + + /* If it's the end of file then return that */ + else if(l == 0) + { + /* Put in an extra line if there was anything */ + if(t > ctx->line && !trim) + { + *t = '\n'; + ++t; + } + + break; + } + + /* Transient errors */ + else if(l == -1 && errno == EAGAIN) + continue; + + /* Fatal errors */ + else if(l == -1) + { + message(ctx, LOG_ERR, "couldn't read data"); + return -1; + } + } + + *t = 0; + + if(trim) + { + while(t > ctx->line && isspace(*(t - 1))) + { + --t; + *t = 0; + } + } + + ctx->linelen = t - ctx->line; + log_fd_data(ctx, ctx->line, fd, 1); + + return ctx->linelen; +} + +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len) +{ + int r; + + while(len > 0) + { + r = write(*fd, buf, len); + + if(r > 0) + { + buf += r; + len -= r; + } + + else if(r == -1) + { + if(errno == EAGAIN) + continue; + + if(errno == EPIPE) + { + shutdown(*fd, SHUT_RDWR); + *fd = -1; + } + + message(ctx, LOG_ERR, "couldn't write data to socket"); + return -1; + } + } + + return 0; +} + +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf) +{ + int len = strlen(buf); + + if(*fd == -1) + { + message(ctx, LOG_ERR, "connection closed. can't write data."); + return -1; + } + + log_fd_data(ctx, buf, fd, 0); + return write_data_raw(ctx, fd, buf, len); +} diff --git a/src/clamsmtpd.h b/src/clamsmtpd.h new file mode 100644 index 0000000..4931e5e --- /dev/null +++ b/src/clamsmtpd.h @@ -0,0 +1,24 @@ +#ifndef __CLAMSMTPD_H__ +#define __CLAMSMTPD_H__ + +/* A generous maximum line length. */ +#define LINE_LENGTH 2000 + +typedef struct clamsmtp_context +{ + unsigned int id; /* Identifier for the connection */ + + int client; /* Connection to client */ + int server; /* Connection to server */ + int clam; /* Connection to clamd */ + + char line[LINE_LENGTH]; /* Working buffer */ + int linelen; /* Length of valid data in above */ +} +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 */ + +#endif /* __CLAMSMTPD_H__ */ diff --git a/src/compat.c b/src/compat.c new file mode 100644 index 0000000..baf1e34 --- /dev/null +++ b/src/compat.c @@ -0,0 +1,77 @@ + +#include "usuals.h" +#include "compat.h" + +#ifndef HAVE_REALLOCF + +void* reallocf(void* ptr, size_t size) +{ + void* ret = realloc(ptr, size); + + if(!ret && size) + free(ptr); + + return ret; +} + +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s) +{ + char* t = s; + while(*t) + { + *t = tolower(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s) +{ + char* t = s; + while(*t) + { + *t = toupper(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRLCPY + +#ifndef HAVE_STRNCPY +#error neither strncpy or strlcpy found +#endif + +void strlcpy(char* dest, const char* src, size_t count) +{ + if(count > 0) + { + strncpy(dest, src, count); + dest[count - 1] = 0; + } +} +#endif + +#ifndef HAVE_STRLCAT + +#ifndef HAVE_STRNCAT +#error neither strncat or strlcat found +#endif + +void strlcat(char* dest, const char* src, size_t count) +{ + if(count > 0) + { + strncat(dest, src, count); + dest[count - 1] = 0; + } +} +#endif + + diff --git a/src/compat.h b/src/compat.h new file mode 100644 index 0000000..6c20ae9 --- /dev/null +++ b/src/compat.h @@ -0,0 +1,51 @@ + + +#ifndef _COMPAT_H_ +#define _COMPAT_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#ifndef HAVE_STDARG_H +#error ERROR: Must have a working stdarg.h header +#else +#include +#endif + +#ifndef HAVE_REALLOCF +void* reallocf(void* p, size_t sz); +#endif + +#include + +/* TODO: Move this logic to configure */ +#if HAVE_ERR_MUTEX == 1 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK_NP +#else +# if HAVE_ERR_MUTEX == 2 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK +# else +# error "Need error checking mutex functionality" +# endif +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s); +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s); +#endif + +#ifndef HAVE_STRLCAT +void strlcat(char *dst, const char *src, size_t size); +#endif + +#ifndef HAVE_STRLCPY +void strlcpy(char *dst, const char *src, size_t size); +#endif + +#endif /* _COMPAT_H_ */ diff --git a/src/sock_any.c b/src/sock_any.c new file mode 100644 index 0000000..acac8ee --- /dev/null +++ b/src/sock_any.c @@ -0,0 +1,275 @@ + +#include +#include +#include +#include +#include +#include + +#include "sock_any.h" + +#include + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport) +{ + size_t l; + char buf[256]; /* TODO: Use a constant */ + char* t; + char* t2; + + memset(any, 0, sizeof(*any)); + + /* Just a port? */ + do + { + #define PORT_CHARS "0123456789" + #define PORT_MIN 1 + #define PORT_MAX 5 + + int port = 0; + + l = strspn(addr, PORT_CHARS); + if(l < PORT_MIN || l > PORT_MAX || addr[l] != 0) + break; + + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + + any->s.in.sin_family = AF_INET; + any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port)); + any->s.in.sin_addr.s_addr = 0; + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + + /* Look and see if we can parse an ipv4 address */ + do + { + #define IPV4_PORT_CHARS + #define IPV4_CHARS "0123456789." + #define IPV4_MIN 3 + #define IPV4_MAX 18 + + int port = 0; + t = NULL; + + l = strlen(addr); + if(l < IPV4_MIN || l > IPV4_MAX) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV4_CHARS); + if(l < IPV4_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0 && buf[l] != ':') + break; + + /* Get the port out */ + if(buf[l] != 0) + { + t = buf + l + 1; + buf[l] = 0; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + any->s.in.sin_family = AF_INET; + any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port)); + + if(inet_pton(AF_INET, buf, &(any->s.in.sin_addr)) <= 0) + break; + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + +#ifdef HAVE_INET6 + do + { + #define IPV6_CHARS "0123456789:" + #define IPV6_MIN 3 + #define IPV6_MAX 48 + + int port = -1; + t = NULL; + + l = strlen(addr); + if(l < IPV6_MIN || l > IPV6_MAX) + break; + + /* If it starts with a '[' then we can get port */ + if(buf[0] == '[') + { + port = 0; + addr++; + } + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV6_CHARS); + if(l < IPV6_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0) + { + /* If had bracket, then needs to end with a bracket */ + if(port != 0 || buf[l] != ']') + break; + + /* Get the port out */ + t = buf + l + 1; + + if(*t = ':') + t++; + } + + if(t) + { + port = strtol(t, &t, 10); + if(*t || port <= 0 || port >= 65536) + break; + } + + any->s.in6.sin6_family = AF_INET6; + any->s.in6.sin6_port = htons((unsigned short)port <= 0 : defport : port); + + if(inet_pton(AF_INET6, buf, &(any->s.in6.sin6_addr)) >= 0) + break; + + any->namelen = sizeof(any->s.in6); + return AF_INET6; + } + while(0); +#endif + + /* A unix socket path */ + do + { + /* No colon and must have a path component */ + if(strchr(addr, ':') || !strchr(addr, '/')) + break; + + l = strlen(addr); + if(l >= sizeof(any->s.un.sun_path)) + break; + + any->s.un.sun_family = AF_UNIX; + strcpy(any->s.un.sun_path, addr); + + any->namelen = sizeof(any->s.un) - (sizeof(any->s.un.sun_path) - l); + return AF_UNIX; + } + while(0); + + /* A DNS name and a port? */ + do + { + struct addrinfo* res; + int port = 0; + t = NULL; + + l = strlen(addr); + if(l >= 255 || !isalpha(addr[0])) + break; + + /* Some basic illegal character checks */ + if(strcspn(addr, " /\\") != l) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + t = strchr(buf, ':'); + if(t) + { + *t = 0; + t++; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + /* Try and resolve the domain name */ + if(getaddrinfo(buf, NULL, NULL, &res) != 0 || !res) + break; + + memcpy(&(any->s.a), res->ai_addr, sizeof(struct sockaddr)); + any->namelen = res->ai_addrlen; + freeaddrinfo(res); + + port = htons((unsigned short)(port <= 0 ? defport : port)); + + switch(any->s.a.sa_family) + { + case PF_INET: + any->s.in.sin_port = port; + break; +#ifdef HAVE_INET6 + case PF_INET6: + any->s.in6.sin6_port = port; + break; +#endif + }; + + return any->s.a.sa_family; + } + while(0); + + return -1; +} + +int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen) +{ + int len = 0; + + switch(any->s.a.sa_family) + { + case AF_UNIX: + len = strlen(any->s.un.sun_path); + if(addrlen < len + 1) + { + errno = ENOSPC; + return -1; + } + + strcpy(addr, any->s.un.sun_path); + break; + + case AF_INET: + if(inet_ntop(any->s.a.sa_family, &(any->s.in.sin_addr), addr, addrlen) == NULL) + return -1; + break; + +#ifdef HAVE_INET6 + case AF_INET6: + if(inet_ntop(any->s.a.sa_family, &(any->s.in6.sin6_addr), addr, addrlen) == NULL) + return -1; + break; +#endif + + default: + errno = EAFNOSUPPORT; + return -1; + } + + return 0; +} diff --git a/src/sock_any.h b/src/sock_any.h new file mode 100644 index 0000000..693bd2a --- /dev/null +++ b/src/sock_any.h @@ -0,0 +1,33 @@ + +#ifndef __SOCK_ANY_H__ +#define __SOCK_ANY_H__ + +#include +#include +#include + +struct sockaddr_any +{ + union _sockaddr_any + { + /* The header */ + struct sockaddr a; + + /* The different types */ + struct sockaddr_un un; + struct sockaddr_in in; +#ifdef HAVE_INET6 + struct sockaddr_in6 in6; +#endif + } s; + size_t namelen; +}; + +#define SANY_ADDR(any) ((any).s.a) +#define SANY_LEN(any) ((any).namelen) +#define SANY_TYPE(any) ((any).s.a.sa_family) + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport); +int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen); + +#endif /* __SOCK_ANY_H__ */ diff --git a/src/usuals.h b/src/usuals.h new file mode 100644 index 0000000..e14ecf5 --- /dev/null +++ b/src/usuals.h @@ -0,0 +1,38 @@ + + +#ifndef __USUALS_H__ +#define __USUALS_H__ + +#include + +#include "config.h" + +#include +#include +#include +#include + +#include "compat.h" + +#ifndef NULL +#define NULL 0 +#endif + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#define countof(x) (sizeof(x) / sizeof(x[0])) + +#ifdef _DEBUG + #include "assert.h" + #define ASSERT assert +#else + #define ASSERT +#endif + +#endif /* __USUALS_H__ */ diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..f0dea56 --- /dev/null +++ b/src/util.c @@ -0,0 +1,271 @@ + +#include + +#include +#include +#include +#include +#include + +#include "usuals.h" +#include "compat.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ---------------------------------------------------------------------------------- + * Logging + */ + +const char kMsgDelimiter[] = ": "; +#define MAX_MSGLEN 256 + +static void vmessage(clamsmtp_context_t* ctx, int level, int err, + const char* msg, va_list ap) +{ + size_t len; + char* m; + int e = errno; + + if(g_daemonized) + { + if(level >= LOG_DEBUG) + return; + } + else + { + if(g_debuglevel < level) + return; + } + + ASSERT(msg); + + len = strlen(msg) + 20 + MAX_MSGLEN; + m = (char*)alloca(len); + + if(m) + { + if(ctx) + snprintf(m, len, "%06X: %s%s", ctx->id, msg, err ? ": " : ""); + else + snprintf(m, len, "%s%s", msg, err ? ": " : ""); + + if(err) + strerror_r(e, m + strlen(m), MAX_MSGLEN); + + m[len - 1] = 0; + msg = m; + } + + /* Either to syslog or stderr */ + if(g_daemonized) + vsyslog(level, msg, ap); + else + vwarnx(msg, ap); +} + +void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vmessage(ctx, level, 0, msg, ap); + va_end(ap); +} + +void message(clamsmtp_context_t* ctx, int level, const char* msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vmessage(ctx, level, 1, msg, ap); + va_end(ap); +} + +#define MAX_LOG_LINE 79 + +void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read) +{ + #define offsetof(s, m) ((size_t)&(((s*)0)->m)) + #define ismember(o, m) (((char*)(m)) < (((char*)(o)) + sizeof(*(o)))) + #define ptrdiff(o, t) + + char prefix[16]; + const char* t; + + ASSERT(ctx); + ASSERT(ismember(ctx, fd)); + + switch((char*)fd - (char*)ctx) + { + case offsetof(clamsmtp_context_t, client): + strcpy(prefix, "CLIENT "); + break; + case offsetof(clamsmtp_context_t, server): + strcpy(prefix, "SERVER "); + break; + case offsetof(clamsmtp_context_t, clam): + strcpy(prefix, "CLAM "); + break; + default: + strcpy(prefix, "???? "); + break; + } + + strcat(prefix, read ? "< " : "> "); + log_data(ctx, data, prefix); +} + + +void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix) +{ + char buf[MAX_LOG_LINE + 1]; + int pos, len; + + for(;;) + { + data += strspn(data, "\r\n"); + + if(!*data) + break; + + pos = strcspn(data, "\r\n"); + + len = pos < MAX_LOG_LINE ? pos : MAX_LOG_LINE; + memcpy(buf, data, len); + buf[len] = 0; + + messagex(ctx, LOG_DEBUG, "%s%s", prefix, buf); + + data += pos; + } +} + +/* ---------------------------------------------------------------------------------- + * Parsing + */ + +int is_first_word(const char* line, const char* word, int len) +{ + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + while(*line && isspace(*line)) + line++; + + if(strncasecmp(line, word, len) != 0) + return 0; + + line += len; + return !*line || isspace(*line); +} + +int check_first_word(const char* line, const char* word, int len, char* delims) +{ + const char* t; + int found = 0; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line; + + while(*t && strchr(delims, *t)) + t++; + + if(strncasecmp(t, word, len) != 0) + return 0; + + t += len; + + while(*t && strchr(delims, *t)) + { + found = 1; + t++; + } + + return (!*t || found) ? t - line : 0; +} + +int is_last_word(const char* line, const char* word, int len) +{ + const char* t; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line + strlen(line); + + while(t > line && isspace(*(t - 1))) + --t; + + if(t - len < line) + return 0; + + return strncasecmp(t - len, word, len) == 0; +} + +int is_blank_line(const char* line) +{ + /* Small optimization */ + if(!*line) + return 1; + + while(*line && isspace(*line)) + line++; + + return *line == 0; +} + +/* ----------------------------------------------------------------------- + * Locking + */ + +void plock() +{ + int r; + +#ifdef _DEBUG + int wait = 0; +#endif + +#ifdef _DEBUG + r = pthread_mutex_trylock(&g_mutex); + if(r == EBUSY) + { + wait = 1; + message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self()); + r = pthread_mutex_lock(&g_mutex); + } + +#else + r = pthread_mutex_lock(&g_mutex); + +#endif + + if(r != 0) + { + errno = r; + message(NULL, LOG_CRIT, "threading problem. couldn't lock mutex"); + } + +#ifdef _DEBUG + else if(wait) + { + message(NULL, LOG_DEBUG, "thread unblocked: %d", pthread_self()); + } +#endif +} + +void punlock() +{ + int r = pthread_mutex_unlock(&g_mutex); + if(r != 0) + { + errno = r; + message(NULL, LOG_CRIT, "threading problem. couldn't unlock mutex"); + } +} + diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..54b8ea6 --- /dev/null +++ b/src/util.h @@ -0,0 +1,19 @@ + +#ifndef __UTIL_H__ +#define __UTIL_H__ + +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); +int is_blank_line(const char* line); + +void plock(); +void punlock(); + +#endif /* __UTIL_H__ */ -- cgit v1.2.3