* "License".
*/
-#include "../config.h"
+#include "config.h"
#include "libspamc.h"
#include "utils.h"
#include <unistd.h>
#include <stdlib.h>
+#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
+#include <sys/un.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
-#ifdef SPAMC_SSL
-#include <openssl/crypto.h>
-#include <openssl/pem.h>
-#include <openssl/ssl.h>
-#include <openssl/err.h>
-#endif
-
#ifdef HAVE_SYSEXITS_H
#include <sysexits.h>
#endif
/* RedHat 5.2 doesn't define Shutdown 2nd Parameter Constants */
/* KAM 12-4-01 */
-#ifndef HAVE_SHUT_RD
-#define SHUT_RD (0) /* No more receptions. */
-#define SHUT_WR (1) /* No more transmissions. */
-#define SHUT_RDWR (2) /* No more receptions or transmissions. */
+/* SJF 2003/04/25 - now test for macros directly */
+#ifndef SHUT_RD
+# define SHUT_RD 0 /* no more receptions */
+#endif
+#ifndef SHUT_WR
+# define SHUT_WR 1 /* no more transmissions */
+#endif
+#ifndef SHUT_RDWR
+# define SHUT_RDWR 2 /* no more receptions or transmissions */
#endif
#ifndef HAVE_H_ERRNO
#define EX__MAX 200
#endif
-static const int DO_CONNECT_DEBUG_SYSLOGS = 0;
+#undef DO_CONNECT_DEBUG_SYSLOGS
+/* or #define DO_CONNECT_DEBUG_SYSLOGS 1 */
static const int ESC_PASSTHROUGHRAW = EX__MAX+666;
*/
/* Set the protocol version that this spamc speaks */
-static const char *PROTOCOL_VERSION="SPAMC/1.2";
+static const char *PROTOCOL_VERSION="SPAMC/1.3";
+
+/* "private" part of struct message.
+ * we use this instead of the struct message directly, so that we
+ * can add new members without affecting the ABI.
+ */
+struct libspamc_private_message {
+ int flags; /* copied from "flags" arg to message_read() */
+};
int libspamc_timeout = 0;
+/*
+ * translate_connect_errno()
+ *
+ * Given a UNIX error number obtained (probably) from "connect(2)",
+ * translate this to a failure code. This module is shared by both
+ * transport modules - UNIX and TCP.
+ *
+ * This should ONLY be called when there is an error.
+ */
+static int
+translate_connect_errno(int err)
+{
+ switch (err)
+ {
+ case EBADF:
+ case EFAULT:
+ case ENOTSOCK:
+ case EISCONN:
+ case EADDRINUSE:
+ case EINPROGRESS:
+ case EALREADY:
+ case EAFNOSUPPORT:
+ return EX_SOFTWARE;
+
+ case ECONNREFUSED:
+ case ETIMEDOUT:
+ case ENETUNREACH:
+ return EX_UNAVAILABLE;
+
+ case EACCES:
+ return EX_NOPERM;
+
+ default:
+ return EX_SOFTWARE;
+ }
+}
+
+/*
+ * opensocket()
+ *
+ * Given a socket type (PF_INET or PF_UNIX), try to create this socket
+ * and store the FD in the pointed-to place. If it's successful, do any
+ * other setup required to make the socket ready to use, such as setting
+ * TCP_NODELAY mode, and in any case we return EX_OK if all is well.
+ *
+ * Upon failure we return one of the other EX_??? error codes.
+ */
+static int opensocket(int type, int *psock)
+{
+const char *typename;
+int proto = 0;
+
+ assert(psock != 0);
+
+ /*----------------------------------------------------------------
+ * Create a few induction variables that are implied by the socket
+ * type given by the user. The typename is strictly used for debug
+ * reporting.
+ */
+ if ( type == PF_UNIX )
+ {
+ typename = "PF_UNIX";
+ }
+ else
+ {
+ typename = "PF_INET";
+ proto = IPPROTO_TCP;
+ }
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog (DEBUG_LEVEL, "dbg: create socket(%s)", typename);
+#endif
+
+ if ( (*psock = socket(type, SOCK_STREAM, proto)) < 0 )
+ {
+ int origerr;
+
+ /*--------------------------------------------------------
+ * At this point we had a failure creating the socket, and
+ * this is pretty much fatal. Translate the error reason
+ * into something the user can understand.
+ */
+ origerr = errno; /* take a copy before syslog() */
+
+ syslog (LOG_ERR, "socket(%s) to spamd failed: %m", typename);
+
+ switch (origerr)
+ {
+ case EPROTONOSUPPORT:
+ case EINVAL:
+ return EX_SOFTWARE;
+
+ case EACCES:
+ return EX_NOPERM;
+
+ case ENFILE:
+ case EMFILE:
+ case ENOBUFS:
+ case ENOMEM:
+ return EX_OSERR;
+
+ default:
+ return EX_SOFTWARE;
+ }
+ }
+
+
+ /*----------------------------------------------------------------
+ * Do a bit of setup on the TCP socket if required. Notes above
+ * suggest this is probably not set
+ */
+#ifdef USE_TCP_NODELAY
+ {
+ int one = 1;
+
+ if ( type == PF_INET
+ && setsockopt(*psock, 0, TCP_NODELAY, &one, sizeof one) != 0 )
+ {
+ switch(errno)
+ {
+ case EBADF:
+ case ENOTSOCK:
+ case ENOPROTOOPT:
+ case EFAULT:
+ syslog(LOG_ERR,
+ "setsockopt(TCP_NODELAY) failed: %m");
+ close (*psock);
+ return EX_SOFTWARE;
+
+ default:
+ break; /* ignored */
+ }
+ }
+ }
+#endif /* USE_TCP_NODELAY */
+
+ return EX_OK; /* all is well */
+}
+
+/*
+ * try_to_connect_unix()
+ *
+ * Given a transport handle that implies using a UNIX domain
+ * socket, try to make a connection to it and store the resulting
+ * file descriptor in *sockptr. Return is EX_OK if we did it,
+ * and some other error code otherwise.
+ */
+static int
+try_to_connect_unix (struct transport *tp, int *sockptr)
+{
+int mysock, status, origerr;
+struct sockaddr_un addrbuf;
+int ret;
+
+ assert(tp != 0);
+ assert(sockptr != 0);
+ assert(tp->socketpath != 0);
+
+ /*----------------------------------------------------------------
+ * If the socket itself can't be created, this is a fatal error.
+ */
+ if ( (ret = opensocket(PF_UNIX, &mysock)) != EX_OK )
+ return ret;
+
+ /* set up the UNIX domain socket */
+ memset(&addrbuf, 0, sizeof addrbuf);
+ addrbuf.sun_family = AF_UNIX;
+ strncpy(addrbuf.sun_path, tp->socketpath, sizeof addrbuf.sun_path - 1);
+ addrbuf.sun_path[sizeof addrbuf.sun_path - 1] = '\0';
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog (DEBUG_LEVEL, "dbg: connect(AF_UNIX) to spamd at %s",
+ addrbuf.sun_path);
+#endif
+
+ status = connect(mysock, (struct sockaddr *) &addrbuf, sizeof(addrbuf));
+
+ origerr = errno;
+
+ if ( status >= 0 )
+ {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog(DEBUG_LEVEL, "dbg: connect(AF_UNIX) ok");
+#endif
+
+ *sockptr = mysock;
+
+ return EX_OK;
+ }
+
+ syslog(LOG_ERR, "connect(AF_UNIX) to spamd %s failed: %m",
+ addrbuf.sun_path);
+
+ close(mysock);
+
+ return translate_connect_errno(origerr);
+}
+
+/*
+ * try_to_connect_tcp()
+ *
+ * Given a transport that implies a TCP connection, either to
+ * localhost or a list of IP addresses, attempt to connect. The
+ * list of IP addresses has already been randomized (if requested)
+ * and limited to just one if fallback has been enabled.
+ */
+static int
+try_to_connect_tcp (const struct transport *tp, int *sockptr)
+{
+int numloops;
+int origerr = 0;
+int ret;
+
+ assert(tp != 0);
+ assert(sockptr != 0);
+ assert(tp->nhosts > 0);
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ for (numloops = 0; numloops < tp->nhosts; numloops++)
+ {
+ syslog(LOG_ERR, "dbg: %d/%d: %s",
+ numloops+1, tp->nhosts, inet_ntoa(tp->hosts[numloops]));
+ }
+#endif
+
+ for (numloops = 0; numloops < MAX_CONNECT_RETRIES; numloops++)
+ {
+ struct sockaddr_in addrbuf;
+ const int hostix = numloops % tp->nhosts;
+ int status, mysock;
+ const char * ipaddr;
+
+ /*--------------------------------------------------------
+ * We always start by creating the socket, as we get only
+ * one attempt to connect() on each one. If this fails,
+ * we're done.
+ */
+ if ( (ret = opensocket(PF_INET, &mysock)) != EX_OK )
+ return ret;
+
+ memset(&addrbuf, 0, sizeof(addrbuf));
+
+ addrbuf.sin_family = AF_INET;
+ addrbuf.sin_port = htons(tp->port);
+ addrbuf.sin_addr = tp->hosts[hostix];
+
+ ipaddr = inet_ntoa(addrbuf.sin_addr);
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog (DEBUG_LEVEL,
+ "dbg: connect(AF_INET) to spamd at %s (try #%d of %d)",
+ ipaddr,
+ numloops+1,
+ MAX_CONNECT_RETRIES);
+#endif
+
+ status = connect(mysock, (struct sockaddr *)&addrbuf, sizeof(addrbuf));
+
+ if (status != 0)
+ {
+ syslog (LOG_ERR,
+ "connect(AF_INET) to spamd at %s failed, retrying (#%d of %d): %m",
+ ipaddr, numloops+1, MAX_CONNECT_RETRIES);
+
+ close(mysock);
+
+ sleep(CONNECT_RETRY_SLEEP);
+ }
+ else
+ {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog(DEBUG_LEVEL,
+ "dbg: connect(AF_INET) to spamd at %s done",
+ ipaddr);
+#endif
+ *sockptr = mysock;
+
+ return EX_OK;
+ }
+ }
+
+ syslog (LOG_ERR, "connection attempt to spamd aborted after %d retries",
+ MAX_CONNECT_RETRIES);
+
+ return translate_connect_errno(origerr);
+}
+
+#if 0
static int
try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
int hent_port, int *sockptr)
struct sockaddr_in addrbuf, *addr;
struct in_addr inaddrlist[256];
- int i; char dbgbuf[2048]; int dbgbuflen = 0; // DBG
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ int dbgiter; char dbgbuf[2048]; int dbgbuflen = 0;
+#endif
/* NOTE: do not call syslog() (unless you are about to return) before
* we take a copy of the h_addr_list.
memset (inaddrlist, 0, sizeof(inaddrlist));
for (hostnum=0; hent->h_addr_list[hostnum] != 0; hostnum++) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
"[%d %lx: %d.%d.%d.%d]",
hostnum, hent->h_addr_list[hostnum],
hent->h_addr_list[hostnum][1],
hent->h_addr_list[hostnum][2],
hent->h_addr_list[hostnum][3]);
+#endif
if (hostnum > 255) {
syslog (LOG_ERR, "too many address in hostent (%d), ignoring others",
return EX_SOFTWARE;
}
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
"[%d: %d.%d.%d.%d] ", sizeof (struct in_addr),
hent->h_addr_list[hostnum][0],
hent->h_addr_list[hostnum][1],
hent->h_addr_list[hostnum][2],
hent->h_addr_list[hostnum][3]);
+#endif
memcpy ((void *) &(inaddrlist[hostnum]),
(void *) hent->h_addr_list[hostnum],
sizeof (struct in_addr));
}
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: %d %s", hostnum, dbgbuf); dbgbuflen = 0;
- }
+#endif
}
- if (DO_CONNECT_DEBUG_SYSLOGS) {
- for (i = 0; i < hostnum; i++) {
- syslog (LOG_DEBUG, "dbg: host addr %d/%d = %lx at %lx",
- i, hostnum, inaddrlist[i].s_addr, &(inaddrlist[i]));
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ for (dbgiter = 0; dbgiter < hostnum; dbgiter++) {
+ syslog (LOG_DEBUG, "dbg: host addr %d/%d = %lx at %lx", dbgiter, hostnum,
+ inaddrlist[dbgiter].s_addr, &(inaddrlist[dbgiter]));
}
- }
+#endif
hent = NULL; /* cannot use hent after this point, syslog() may overwrite it */
- if (DO_CONNECT_DEBUG_SYSLOGS) { syslog (LOG_DEBUG, "dbg: socket"); }
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+ syslog (LOG_DEBUG, "dbg: socket");
+#endif
if(-1 == (mysock = socket(PF_INET,SOCK_STREAM,0)))
{
return EX_SOFTWARE;
}
}
-
- if (DO_CONNECT_DEBUG_SYSLOGS) { syslog (LOG_DEBUG, "dbg: setsockopt"); }
#ifdef USE_TCP_NODELAY
/* TODO: should this be up above the connect()? */
#endif
for (numloops=0; numloops < MAX_CONNECT_RETRIES; numloops++) {
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: connect() to spamd %d", numloops);
- }
+#endif
+
if (argaddr != NULL) {
addr = (struct sockaddr_in *) argaddr; /* use the one provided */
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: using argaddr");
- }
+#endif
} else {
/* cycle through the addrs in hent */
return EX_SOFTWARE;
}
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: cpy addr %d/%d at %lx",
numloops%hostnum, hostnum, &(inaddrlist[numloops % hostnum]));
- }
+#endif
memcpy (&addrbuf.sin_addr, &(inaddrlist[numloops % hostnum]),
sizeof(addrbuf.sin_addr));
addr = &addrbuf;
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: conn addr %d/%d = %lx",
numloops%hostnum, hostnum, addrbuf.sin_addr.s_addr);
- }
+#endif
}
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: connect() to spamd at %s",
inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
+#endif
status = connect(mysock,(const struct sockaddr *) addr, sizeof(*addr));
- if (DO_CONNECT_DEBUG_SYSLOGS) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
syslog (LOG_DEBUG, "dbg: connect() to spamd at %s done",
inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
- }
+#endif
if (status < 0)
{
return EX_SOFTWARE;
}
}
+#endif
/* Aug 14, 2002 bj: Reworked things. Now we have message_read, message_write,
* message_dump, lookup_host, message_filter, and message_process, and a bunch
m->is_spam=EX_TOOBIG;
m->score=0.0; m->threshold=0.0;
m->out=NULL; m->out_len=0;
+ m->content_length=-1;
}
-static int message_read_raw(int fd, struct message *m){
+static int
+message_read_raw(int fd, struct message *m){
clear_message(m);
if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
- m->raw_len=full_read(fd, (unsigned char *) m->raw, m->max_len+1, m->max_len+1);
+ m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
if(m->raw_len<=0){
free(m->raw); m->raw=NULL; m->raw_len=0;
return EX_IOERR;
if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
/* Find the DATA line */
- m->raw_len=full_read(fd, (unsigned char *) m->raw, m->max_len+1, m->max_len+1);
+ m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
if(m->raw_len<=0){
free(m->raw); m->raw=NULL; m->raw_len=0;
return EX_IOERR;
int message_read(int fd, int flags, struct message *m){
libspamc_timeout = 0;
+ /* create the "private" part of the struct message */
+ m->priv = malloc (sizeof (struct libspamc_private_message));
+ if (m->priv == NULL) {
+ syslog(LOG_ERR, "message_read: malloc failed");
+ return EX_OSERR;
+ }
+ m->priv->flags = flags;
+
switch(flags&SPAMC_MODE_MASK){
case SPAMC_RAW_MODE:
return message_read_raw(fd, m);
off_t jlimit;
char buffer[1024];
- if(m->is_spam==EX_ISSPAM || m->is_spam==EX_NOTSPAM){
- return full_write(fd, (unsigned char *) m->out, m->out_len);
+ if (m->priv->flags&SPAMC_CHECK_ONLY) {
+ if(m->is_spam==EX_ISSPAM || m->is_spam==EX_NOTSPAM){
+ return full_write(fd, m->out, m->out_len);
+
+ } else {
+ syslog(LOG_ERR, "oops! SPAMC_CHECK_ONLY is_spam: %d\n", m->is_spam);
+ return -1;
+ }
}
+ /* else we're not in CHECK_ONLY mode */
switch(m->type){
case MESSAGE_NONE:
syslog(LOG_ERR, "Cannot write this message, it's MESSAGE_NONE!\n");
return -1;
case MESSAGE_ERROR:
- return full_write(fd, (unsigned char *) m->raw, m->raw_len);
+ return full_write(fd, m->raw, m->raw_len);
case MESSAGE_RAW:
- return full_write(fd, (unsigned char *) m->out, m->out_len);
+ return full_write(fd, m->out, m->out_len);
case MESSAGE_BSMTP:
- total=full_write(fd, (unsigned char *) m->pre, m->pre_len);
+ total=full_write(fd, m->pre, m->pre_len);
for(i=0; i<m->out_len; ){
jlimit = (off_t) (sizeof(buffer)/sizeof(*buffer)-4);
for(j=0; i < (off_t) m->out_len &&
buffer[j++]=m->out[i++];
}
}
- total+=full_write(fd, (unsigned char *) buffer, j);
+ total+=full_write(fd, buffer, j);
}
- return total+full_write(fd, (unsigned char *) m->post, m->post_len);
+ return total+full_write(fd, m->post, m->post_len);
default:
syslog(LOG_ERR, "Unknown message type %d\n", m->type);
if(m!=NULL && m->type!=MESSAGE_NONE) {
message_write(out_fd, m);
}
- while((bytes=full_read(in_fd, (unsigned char *) buf, 8192, 8192))>0){
- if (bytes!=full_write(out_fd, (unsigned char *) buf, bytes)) {
+ while((bytes=full_read(in_fd, buf, 8192, 8192))>0){
+ if (bytes!=full_write(out_fd, buf, bytes)) {
syslog(LOG_ERR, "oops! message_dump of %d returned different", bytes);
}
}
}
-static int _message_filter(const struct sockaddr *addr,
- const struct hostent *hent, int hent_port, char *username,
+static int
+_spamc_read_full_line (struct message *m, int flags, SSL *ssl, int sock,
+ char *buf, int *lenp, int bufsiz)
+{
+ int failureval;
+ int bytesread = 0;
+ int len;
+
+ UNUSED_VARIABLE(m);
+
+ /* Now, read from spamd */
+ for(len=0; len<bufsiz-1; len++) {
+ if(flags&SPAMC_USE_SSL) {
+ bytesread = ssl_timeout_read (ssl, buf+len, 1);
+ } else {
+ bytesread = fd_timeout_read (sock, buf+len, 1);
+ }
+
+ if(buf[len]=='\n') {
+ buf[len]='\0';
+ if (len > 0 && buf[len-1] == '\r') {
+ len--;
+ buf[len]='\0';
+ }
+ *lenp = len;
+ return EX_OK;
+ }
+
+ if(bytesread<=0){
+ failureval = EX_IOERR; goto failure;
+ }
+ }
+
+ syslog(LOG_ERR, "spamd responded with line of %d bytes, dying", len);
+ failureval = EX_TOOBIG;
+
+failure:
+ return failureval;
+}
+
+/*
+ * May 7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
+ * work around using our own locale-independent float-parser code.
+ */
+static float
+_locale_safe_string_to_float (char *buf, int siz)
+{
+ int is_neg;
+ char *cp, *dot;
+ int divider;
+ float ret, postdot;
+
+ buf[siz-1] = '\0'; /* ensure termination */
+
+ /* ok, let's illustrate using "100.033" as an example... */
+
+ is_neg = 0;
+ if (*buf == '-') { is_neg = 1; }
+
+ ret = (float) (strtol (buf, &dot, 10));
+ if (dot == NULL) { return 0.0; }
+ if (dot != NULL && *dot != '.') { return ret; }
+
+ /* ex: ret == 100.0 */
+
+ cp = (dot + 1);
+ postdot = (float) (strtol (cp, NULL, 10));
+ if (postdot == 0.0) { return ret; }
+
+ /* ex: postdot == 33.0, cp="033" */
+
+ /* now count the number of decimal places and figure out what power of 10 to use */
+ divider = 1;
+ while (*cp != '\0') {
+ divider *= 10; cp++;
+ }
+
+ /* ex:
+ * cp="033", divider=1
+ * cp="33", divider=10
+ * cp="3", divider=100
+ * cp="", divider=1000
+ */
+
+ if (is_neg) {
+ ret -= (postdot / ((float) divider));
+ } else {
+ ret += (postdot / ((float) divider));
+ }
+ /* ex: ret == 100.033, tada! ... hopefully */
+
+ return ret;
+}
+
+static int
+_handle_spamd_header (struct message *m, int flags, char *buf, int len)
+{
+ char is_spam[6];
+ char s_str[21], t_str[21];
+
+ UNUSED_VARIABLE(len);
+
+ /* Feb 12 2003 jm: actually, I think sccanf is working fine here ;)
+ * let's stick with it for this parser.
+ * May 7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
+ * work around using our own locale-independent float-parser code.
+ */
+ if (sscanf(buf, "Spam: %5s ; %20s / %20s", is_spam, s_str, t_str) == 3)
+ {
+ m->score = _locale_safe_string_to_float (s_str, 20);
+ m->threshold = _locale_safe_string_to_float (t_str, 20);
+
+ /* Format is "Spam: x; y / x" */
+ m->is_spam=strcasecmp("true", is_spam) == 0 ? EX_ISSPAM: EX_NOTSPAM;
+
+ if(flags&SPAMC_CHECK_ONLY) {
+ m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
+ "%.1f/%.1f\n", m->score, m->threshold);
+ }
+ else if ((flags & SPAMC_REPORT_IFSPAM && m->is_spam == EX_ISSPAM)
+ || (flags & SPAMC_REPORT))
+ {
+ m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
+ "%.1f/%.1f\n", m->score, m->threshold);
+ }
+ return EX_OK;
+
+ } else if(sscanf(buf, "Content-length: %d", &m->content_length) == 1) {
+ if (m->content_length < 0) {
+ syslog(LOG_ERR, "spamd responded with bad Content-length '%s'", buf);
+ return EX_PROTOCOL;
+ }
+ return EX_OK;
+ }
+
+ syslog(LOG_ERR, "spamd responded with bad header '%s'", buf);
+ return EX_PROTOCOL;
+}
+
+int message_filter(struct transport *tp, const char *username,
int flags, struct message *m)
{
- char buf[8192], is_spam[6];
+ char buf[8192];
int bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
- int len, expected_len, i, header_read=0;
- int sock;
+ int len, i;
+ int sock = -1;
+ int rc;
+ char versbuf[20];
float version;
int response;
int failureval;
-#ifdef SPAMC_SSL
SSL_CTX* ctx;
SSL* ssl;
SSL_METHOD *meth;
- if(flags&SPAMC_USE_SSL){
+ if (flags&SPAMC_USE_SSL) {
+#ifdef SPAMC_SSL
SSLeay_add_ssl_algorithms();
meth = SSLv2_client_method();
SSL_load_error_strings();
ctx = SSL_CTX_new(meth);
- }
+#else
+ UNUSED_VARIABLE(ssl);
+ UNUSED_VARIABLE(meth);
+ UNUSED_VARIABLE(ctx);
+ syslog(LOG_ERR, "spamc not built with SSL support");
+ return EX_SOFTWARE;
#endif
+ }
m->is_spam=EX_TOOBIG;
if((m->out=malloc(m->max_len+EXPANSION_ALLOWANCE+1))==NULL){
- return EX_OSERR;
+ failureval = EX_OSERR; goto failure;
}
m->out_len=0;
+
/* Build spamd protocol header */
- len=snprintf(buf, bufsiz, "%s %s\r\n", (flags&SPAMC_CHECK_ONLY)?"CHECK":"PROCESS", PROTOCOL_VERSION);
+ if(flags & SPAMC_CHECK_ONLY)
+ len=snprintf(buf, bufsiz, "CHECK %s\r\n", PROTOCOL_VERSION);
+ else if(flags & SPAMC_REPORT_IFSPAM)
+ len=snprintf(buf, bufsiz, "REPORT_IFSPAM %s\r\n", PROTOCOL_VERSION);
+ else if(flags & SPAMC_REPORT)
+ len=snprintf(buf, bufsiz, "REPORT %s\r\n", PROTOCOL_VERSION);
+ else if(flags & SPAMC_SYMBOLS)
+ len=snprintf(buf, bufsiz, "SYMBOLS %s\r\n", PROTOCOL_VERSION);
+ else
+ len=snprintf(buf, bufsiz, "PROCESS %s\r\n", PROTOCOL_VERSION);
+
if(len<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
if(username!=NULL){
len+=i=snprintf(buf+len, bufsiz-len, "User: %s\r\n", username);
libspamc_timeout = m->timeout;
- if((i=try_to_connect(addr, (struct hostent *) hent,
- hent_port, &sock)) != EX_OK)
+ if ( tp->socketpath )
+ rc = try_to_connect_unix(tp, &sock);
+ else
+ rc = try_to_connect_tcp(tp, &sock);
+
+ if ( rc != EX_OK )
{
free(m->out); m->out=m->msg; m->out_len=m->msg_len;
return i;
SSL_write(ssl, m->msg, m->msg_len);
#endif
} else {
- full_write(sock, (unsigned char *) buf, len);
- full_write(sock, (unsigned char *) m->msg, m->msg_len);
+ full_write(sock, buf, len);
+ full_write(sock, m->msg, m->msg_len);
shutdown(sock, SHUT_WR);
}
- /* Now, read from spamd */
- for(len=0; len<bufsiz; len++) {
- if(flags&SPAMC_USE_SSL) {
-#ifdef SPAMC_SSL
- i=timeout_read(SSL_read, ssl, buf+len, 1);
-#endif
- } else {
- i=timeout_read(read, sock, buf+len, 1);
- }
-
- if(i<0){
- failureval = EX_IOERR; goto failure;
- }
- if(i==0){
- /* Read to end of message! Must be a version <1.0 server */
- if(len<100){
- /* Nope, communication error */
- failureval = EX_IOERR; goto failure;
- }
- break;
- }
- if(buf[len]=='\n'){
- buf[len]='\0';
- if(sscanf(buf, "SPAMD/%f %d %*s", &version, &response)!=2){
- syslog(LOG_ERR, "spamd responded with bad string '%s'", buf);
- failureval = EX_PROTOCOL; goto failure;
- }
- header_read=-1;
- break;
- }
- }
- if(!header_read){
- /* No header, so it must be a version <1.0 server */
- memcpy(m->out, buf, len);
- m->out_len=len;
- } else {
- /* Handle different versioned headers */
- if(version-1.0>0.01){
- for(len=0; len<bufsiz; len++){
-#ifdef SPAMC_SSL
- if(flags&SPAMC_USE_SSL){
- i=timeout_read(SSL_read, ssl, buf+len, 1);
- } else{
-#endif
- i=timeout_read(read, sock, buf+len, 1);
-#ifdef SPAMC_SSL
- }
-#endif
- if(i<=0){
- failureval = (i<0)?EX_IOERR:EX_PROTOCOL; goto failure;
- }
- if(buf[len]=='\n'){
- buf[len]='\0';
- if(flags&SPAMC_CHECK_ONLY){
- /* Check only mode, better be "Spam: x; y / x" */
- i=sscanf(buf, "Spam: %5s ; %f / %f", is_spam, &m->score, &m->threshold);
-
- if(i!=3){
- free(m->out); m->out=m->msg; m->out_len=m->msg_len;
- return EX_PROTOCOL;
- }
- m->out_len=snprintf(m->out, m->max_len+EXPANSION_ALLOWANCE, "%.1f/%.1f\n", m->score, m->threshold);
- m->is_spam=strcasecmp("true", is_spam)?EX_NOTSPAM:EX_ISSPAM;
- close(sock);
- return EX_OK;
- } else {
- /* Not check-only, better be Content-length */
- if(sscanf(buf, "Content-length: %d", &expected_len)!=1){
- failureval = EX_PROTOCOL;
- goto failure;
- }
- }
-
- /* Should be end of headers now */
- if(flags&SPAMC_USE_SSL){
-#ifdef SPAMC_SSL
- i=timeout_read(SSL_read,ssl, buf, 2);
-#endif
- } else{
- i=full_read (sock, (unsigned char *) buf, 2, 2);
- }
-
- if(i!=2 || buf[0]!='\r' || buf[1]!='\n'){
- /* Nope, bail. */
- failureval = EX_PROTOCOL; goto failure;
- }
+ /* ok, now read and parse it. SPAMD/1.2 line first... */
+ failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
+ if (failureval != EX_OK) { goto failure; }
- break;
- }
- }
- }
+ if(sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response)!=2) {
+ syslog(LOG_ERR, "spamd responded with bad string '%s'", buf);
+ failureval = EX_PROTOCOL; goto failure;
}
- if(flags&SPAMC_CHECK_ONLY){
- /* We should have gotten headers back... Damnit. */
+ versbuf[19] = '\0';
+ version = _locale_safe_string_to_float (versbuf, 20);
+ if (version < 1.0) {
+ syslog(LOG_ERR, "spamd responded with bad version string '%s'", versbuf);
failureval = EX_PROTOCOL; goto failure;
}
- if(flags&SPAMC_USE_SSL){
-#ifdef SPAMC_SSL
- len=timeout_read(SSL_read,ssl, m->out+m->out_len,
- m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
-#endif
- } else{
- len=full_read(sock, (unsigned char *) m->out+m->out_len,
- m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
- m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+ m->score = 0;
+ m->threshold = 0;
+ m->is_spam = EX_TOOBIG;
+ while (1) {
+ failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
+ if (failureval != EX_OK) { goto failure; }
+
+ if (len == 0 && buf[0] == '\0') {
+ break; /* end of headers */
+ }
+
+ if (_handle_spamd_header(m, flags, buf, len) < 0) {
+ failureval = EX_PROTOCOL; goto failure;
+ }
}
- if(len+m->out_len>m->max_len+EXPANSION_ALLOWANCE){
- failureval = EX_TOOBIG; goto failure;
+ len = 0; /* overwrite those headers */
+
+ if (flags&SPAMC_CHECK_ONLY) {
+ close(sock); sock = -1;
+ if (m->is_spam == EX_TOOBIG) {
+ /* We should have gotten headers back... Damnit. */
+ failureval = EX_PROTOCOL; goto failure;
+ }
+ return EX_OK;
}
- m->out_len+=len;
+ else {
+ if (m->content_length < 0) {
+ /* should have got a length too. */
+ failureval = EX_PROTOCOL; goto failure;
+ }
- shutdown(sock, SHUT_RD);
- close(sock);
+ /* have we already got something in the buffer (e.g. REPORT and
+ * REPORT_IFSPAM both create a line from the "Spam:" hdr)? If
+ * so, add the size of that so our sanity check passes.
+ */
+ if (m->out_len > 0) {
+ m->content_length += m->out_len;
+ }
+
+ if (flags&SPAMC_USE_SSL) {
+ len = full_read_ssl (ssl, (unsigned char *) m->out+m->out_len,
+ m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
+ m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+ } else{
+ len = full_read (sock, m->out+m->out_len,
+ m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
+ m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+ }
+
+
+ if(len+m->out_len>m->max_len+EXPANSION_ALLOWANCE){
+ failureval = EX_TOOBIG; goto failure;
+ }
+ m->out_len+=len;
+
+ shutdown(sock, SHUT_RD);
+ close(sock); sock = -1;
+ }
libspamc_timeout = 0;
- if(m->out_len!=expected_len){
- syslog(LOG_ERR, "failed sanity check, %d bytes claimed, %d bytes seen", expected_len, m->out_len);
+ if(m->out_len!=m->content_length) {
+ syslog(LOG_ERR, "failed sanity check, %d bytes claimed, %d bytes seen",
+ m->content_length, m->out_len);
failureval = EX_PROTOCOL; goto failure;
}
failure:
free(m->out); m->out=m->msg; m->out_len=m->msg_len;
- close(sock);
+ if (sock != -1) {
+ close(sock);
+ }
libspamc_timeout = 0;
+ if(flags&SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
- if(flags&SPAMC_USE_SSL){
SSL_free(ssl);
SSL_CTX_free(ctx);
- }
#endif
+ }
return failureval;
}
-static int _lookup_host(const char *hostname, struct hostent *out_hent)
-{
- struct hostent *hent = NULL;
- int origherr;
-
- /* no need to try using inet_addr(), gethostbyname() will do that */
-
- if (NULL == (hent = gethostbyname(hostname))) {
- origherr = h_errno; /* take a copy before syslog() */
- syslog (LOG_ERR, "gethostbyname(%s) failed: h_errno=%d",
- hostname, origherr);
- switch(origherr)
- {
- case HOST_NOT_FOUND:
- case NO_ADDRESS:
- case NO_RECOVERY:
- return EX_NOHOST;
- case TRY_AGAIN:
- return EX_TEMPFAIL;
- default:
- return EX_OSERR;
- }
- }
-
- memcpy (out_hent, hent, sizeof(struct hostent));
- return EX_OK;
-}
-
-int message_process(const char *hostname, int port, char *username, int max_size, int in_fd, int out_fd, const int flags){
- struct hostent hent;
+int message_process(struct transport *trans, char *username, int max_size, int in_fd, int out_fd, const int flags){
int ret;
struct message m;
m.type=MESSAGE_NONE;
- ret=lookup_host_for_failover(hostname, &hent);
- if(ret!=EX_OK) goto FAIL;
-
m.max_len=max_size;
ret=message_read(in_fd, flags, &m);
if(ret!=EX_OK) goto FAIL;
- ret=message_filter_with_failover(&hent, port, username, flags, &m);
+ ret=message_filter(trans, username, flags, &m);
if(ret!=EX_OK) goto FAIL;
if(message_write(out_fd, &m)<0) goto FAIL;
if(m.is_spam!=EX_TOOBIG) {
FAIL:
if(flags&SPAMC_CHECK_ONLY){
- full_write(out_fd, (unsigned char *) "0/0\n", 4);
+ full_write(out_fd, "0/0\n", 4);
message_cleanup(&m);
return EX_NOTSPAM;
} else {
void message_cleanup(struct message *m) {
if (m->out != NULL && m->out != m->raw) free(m->out);
if (m->raw != NULL) free(m->raw);
+ if (m->priv != NULL) free(m->priv);
clear_message(m);
}
/* Aug 14, 2002 bj: Obsolete! */
-int process_message(const char *hostname, int port, char *username, int max_size, int in_fd, int out_fd, const int my_check_only, const int my_safe_fallback){
+int process_message(struct transport *tp, char *username, int max_size, int in_fd, int out_fd, const int my_check_only, const int my_safe_fallback){
int flags;
flags=SPAMC_RAW_MODE;
if(my_check_only) flags|=SPAMC_CHECK_ONLY;
if(my_safe_fallback) flags|=SPAMC_SAFE_FALLBACK;
- return message_process(hostname, port, username, max_size, in_fd, out_fd, flags);
+ return message_process(tp, username, max_size, in_fd, out_fd, flags);
}
-/* public APIs, which call into the static code and enforce sockaddr-OR-hostent
- * conventions */
+/*
+ * init_transport()
+ *
+ * Given a pointer to a transport structure, set it to "all empty".
+ * The default is a localhost connection.
+ */
+void transport_init(struct transport *tp)
+{
+ assert(tp != 0);
-int lookup_host(const char *hostname, int port, struct sockaddr *out_addr)
+ memset(tp, 0, sizeof *tp);
+
+ tp->type = TRANSPORT_LOCALHOST;
+ tp->port = 783;
+}
+
+/*
+ * randomize_hosts()
+ *
+ * Given the transport object that contains one or more IP addresses
+ * in this "hosts" list, rotate it by a random number of shifts to
+ * randomize them - this is a kind of load balancing. It's possible
+ * that the random number will be 0, which says not to touch. We don't
+ * do anything unless
+ */
+
+static void randomize_hosts(struct transport *tp)
{
- struct sockaddr_in *addr = (struct sockaddr_in *)out_addr;
- struct hostent hent;
- int ret;
-
- memset(&out_addr, 0, sizeof(out_addr));
- addr->sin_family=AF_INET;
- addr->sin_port=htons(port);
- ret = _lookup_host(hostname, &hent);
- memcpy (&(addr->sin_addr), hent.h_addr, sizeof(addr->sin_addr));
- return ret;
+int rnum;
+
+ assert(tp != 0);
+
+ if ( tp->nhosts <= 1 ) return;
+
+ rnum = rand() % tp->nhosts;
+
+ while ( rnum-- > 0 )
+ {
+ struct in_addr tmp = tp->hosts[0];
+ int i;
+
+ for (i = 1; i < tp->nhosts; i++ )
+ tp->hosts[i-1] = tp->hosts[i];
+
+ tp->hosts[i-1] = tmp;
+ }
+}
+
+/*
+ * transport_setup()
+ *
+ * Given a "transport" object that says how we're to connect to the
+ * spam daemon, perform all the initial setup required to make the
+ * connection process a smooth one. The main work is to do the host
+ * name lookup and copy over all the IP addresses to make a local copy
+ * so they're not kept in the resolver's static state.
+ *
+ * Here we also manage quasi-load balancing and failover: if we're
+ * doing load balancing, we randomly "rotate" the list to put it in
+ * a different order, and then if we're not doing failover we limit
+ * the hosts to just one. This way *all* connections are done with
+ * the intention of failover - makes the code a bit more clear.
+ */
+int transport_setup(struct transport *tp, int flags)
+{
+struct hostent *hp = 0;
+char **addrp;
+
+ assert(tp != 0);
+
+ switch ( tp->type )
+ {
+ case TRANSPORT_UNIX:
+ assert(tp->socketpath != 0);
+ return EX_OK;
+
+ case TRANSPORT_LOCALHOST:
+ tp->hosts[0].s_addr = inet_addr("127.0.0.1");
+ tp->nhosts = 1;
+ return EX_OK;
+
+ case TRANSPORT_TCP:
+ if (NULL == (hp = gethostbyname(tp->hostname)))
+ {
+ int origherr = h_errno; /* take a copy before syslog() */
+
+ syslog (LOG_ERR, "gethostbyname(%s) failed: h_errno=%d",
+ tp->hostname, origherr);
+ switch (origherr)
+ {
+ case HOST_NOT_FOUND:
+ case NO_ADDRESS:
+ case NO_RECOVERY:
+ return EX_NOHOST;
+ case TRY_AGAIN:
+ return EX_TEMPFAIL;
+ default:
+ return EX_OSERR;
+ }
+ }
+
+ /*--------------------------------------------------------
+ * If we have no hosts at all, or if they are some other
+ * kind of address family besides IPv4, then we really
+ * just have no hosts at all.
+ */
+ if ( hp->h_addr_list[0] == 0 )
+ {
+ /* no hosts in this list */
+ return EX_NOHOST;
+ }
+
+ if ( hp->h_length != sizeof tp->hosts[0]
+ || hp->h_addrtype != AF_INET )
+ {
+ /* FAIL - bad size/protocol/family? */
+ return EX_NOHOST;
+ }
+
+ /*--------------------------------------------------------
+ * Copy all the IP addresses into our private structure.
+ * This gets them out of the resolver's static area and
+ * means we won't ever walk all over the list with other
+ * calls.
+ */
+ tp->nhosts = 0;
+
+ for (addrp = hp->h_addr_list; *addrp; addrp++)
+ {
+ if (tp->nhosts >= TRANSPORT_MAX_HOSTS-1) {
+ syslog (LOG_ERR, "hit limit of %d hosts, ignoring remainder", TRANSPORT_MAX_HOSTS-1);
+ break;
+ }
+
+ memcpy(&tp->hosts[tp->nhosts], *addrp,
+ sizeof tp->hosts[0]);
+
+ tp->nhosts++;
+ }
+
+ /*--------------------------------------------------------
+ * QUASI-LOAD-BALANCING
+ *
+ * If the user wants to do quasi load balancing, "rotate"
+ * the list by a random amount based on the current time.
+ * This may later be truncated to a single item. This is
+ * meaningful only if we have more than one host.
+ */
+ if ( (flags & SPAMC_RANDOMIZE_HOSTS) && tp->nhosts > 1 )
+ {
+ randomize_hosts(tp);
+ }
+
+ /*--------------------------------------------------------
+ * If the user wants no fallback, simply truncate the host
+ * list to just one - this pretends that this is the extent
+ * of our connection list - then it's not a special case.
+ */
+ if ( !(flags & SPAMC_SAFE_FALLBACK) && tp->nhosts > 1 )
+ {
+ /* truncating list */
+ tp->nhosts = 1;
+ }
+ }
+ return EX_OK;
}
-int lookup_host_for_failover(const char *hostname, struct hostent *hent) {
- return _lookup_host(hostname, hent);
+
+/* --------------------------------------------------------------------------- */
+
+/*
+ * Unit tests. Must be built externally, e.g.:
+ *
+ * gcc -g -DLIBSPAMC_UNIT_TESTS spamd/spamc.c spamd/libspamc.c spamd/utils.c -o libspamctest
+ * ./libspamctest
+ *
+ */
+#ifdef LIBSPAMC_UNIT_TESTS
+
+static void
+_test_locale_safe_string_to_float_val (float input) {
+ char inputstr[99], cmpbuf1[99], cmpbuf2[99];
+ float output;
+
+ snprintf (inputstr, 99, "%f", input);
+ output = _locale_safe_string_to_float (inputstr, 99);
+ if (input == output) { return; }
+
+ /* could be a rounding error. print as string and compare those */
+ snprintf (cmpbuf1, 98, "%f", input);
+ snprintf (cmpbuf2, 98, "%f", output);
+ if (!strcmp (cmpbuf1, cmpbuf2)) { return; }
+
+ printf ("FAIL: input=%f != output=%f\n", input, output);
}
-int message_filter(const struct sockaddr *addr, char *username, int flags,
- struct message *m)
-{ return _message_filter (addr, NULL, 0, username, flags, m); }
+static void
+unit_test_locale_safe_string_to_float (void) {
+ float statictestset[] = { /* will try both +ve and -ve */
+ 0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001,
+ 9.1, 9.91, 9.991, 9.9991, 9.99991, 9.999991,
+ 0.0 /* end of set constant */
+ };
+ float num;
+ int i;
+
+ printf ("starting unit_test_locale_safe_string_to_float\n");
+ /* tests of precision */
+ for (i = 0; statictestset[i] != 0.0; i++) {
+ _test_locale_safe_string_to_float_val (statictestset[i]);
+ _test_locale_safe_string_to_float_val (-statictestset[i]);
+ _test_locale_safe_string_to_float_val (1-statictestset[i]);
+ _test_locale_safe_string_to_float_val (1+statictestset[i]);
+ }
+ /* now exhaustive, in steps of 0.01 */
+ for (num = -1000.0; num < 1000.0; num += 0.01) {
+ _test_locale_safe_string_to_float_val (num);
+ }
+ printf ("finished unit_test_locale_safe_string_to_float\n");
+}
-int message_filter_with_failover (const struct hostent *hent, int port,
- char *username, int flags, struct message *m)
-{ return _message_filter (NULL, hent, port, username, flags, m); }
+void
+do_libspamc_unit_tests (void) {
+ unit_test_locale_safe_string_to_float();
+ exit (0);
+}
+#endif /* LIBSPAMC_UNIT_TESTS */