From cbb33598caf95c15ed5f4dbfbe4d9beb91f19711 Mon Sep 17 00:00:00 2001 From: Christoph Hohmann Date: Sat, 4 Oct 2003 18:16:23 +0000 Subject: [PATCH 1/1] 0.9.6claws4 * src/plugins/spamassassin/libspamc.[ch] * src/plugins/spamassassin/spamassassin.c * src/plugins/spamassassin/utils.[ch] update libspamc to version from SpamAssassin 2.60 --- ChangeLog.claws | 7 + configure.ac | 2 +- src/plugins/spamassassin/libspamc.c | 699 ++++++++++++++++++++---- src/plugins/spamassassin/libspamc.h | 91 ++- src/plugins/spamassassin/spamassassin.c | 13 +- src/plugins/spamassassin/utils.c | 37 +- src/plugins/spamassassin/utils.h | 7 +- 7 files changed, 720 insertions(+), 136 deletions(-) diff --git a/ChangeLog.claws b/ChangeLog.claws index 440a80cc0..f3fb62593 100644 --- a/ChangeLog.claws +++ b/ChangeLog.claws @@ -1,3 +1,10 @@ +2003-10-04 [christoph] 0.9.6claws4 + + * src/plugins/spamassassin/libspamc.[ch] + * src/plugins/spamassassin/spamassassin.c + * src/plugins/spamassassin/utils.[ch] + update libspamc to version from SpamAssassin 2.60 + 2003-10-03 [christoph] 0.9.6claws3 * src/send_message.c diff --git a/configure.ac b/configure.ac index 83f1f53bb..298190b27 100644 --- a/configure.ac +++ b/configure.ac @@ -11,7 +11,7 @@ MINOR_VERSION=9 MICRO_VERSION=6 INTERFACE_AGE=0 BINARY_AGE=0 -EXTRA_VERSION=3 +EXTRA_VERSION=4 if test $EXTRA_VERSION -eq 0; then VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.${MICRO_VERSION}claws else diff --git a/src/plugins/spamassassin/libspamc.c b/src/plugins/spamassassin/libspamc.c index 5d5f98872..84d21da41 100644 --- a/src/plugins/spamassassin/libspamc.c +++ b/src/plugins/spamassassin/libspamc.c @@ -12,12 +12,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -42,10 +44,15 @@ /* 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 @@ -86,8 +93,304 @@ static const int EXPANSION_ALLOWANCE = 16384; /* Set the protocol version that this spamc speaks */ 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) @@ -315,6 +618,7 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent, 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 @@ -337,7 +641,7 @@ 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; @@ -360,7 +664,7 @@ static int message_read_bsmtp(int fd, struct message *m){ 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; @@ -416,6 +720,14 @@ static int message_read_bsmtp(int fd, struct message *m){ 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); @@ -435,30 +747,30 @@ long message_write(int fd, struct message *m){ off_t jlimit; char buffer[1024]; - /* if we're to output a message, m->is_spam will be EX_OUTPUTMESSAGE */ - 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); - if (m->is_spam != EX_OUTPUTMESSAGE && m->is_spam != EX_TOOBIG) { - syslog(LOG_ERR, - "Cannot write this message, is_spam = %d!\n", m->is_spam); - return -1; + } 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; iout_len; ){ jlimit = (off_t) (sizeof(buffer)/sizeof(*buffer)-4); for(j=0; i < (off_t) m->out_len && @@ -475,9 +787,9 @@ long message_write(int fd, struct message *m){ 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); @@ -492,8 +804,8 @@ void message_dump(int in_fd, int out_fd, struct message *m){ 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); } } @@ -507,6 +819,8 @@ _spamc_read_full_line (struct message *m, int flags, SSL *ssl, int sock, int bytesread = 0; int len; + UNUSED_VARIABLE(m); + /* Now, read from spamd */ for(len=0; lenout_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) { @@ -628,14 +950,14 @@ _handle_spamd_header (struct message *m, int flags, char *buf, int len) return EX_PROTOCOL; } -static int _message_filter(const struct sockaddr *addr, - const struct hostent *hent, int hent_port, char *username, +int message_filter(struct transport *tp, const char *username, int flags, struct message *m) { char buf[8192]; int bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */ int len, i; int sock = -1; + int rc; char versbuf[20]; float version; int response; @@ -651,7 +973,9 @@ static int _message_filter(const struct sockaddr *addr, SSL_load_error_strings(); ctx = SSL_CTX_new(meth); #else - (void) ssl; (void) meth; (void) ctx; /* avoid "unused" warnings */ + UNUSED_VARIABLE(ssl); + UNUSED_VARIABLE(meth); + UNUSED_VARIABLE(ctx); syslog(LOG_ERR, "spamc not built with SSL support"); return EX_SOFTWARE; #endif @@ -688,8 +1012,12 @@ static int _message_filter(const struct sockaddr *addr, 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; @@ -710,8 +1038,8 @@ static int _message_filter(const struct sockaddr *addr, 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); } @@ -719,11 +1047,12 @@ static int _message_filter(const struct sockaddr *addr, failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz); if (failureval != EX_OK) { goto failure; } - if(sscanf(buf, "SPAMD/%s %d %*s", versbuf, &response)!=2) { + 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; } + 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); @@ -749,25 +1078,33 @@ static int _message_filter(const struct sockaddr *addr, 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; + 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; } else { - m->is_spam=EX_OUTPUTMESSAGE; if (m->content_length < 0) { /* should have got a length too. */ failureval = EX_PROTOCOL; goto failure; } + /* 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 = ssl_timeout_read (ssl, m->out+m->out_len, + 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, (unsigned char *) m->out+m->out_len, + 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); } @@ -807,49 +1144,17 @@ failure: 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) { @@ -861,7 +1166,7 @@ int message_process(const char *hostname, int port, char *username, int max_size 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 { @@ -874,46 +1179,246 @@ FAIL: 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); + + 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 + */ -int lookup_host(const char *hostname, int port, struct sockaddr *out_addr) +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 */ diff --git a/src/plugins/spamassassin/libspamc.h b/src/plugins/spamassassin/libspamc.h index 444167661..cf00e8aff 100644 --- a/src/plugins/spamassassin/libspamc.h +++ b/src/plugins/spamassassin/libspamc.h @@ -11,13 +11,13 @@ #include #include +#include #include #include #define EX_NOTSPAM 0 #define EX_ISSPAM 1 #define EX_TOOBIG 866 -#define EX_OUTPUTMESSAGE 867 /* Aug 14, 2002 bj: Bitflags instead of lots of bool parameters */ #define SPAMC_MODE_MASK 1 @@ -35,6 +35,9 @@ /* Feb 1 2003 jm: might as well fix bug 191 as well */ #define SPAMC_SYMBOLS (1<<24) +/* 2003/04/16 SJF: randomize hostname order (quasi load balancing) */ +#define SPAMC_RANDOMIZE_HOSTS (1<<23) + /* Aug 14, 2002 bj: A struct for storing a message-in-progress */ typedef enum { @@ -45,6 +48,8 @@ typedef enum { MAX_MESSAGE_TYPE } message_type_t; +struct libspamc_private_message; + struct message { /* Set before passing the struct on! */ int max_len; /* messages larger than this will return EX_TOOBIG */ @@ -60,14 +65,60 @@ struct message { /* Filled in by filter_message */ int is_spam; /* EX_ISSPAM if the message is spam, EX_NOTSPAM - if not, EX_OUTPUTMESSAGE if a filtered message - is returned in "out" below. */ + if not */ float score, threshold; /* score and threshold */ char *out; int out_len; /* Output from spamd. Either the filtered message, or the check-only response. Or else, a pointer to msg above. */ + + /* these members added in SpamAssassin version 2.60: */ + struct libspamc_private_message *priv; +}; + +/*------------------------------------------------------------------------ + * TRANSPORT (2004/04/16 - SJF) + * + * The code to connect with the daemon has gotten more complicated: support + * for SSL, fallback to multiple hosts, and using UNIX domain sockets. The + * code has gotten ugly with way too many parameters being passed all around. + * + * So we've created this object to hold all the info required to connect with + * the remote site, including a self-contained list of all the IP addresses + * in the event this is using TCP sockets. These multiple IPs can be obtained + * only from DNS returning more than one A record for a single name, and + * this allows for fallback. + * + * We also allow a kind of quasi-load balancing, where we take the list of + * A records from DNS and randomize them before starting out - this lets + * us spread the load out among multiple servers if desired. The idea for + * load balancing goes to Jeremy Zawodny. + * + * By putting all our data here, we remove "fallback" from being a special + * case. We may find ourselves with several IP addresses, but if the user + * disables fallback, we set the IP address count to one. Now the connect + * code just loops over that same address. + */ +#define TRANSPORT_LOCALHOST 0x01 /* TCP to localhost only */ +#define TRANSPORT_TCP 0x02 /* standard TCP socket */ +#define TRANSPORT_UNIX 0x03 /* UNIX domain socket */ + +#define TRANSPORT_MAX_HOSTS 256 /* max hosts we can failover between */ + +struct transport { + int type; + + const char *socketpath; /* for UNIX dommain socket */ + const char *hostname; /* for TCP sockets */ + + unsigned short port; /* for TCP sockets */ + + struct in_addr hosts[TRANSPORT_MAX_HOSTS]; + int nhosts; }; +extern void transport_init(struct transport *tp); +extern int transport_setup(struct transport *tp, int flags); + /* Aug 14, 2002 bj: New interface functions */ /* Read in a message from the fd, with the mode specified in the flags. @@ -81,31 +132,13 @@ int message_read(int in_fd, int flags, struct message *m); * the "score/threshold" line. */ long message_write(int out_fd, struct message *m); -/* Pass the message through spamd (at addr) as the specified user, with the - * given flags. Returns EX_OK on success, or various errors on error. If it was - * successful, message_write will print either the CHECK_ONLY output, or the - * filtered message in the appropriate output format. */ -int message_filter(const struct sockaddr *addr, char *username, int flags, struct message *m); - -/* Convert the host/port into a struct sockaddr. Returns EX_OK on success, or - * else an error EX. */ -int lookup_host(const char *hostname, int port, struct sockaddr *a); - -/* Pass the message through one of a set of spamd's. This variant will handle - * multiple spamd machines; if a connect failure occurs, it will fail-over to - * the next one in the struct hostent. Otherwise identical to message_filter(). +/* Process the message through the spamd filter, making as many connection + * attempts as are implied by the transport structure. To make this do + * failover, more than one host is defined, but if there is only one there, + * no failover is done. */ -int message_filter_with_failover (const struct hostent *hent, int port, char - *username, int flags, struct message *m); - -/* Convert the host into a struct hostent, for use with - * message_filter_with_failover() above. Returns EX_OK on success, or else an - * error EX. Note that the data filled into hent is from gethostbyname()'s - * static storage, so any call to gethostbyname() between - * lookup_host_for_failover() and message_filter_with_failover() will overwrite - * this. Take a copy, and use that instead, if you think a call may occur in - * your code, or library code that you use (such as syslog()). */ -int lookup_host_for_failover(const char *hostname, struct hostent *hent); +int message_filter(struct transport *tp, const char *username, + int flags, struct message *m); /* Dump the message. If there is any data in the message (typically, m->type * will be MESSAGE_ERROR) it will be message_writed. Then, fd_in will be piped @@ -116,14 +149,14 @@ void message_dump(int in_fd, int out_fd, struct message *m); /* Do a message_read->message_filter->message_write sequence, handling errors * appropriately with dump_message or appropriate CHECK_ONLY output. Returns * EX_OK or EX_ISSPAM/EX_NOTSPAM on success, some error EX on error. */ -int message_process(const char *hostname, int port, char *username, int max_size, int in_fd, int out_fd, const int flags); +int message_process(struct transport *trans, char *username, int max_size, int in_fd, int out_fd, const int flags); /* Cleanup the resources we allocated for storing the message. Call after * you're done processing. */ void message_cleanup(struct message *m); /* Aug 14, 2002 bj: This is now legacy, don't use it. */ -int process_message(const char *hostname, int port, char *username, +int process_message(struct transport *tp, char *username, int max_size, int in_fd, int out_fd, const int check_only, const int safe_fallback); diff --git a/src/plugins/spamassassin/spamassassin.c b/src/plugins/spamassassin/spamassassin.c index 195dfe530..dde2a5616 100644 --- a/src/plugins/spamassassin/spamassassin.c +++ b/src/plugins/spamassassin/spamassassin.c @@ -111,12 +111,17 @@ gboolean timeout_func(gpointer data) static gboolean msg_is_spam(FILE *fp) { - struct sockaddr addr; + struct transport trans; struct message m; gboolean is_spam = FALSE; - if (lookup_host(config.hostname, config.port, &addr) != EX_OK) { - debug_print("failed to look up spamd host\n"); + transport_init(&trans); + trans.type = TRANSPORT_TCP; + trans.hostname = config.hostname; + trans.port = config.port; + + if (transport_setup(&trans, flags) != EX_OK) { + debug_print("failed to setup transport\n"); return FALSE; } @@ -130,7 +135,7 @@ static gboolean msg_is_spam(FILE *fp) return FALSE; } - if (message_filter(&addr, username, flags, &m) != EX_OK) { + if (message_filter(&trans, username, flags, &m) != EX_OK) { debug_print("filtering the message failed\n"); message_cleanup(&m); return FALSE; diff --git a/src/plugins/spamassassin/utils.c b/src/plugins/spamassassin/utils.c index a4bb891bc..b67ca673e 100644 --- a/src/plugins/spamassassin/utils.c +++ b/src/plugins/spamassassin/utils.c @@ -22,6 +22,7 @@ /* Aug 14, 2002 bj: EINTR and EAGAIN aren't fatal, are they? */ /* Aug 14, 2002 bj: moved these to utils.c */ /* Jan 13, 2003 ym: added timeout functionality */ +/* Apr 24, 2003 sjf: made full_read and full_write void* params */ /* -------------------------------------------------------------------------- */ @@ -38,7 +39,7 @@ sigfunc* sig_catch(int sig, void (*f)(int)) } static void catch_alrm(int x) { - /* dummy */ + UNUSED_VARIABLE(x); } ssize_t @@ -75,6 +76,12 @@ ssl_timeout_read (SSL *ssl, void *buf, int nbytes) int nred; sigfunc* sig; +#ifndef SPAMC_SSL + UNUSED_VARIABLE(ssl); + UNUSED_VARIABLE(buf); + UNUSED_VARIABLE(nbytes); +#endif + sig = sig_catch(SIGALRM, catch_alrm); if (libspamc_timeout > 0) { alarm(libspamc_timeout); @@ -104,8 +111,9 @@ ssl_timeout_read (SSL *ssl, void *buf, int nbytes) /* -------------------------------------------------------------------------- */ int -full_read (int fd, unsigned char *buf, int min, int len) +full_read (int fd, void *vbuf, int min, int len) { + unsigned char *buf = (unsigned char *)vbuf; int total; int thistime; @@ -126,8 +134,31 @@ full_read (int fd, unsigned char *buf, int min, int len) } int -full_write (int fd, const unsigned char *buf, int len) +full_read_ssl (SSL *ssl, unsigned char *buf, int min, int len) +{ + int total; + int thistime; + + for (total = 0; total < min; ) { + thistime = ssl_timeout_read (ssl, buf+total, len-total); + + if (thistime < 0) { + return -1; + } else if (thistime == 0) { + /* EOF, but we didn't read the minimum. return what we've read + * so far and next read (if there is one) will return 0. */ + return total; + } + + total += thistime; + } + return total; +} + +int +full_write (int fd, const void *vbuf, int len) { + const unsigned char *buf = (const unsigned char *)vbuf; int total; int thistime; diff --git a/src/plugins/spamassassin/utils.h b/src/plugins/spamassassin/utils.h index 9522226aa..4df7c5333 100644 --- a/src/plugins/spamassassin/utils.h +++ b/src/plugins/spamassassin/utils.h @@ -1,6 +1,8 @@ #ifndef UTILS_H #define UTILS_H +#define UNUSED_VARIABLE(v) ((void)(v)) + extern int libspamc_timeout; /* default timeout in seconds */ #ifdef SPAMC_SSL @@ -18,7 +20,8 @@ ssize_t fd_timeout_read (int fd, void *, size_t ); int ssl_timeout_read (SSL *ssl, void *, int ); /* these are fd-only, no SSL support */ -int full_read(int fd, unsigned char *buf, int min, int len); -int full_write(int fd, const unsigned char *buf, int len); +int full_read(int fd, void *buf, int min, int len); +int full_read_ssl(SSL *ssl, unsigned char *buf, int min, int len); +int full_write(int fd, const void *buf, int len); #endif -- 2.25.1