diff options
author | Stef Walter <stef@memberwebs.com> | 2004-07-08 18:27:54 +0000 |
---|---|---|
committer | Stef Walter <stef@memberwebs.com> | 2004-07-08 18:27:54 +0000 |
commit | ac7e532095160a85ca03476aa707ef80a8a8ce5b (patch) | |
tree | 4e331300e5f11192869e66b9f08e58c4cf13cce3 /src | |
parent | 17e2bbdacafb2c969d26a89ede3b57b60dc19bc1 (diff) |
Initial import
Diffstat (limited to 'src')
-rw-r--r-- | src/.cvsignore | 5 | ||||
-rw-r--r-- | src/Makefile.am | 8 | ||||
-rw-r--r-- | src/clamsmtpd.8 | 132 | ||||
-rw-r--r-- | src/clamsmtpd.c | 1215 | ||||
-rw-r--r-- | src/clamsmtpd.h | 24 | ||||
-rw-r--r-- | src/compat.c | 77 | ||||
-rw-r--r-- | src/compat.h | 51 | ||||
-rw-r--r-- | src/sock_any.c | 275 | ||||
-rw-r--r-- | src/sock_any.h | 33 | ||||
-rw-r--r-- | src/usuals.h | 38 | ||||
-rw-r--r-- | src/util.c | 271 | ||||
-rw-r--r-- | src/util.h | 19 |
12 files changed, 2148 insertions, 0 deletions
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 <sys/time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <sys/stat.h> + +#include <paths.h> +#include <stdio.h> +#include <pthread.h> +#include <unistd.h> +#include <fcntl.h> +#include <syslog.h> +#include <signal.h> +#include <errno.h> + +#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 <CRLF>.<CRLF>\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 <sys/types.h> + +#ifndef HAVE_STDARG_H +#error ERROR: Must have a working stdarg.h header +#else +#include <stdarg.h> +#endif + +#ifndef HAVE_REALLOCF +void* reallocf(void* p, size_t sz); +#endif + +#include <pthread.h> + +/* 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 <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <string.h> + +#include "sock_any.h" + +#include <arpa/inet.h> + +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 <sys/socket.h> +#include <sys/un.h> +#include <netinet/in.h> + +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 <sys/types.h> + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +#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 <sys/types.h> + +#include <syslog.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> + +#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__ */ |