blob: ed5f66a77357d0edd67c1aba9604cfd071efec63 [file] [log] [blame]
/* <@LICENSE>
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* </@LICENSE>
*/
/*
Compile with extra warnings -- gcc only, not suitable for use as default:
gcc -Wextra -Wdeclaration-after-statement -Wall -g -O2 spamc/spamc.c \
spamc/getopt.c spamc/libspamc.c spamc/utils.c -o spamc/spamc -ldl -lz
*/
#include "config.h"
#include "libspamc.h"
#include <stdarg.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#define strcasecmp stricmp
#define sleep Sleep
#include <io.h>
#else
#include <strings.h>
#include <syslog.h>
#include <unistd.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>
#define closesocket(x) close(x)
#endif
#ifdef HAVE_SYSEXITS_H
#include <sysexits.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_ZLIB_H
#include <zlib.h>
#endif
/* must load *after* errno.h, Bug 6697 */
#include "utils.h"
/* RedHat 5.2 doesn't define Shutdown 2nd Parameter Constants */
/* KAM 12-4-01 */
/* 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 h_errno errno
#endif
#ifdef _WIN32
#define spamc_get_errno() WSAGetLastError()
#else
#define spamc_get_errno() errno
#endif
#ifndef HAVE_OPTARG
extern char *optarg;
#endif
#ifndef HAVE_INADDR_NONE
#define INADDR_NONE ((in_addr_t) 0xffffffff)
#endif
/* jm: turned off for now, it should not be necessary. */
#undef USE_TCP_NODELAY
#ifndef HAVE_EX__MAX
/* jm: very conservative figure, should be well out of range on almost all NIXes */
#define EX__MAX 200
#endif
#undef DO_CONNECT_DEBUG_SYSLOGS
/*
#define DO_CONNECT_DEBUG_SYSLOGS 1
#define CONNECT_DEBUG_LEVEL LOG_DEBUG
*/
/* bug 4477 comment 14 */
#ifdef NI_MAXHOST
#define SPAMC_MAXHOST NI_MAXHOST
#else
#define SPAMC_MAXHOST 256
#endif
#ifdef NI_MAXSERV
#define SPAMC_MAXSERV NI_MAXSERV
#else
#define SPAMC_MAXSERV 256
#endif
/* static const int ESC_PASSTHROUGHRAW = EX__MAX + 666; No longer seems to be used */
/* set EXPANSION_ALLOWANCE to something more than might be
added to a message in X-headers and the report template */
static const int EXPANSION_ALLOWANCE = 16384;
/* set NUM_CHECK_BYTES to number of bytes that have to match at beginning and end
of the data streams before and after processing by spamd
Aug 7 2002 jm: no longer seems to be used
static const int NUM_CHECK_BYTES = 32;
*/
/* Set the protocol version that this spamc speaks */
static const char *PROTOCOL_VERSION = "SPAMC/1.5";
/* "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 alloced_size; /* allocated space for the "out" buffer */
void (*spamc_header_callback)(struct message *m, int flags, char *buf, int len);
void (*spamd_header_callback)(struct message *m, int flags, const char *buf, int len);
};
void (*libspamc_log_callback)(int flags, int level, char *msg, va_list args) = NULL;
int libspamc_timeout = 0;
int libspamc_connect_timeout = 0; /* Sep 8, 2008 mrgus: separate connect timeout */
/*
* 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 family (PF_INET or PF_INET6 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.
*/
#ifdef SPAMC_HAS_ADDRINFO
static int _opensocket(int flags, struct addrinfo *res, int *psock)
{
#else
static int _opensocket(int flags, int type, int *psock)
{
int proto = 0;
#endif
const char *typename;
int origerr;
#ifdef _WIN32
int socktout;
#endif
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.
*/
#ifdef SPAMC_HAS_ADDRINFO
switch(res->ai_family) {
case PF_UNIX:
typename = "PF_UNIX";
break;
case PF_INET:
typename = "PF_INET";
break;
case PF_INET6:
typename = "PF_INET6";
break;
default:
typename = "Unknown";
break;
}
#else
if (type == PF_UNIX) {
typename = "PF_UNIX";
}
else {
typename = "PF_INET";
proto = IPPROTO_TCP;
}
#endif
#ifdef DO_CONNECT_DEBUG_SYSLOGS
libspamc_log(flags, CONNECT_DEBUG_LEVEL, "dbg: create socket(%s)", typename);
#endif
#ifdef SPAMC_HAS_ADDRINFO
if ((*psock = socket(res->ai_family, res->ai_socktype, res->ai_protocol))
#else
if ((*psock = socket(type, SOCK_STREAM, proto))
#endif
#ifndef _WIN32
< 0
#else
== INVALID_SOCKET
#endif
) {
/*--------------------------------------------------------
* 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 = spamc_get_errno();
#ifndef _WIN32
libspamc_log(flags, LOG_ERR, "socket(%s) to spamd failed: %s", typename, strerror(origerr));
#else
libspamc_log(flags, LOG_ERR, "socket(%s) to spamd failed: %d", typename, origerr);
#endif
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;
}
}
#ifdef _WIN32
/* bug 4344: makes timeout functional on Win32 */
socktout = libspamc_timeout * 1000;
if (type == PF_INET
&& setsockopt(*psock, SOL_SOCKET, SO_RCVTIMEO, (char *)&socktout, sizeof(socktout)) != 0)
{
origerr = spamc_get_errno();
switch (origerr)
{
case EBADF:
case ENOTSOCK:
case ENOPROTOOPT:
case EFAULT:
libspamc_log(flags, LOG_ERR, "setsockopt(SO_RCVTIMEO) failed: %d", origerr);
closesocket(*psock);
return EX_SOFTWARE;
default:
break; /* ignored */
}
}
#endif
/*----------------------------------------------------------------
* 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
#ifdef PF_INET6
|| type == PF_INET6
#endif
) && setsockopt(*psock, 0, TCP_NODELAY, &one, sizeof one) != 0) {
origerr = spamc_get_errno();
switch (origerr) {
case EBADF:
case ENOTSOCK:
case ENOPROTOOPT:
case EFAULT:
libspamc_log(flags, LOG_ERR,
#ifndef _WIN32
"setsockopt(TCP_NODELAY) failed: %s", strerror(origerr));
#else
"setsockopt(TCP_NODELAY) failed: %d", origerr);
#endif
closesocket(*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)
{
#ifndef _WIN32
int mysock, status, origerr;
struct sockaddr_un addrbuf;
#ifdef SPAMC_HAS_ADDRINFO
struct addrinfo hints, *res;
#else
int res = PF_UNIX;
#endif
int ret;
assert(tp != 0);
assert(sockptr != 0);
assert(tp->socketpath != 0);
#ifdef SPAMC_HAS_ADDRINFO
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNIX;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = 0;
res = &hints;
#endif
/*----------------------------------------------------------------
* If the socket itself can't be created, this is a fatal error.
*/
if ((ret = _opensocket(tp->flags, res, &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
libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL, "dbg: connect(AF_UNIX) to spamd at %s",
addrbuf.sun_path);
#endif
status = timeout_connect(mysock, (struct sockaddr *) &addrbuf, sizeof(addrbuf));
origerr = errno;
if (status >= 0) {
#ifdef DO_CONNECT_DEBUG_SYSLOGS
libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL, "dbg: connect(AF_UNIX) ok");
#endif
*sockptr = mysock;
return EX_OK;
}
libspamc_log(tp->flags, LOG_ERR, "connect(AF_UNIX) to spamd using --socket='%s' failed: %s",
addrbuf.sun_path, strerror(origerr));
closesocket(mysock);
return _translate_connect_errno(origerr);
#else
(void) tp; /* not used. suppress compiler warning */
(void) sockptr; /* not used. suppress compiler warning */
return EX_OSERR;
#endif
}
/*
* 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;
#ifdef SPAMC_HAS_ADDRINFO
struct addrinfo *res = NULL;
char port[SPAMC_MAXSERV-1]; /* port, for logging */
#else
int res = PF_INET;
#endif
char host[SPAMC_MAXHOST-1]; /* hostname, for logging */
int connect_retries, retry_sleep;
assert(tp != 0);
assert(sockptr != 0);
assert(tp->nhosts > 0);
/* default values */
retry_sleep = tp->retry_sleep;
connect_retries = tp->connect_retries;
if (connect_retries == 0) {
connect_retries = 3;
}
if (retry_sleep < 0) {
retry_sleep = 1;
}
for (numloops = 0; numloops < connect_retries; numloops++) {
const int hostix = numloops % tp->nhosts;
int status, mysock;
int innocent = 0;
/*--------------------------------------------------------
* We always start by creating the socket, as we get only
* one attempt to connect() on each one. If this fails,
* we're done.
*/
#ifdef SPAMC_HAS_ADDRINFO
res = tp->hosts[hostix];
while(res) {
char *family = NULL;
switch(res->ai_family) {
case AF_INET:
family = "AF_INET";
break;
case AF_INET6:
family = "AF_INET6";
break;
default:
family = "Unknown";
break;
}
if ((ret = _opensocket(tp->flags, res, &mysock)) != EX_OK) {
res = res->ai_next;
continue;
}
getnameinfo(res->ai_addr, res->ai_addrlen,
host, sizeof(host),
port, sizeof(port),
NI_NUMERICHOST|NI_NUMERICSERV);
#ifdef DO_CONNECT_DEBUG_SYSLOGS
libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL,
"dbg: connect(%s) to spamd (host %s, port %s) (try #%d of %d)",
family, host, port, numloops + 1, connect_retries);
#endif
/* this is special-cased so that we have an address we can
* safely use as an "always fail" test case */
if (!strcmp(host, "255.255.255.255")) {
libspamc_log(tp->flags, LOG_ERR,
"connect to spamd on %s failed, broadcast addr",
host);
status = -1;
}
else {
status = timeout_connect(mysock, res->ai_addr, res->ai_addrlen);
if (status != 0) origerr = spamc_get_errno();
}
#else
struct sockaddr_in addrbuf;
const char *ipaddr;
const char* family="AF_INET";
if ((ret = _opensocket(tp->flags, 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);
/* make a copy in host, for logging (bug 5577) */
strncpy (host, ipaddr, sizeof(host) - 1);
#ifdef DO_CONNECT_DEBUG_SYSLOGS
libspamc_log(tp->flags, LOG_DEBUG,
"dbg: connect(AF_INET) to spamd at %s (try #%d of %d)",
ipaddr, numloops + 1, connect_retries);
#endif
/* this is special-cased so that we have an address we can
* safely use as an "always fail" test case */
if (!strcmp(ipaddr, "255.255.255.255")) {
libspamc_log(tp->flags, LOG_ERR,
"connect to spamd on %s failed, broadcast addr",
ipaddr);
status = -1;
}
else {
status = timeout_connect(mysock, (struct sockaddr *) &addrbuf,
sizeof(addrbuf));
if (status != 0) origerr = spamc_get_errno();
}
#endif
if (status != 0) {
closesocket(mysock);
innocent = origerr == ECONNREFUSED && numloops+1 < tp->nhosts;
libspamc_log(tp->flags, innocent ? LOG_DEBUG : LOG_ERR,
"connect to spamd on %s failed, retrying (#%d of %d): %s",
host, numloops+1, connect_retries,
#ifdef _WIN32
origerr
#else
strerror(origerr)
#endif
);
} else {
#ifdef DO_CONNECT_DEBUG_SYSLOGS
libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL,
"dbg: connect(%s) to spamd done",family);
#endif
*sockptr = mysock;
return EX_OK;
}
#ifdef SPAMC_HAS_ADDRINFO
res = res->ai_next;
}
#endif
if (numloops+1 < connect_retries && !innocent) sleep(retry_sleep);
} /* for(numloops...) */
libspamc_log(tp->flags, LOG_ERR,
"connection attempt to spamd aborted after %d retries",
connect_retries);
return _translate_connect_errno(origerr);
}
#ifdef SPAMC_SSL
static char * _ssl_err_as_string (void) {
BIO *bio = BIO_new(BIO_s_mem());
ERR_print_errors(bio);
char *buf = NULL;
size_t len = BIO_get_mem_data(bio, &buf);
char *ret = (char *)calloc(1, 1 + len);
if (!ret) {
BIO_free(bio);
char *err = "(could not get SSL error)";
return err;
}
memcpy(ret, buf, len);
BIO_free(bio);
/* Only return up to first newline */
char *lf = strchr(ret, '\n');
if (lf)
*lf = '\0';
return ret;
}
static SSL_CTX * _try_ssl_ctx_init(int flags)
{
const SSL_METHOD *meth;
SSL_CTX *ctx;
SSLeay_add_ssl_algorithms();
SSL_load_error_strings();
/* this method allows negotiation of version */
meth = SSLv23_client_method();
ctx = SSL_CTX_new(meth);
if (ctx == NULL) {
libspamc_log(flags, LOG_ERR, "cannot create SSL CTX context: %s",
_ssl_err_as_string());
return NULL;
}
if (flags & SPAMC_TLSV1) {
/* allow TLSv1.0 or better */
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3);
} else {
/* allow SSLv3 or better */
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
}
SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
return ctx;
}
static int _try_ssl_connect(SSL_CTX *ctx, struct transport *tp,
SSL **pssl, int flags, int sock)
{
SSL *ssl;
int ssl_rtn;
if (tp->ssl_ca_file || tp->ssl_ca_path) {
if (!SSL_CTX_load_verify_locations(ctx, tp->ssl_ca_file,
tp->ssl_ca_path)) {
libspamc_log(flags, LOG_ERR,
"error loading CA file %s or path %s: %s",
tp->ssl_ca_file ? tp->ssl_ca_file : "(void)",
tp->ssl_ca_path ? tp->ssl_ca_path : "(void)",
_ssl_err_as_string());
return EX_OSERR;
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
} else {
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
}
if (flags & SPAMC_CLIENT_SSL_CERT) {
/* libspamc_log(flags, LOG_ERR, "loading client cert %s key %s",
tp->ssl_cert_file, tp->ssl_key_file); */
if (!SSL_CTX_use_certificate_file(ctx, tp->ssl_cert_file,
SSL_FILETYPE_PEM)) {
libspamc_log(flags, LOG_ERR,
"unable to load certificate file %s: %s",
tp->ssl_cert_file, _ssl_err_as_string());
return EX_OSERR;
}
if (!SSL_CTX_use_PrivateKey_file(ctx, tp->ssl_key_file,
SSL_FILETYPE_PEM)) {
libspamc_log(flags, LOG_ERR,
"unable to load key file %s: %s",
tp->ssl_key_file, _ssl_err_as_string());
return EX_OSERR;
}
if (!SSL_CTX_check_private_key(ctx)) {
libspamc_log(flags, LOG_ERR,
"key file %s and cert file %s do not match: %s",
tp->ssl_key_file, tp->ssl_cert_file,
_ssl_err_as_string());
return EX_OSERR;
}
}
ssl = SSL_new(ctx);
if (ssl == NULL) {
libspamc_log(flags, LOG_ERR,
"SSL_new failed: %s", _ssl_err_as_string());
return EX_OSERR;
}
*pssl = ssl;
if (!SSL_set_fd(ssl, sock)) {
libspamc_log(flags, LOG_ERR,
"SSL_set_fd failed: %s", _ssl_err_as_string());
return EX_OSERR;
}
ssl_rtn = SSL_connect(ssl);
if (ssl_rtn != 1) {
int ssl_err = SSL_get_error(ssl, ssl_rtn);
libspamc_log(flags, LOG_ERR,
"SSL_connect error: %s", _ssl_err_as_string());
return EX_UNAVAILABLE;
}
return EX_OK;
}
#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
* of helper functions.
*/
static void _clear_message(struct message *m)
{
m->type = MESSAGE_NONE;
m->raw = NULL;
m->raw_len = 0;
m->pre = NULL;
m->pre_len = 0;
m->msg = NULL;
m->msg_len = 0;
m->post = NULL;
m->post_len = 0;
m->is_spam = EX_TOOBIG;
m->score = 0.0;
m->threshold = 0.0;
m->outbuf = NULL;
m->out = NULL;
m->out_len = 0;
m->content_length = -1;
}
static void _free_zlib_buffer(unsigned char **zlib_buf, int *zlib_bufsiz)
{
if(*zlib_buf) {
free(*zlib_buf);
*zlib_buf=NULL;
}
*zlib_bufsiz=0;
}
static void _use_msg_for_out(struct message *m)
{
if (m->outbuf)
free(m->outbuf);
m->outbuf = NULL;
m->out = m->msg;
m->out_len = m->msg_len;
}
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, 1, 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;
}
m->type = MESSAGE_ERROR;
if (m->raw_len > (int) m->max_len)
{
libspamc_log(m->priv->flags, LOG_NOTICE,
"skipped message, greater than max message size (%d bytes)",
m->max_len);
return EX_TOOBIG;
}
m->type = MESSAGE_RAW;
m->msg = m->raw;
m->msg_len = m->raw_len;
m->out = m->msg;
m->out_len = m->msg_len;
return EX_OK;
}
static int _message_read_bsmtp(int fd, struct message *m)
{
unsigned int i, j, p_len;
char prev;
char* p;
_clear_message(m);
if ((m->raw = malloc(m->max_len + 1)) == NULL)
return EX_OSERR;
/* Find the DATA line */
m->raw_len = full_read(fd, 1, 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;
}
m->type = MESSAGE_ERROR;
if (m->raw_len > (int) m->max_len)
return EX_TOOBIG;
p = m->pre = m->raw;
/* Search for \nDATA\n which marks start of actual message */
while ((p_len = (m->raw_len - (p - m->raw))) > 8) { /* leave room for at least \nDATA\n.\n */
char* q = memchr(p, '\n', p_len - 8); /* find next \n then see if start of \nDATA\n */
if (q == NULL) break;
q++;
if (((q[0]|0x20) == 'd') && /* case-insensitive ASCII comparison */
((q[1]|0x20) == 'a') &&
((q[2]|0x20) == 't') &&
((q[3]|0x20) == 'a')) {
q+=4;
if (q[0] == '\r') ++q;
if (*(q++) == '\n') { /* leave q at start of message if we found it */
m->msg = q;
m->pre_len = q - m->raw;
m->msg_len = m->raw_len - m->pre_len;
break;
}
}
p = q; /* the above code ensures no other '\n' comes before q */
}
if (m->msg == NULL)
return EX_DATAERR;
/* ensure this is >= 0 */
if (m->msg_len < 0) {
return EX_SOFTWARE;
}
/* Find the end-of-DATA line */
prev = '\n';
for (i = j = 0; i < (unsigned int) m->msg_len; i++) {
if (prev == '\n' && m->msg[i] == '.') {
/* Dot at the beginning of a line */
if (((int) (i+1) == m->msg_len)
|| ((int) (i+1) < m->msg_len && m->msg[i + 1] == '\n')
|| ((int) (i+2) < m->msg_len && m->msg[i + 1] == '\r' && m->msg[i + 2] == '\n')) {
/* Lone dot! That's all, folks */
m->post = m->msg + i;
m->post_len = m->msg_len - i;
m->msg_len = j;
break;
}
else if ((int) (i+1) < m->msg_len && m->msg[i + 1] == '.') {
/* Escaping dot, eliminate. */
prev = '.';
continue;
} /* Else an ordinary dot, drop down to ordinary char handler */
}
prev = m->msg[i];
m->msg[j++] = m->msg[i];
}
/* if bad format with no end "\n.\n", error out */
if (m->post == NULL)
return EX_DATAERR;
m->type = MESSAGE_BSMTP;
m->out = m->msg;
m->out_len = m->msg_len;
return EX_OK;
}
int message_read(int fd, int flags, struct message *m)
{
assert(m != NULL);
libspamc_timeout = 0;
/* create the "private" part of the struct message */
m->priv = malloc(sizeof(struct libspamc_private_message));
if (m->priv == NULL) {
libspamc_log(flags, LOG_ERR, "message_read: malloc failed");
return EX_OSERR;
}
m->priv->flags = flags;
m->priv->alloced_size = 0;
m->priv->spamc_header_callback = 0;
m->priv->spamd_header_callback = 0;
if (flags & SPAMC_PING) {
_clear_message(m);
return EX_OK;
}
switch (flags & SPAMC_MODE_MASK) {
case SPAMC_RAW_MODE:
return _message_read_raw(fd, m);
case SPAMC_BSMTP_MODE:
return _message_read_bsmtp(fd, m);
default:
libspamc_log(flags, LOG_ERR, "message_read: Unknown mode %d",
flags & SPAMC_MODE_MASK);
return EX_USAGE;
}
}
long message_write(int fd, struct message *m)
{
long total = 0;
off_t i, j;
off_t jlimit;
char buffer[1024];
assert(m != NULL);
if (m->priv->flags & (SPAMC_CHECK_ONLY|SPAMC_PING)) {
if (m->is_spam == EX_ISSPAM || m->is_spam == EX_NOTSPAM) {
return full_write(fd, 1, m->out, m->out_len);
}
else {
libspamc_log(m->priv->flags, LOG_ERR, "oops! SPAMC_CHECK_ONLY is_spam: %d",
m->is_spam);
return -1;
}
}
/* else we're not in CHECK_ONLY mode */
switch (m->type) {
case MESSAGE_NONE:
libspamc_log(m->priv->flags, LOG_ERR, "Cannot write this message, it's MESSAGE_NONE!");
return -1;
case MESSAGE_ERROR:
return full_write(fd, 1, m->raw, m->raw_len);
case MESSAGE_RAW:
return full_write(fd, 1, m->out, m->out_len);
case MESSAGE_BSMTP:
total = full_write(fd, 1, 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 && j < jlimit;) {
if (i + 1 < m->out_len && m->out[i] == '\n'
&& m->out[i + 1] == '.') {
if (j > jlimit - 4) {
break; /* avoid overflow */
}
buffer[j++] = m->out[i++];
buffer[j++] = m->out[i++];
buffer[j++] = '.';
}
else {
buffer[j++] = m->out[i++];
}
}
total += full_write(fd, 1, buffer, j);
}
return total + full_write(fd, 1, m->post, m->post_len);
default:
libspamc_log(m->priv->flags, LOG_ERR, "Unknown message type %d", m->type);
return -1;
}
}
void message_dump(int in_fd, int out_fd, struct message *m, int flags)
{
char buf[8196];
int bytes;
if (m == NULL) {
libspamc_log(flags, LOG_ERR, "oops! message_dump called with NULL message");
return;
}
if (m->type != MESSAGE_NONE) {
message_write(out_fd, m);
}
while ((bytes = full_read(in_fd, 1, buf, 8192, 8192)) > 0) {
if (bytes != full_write(out_fd, 1, buf, bytes)) {
libspamc_log(flags, LOG_ERR, "oops! message_dump of %d returned different",
bytes);
}
}
}
static int
_spamc_read_full_line(struct message *m, int flags, SSL * ssl, int sock,
char *buf, size_t *lenp, size_t bufsiz)
{
int failureval;
int bytesread = 0;
size_t len;
UNUSED_VARIABLE(m);
*lenp = 0;
/* 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, 0, buf + len, 1);
}
if (bytesread <= 0) {
failureval = EX_IOERR;
goto failure;
}
if (buf[len] == '\n') {
buf[len] = '\0';
if (len > 0 && buf[len - 1] == '\r') {
len--;
buf[len] = '\0';
}
*lenp = len;
return EX_OK;
}
}
libspamc_log(flags, 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));
/* note: don't compare floats == 0.0, it's unsafe. use a range */
if (postdot >= -0.00001 && postdot <= 0.00001) {
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,
unsigned int *didtellflags)
{
char is_spam[6];
char s_str[21], t_str[21];
char didset_ret[15];
char didremove_ret[15];
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);
/* set bounds on these to ensure no buffer overflow in the sprintf */
if (m->score > 1e10)
m->score = 1e10;
else if (m->score < -1e10)
m->score = -1e10;
if (m->threshold > 1e10)
m->threshold = 1e10;
else if (m->threshold < -1e10)
m->threshold = -1e10;
/* 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 = sprintf(m->out,
"%.1f/%.1f\n", m->score, m->threshold);
}
else if ((flags & SPAMC_REPORT_IFSPAM && m->is_spam == EX_ISSPAM)
|| (flags & SPAMC_REPORT)) {
m->out_len = sprintf(m->out,
"%.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) {
libspamc_log(flags, LOG_ERR, "spamd responded with bad Content-length '%s'",
buf);
return EX_PROTOCOL;
}
return EX_OK;
}
else if (sscanf(buf, "DidSet: %14s", didset_ret) == 1) {
if (strstr(didset_ret, "local")) {
*didtellflags |= SPAMC_SET_LOCAL;
}
if (strstr(didset_ret, "remote")) {
*didtellflags |= SPAMC_SET_REMOTE;
}
}
else if (sscanf(buf, "DidRemove: %14s", didremove_ret) == 1) {
if (strstr(didremove_ret, "local")) {
*didtellflags |= SPAMC_REMOVE_LOCAL;
}
if (strstr(didremove_ret, "remote")) {
*didtellflags |= SPAMC_REMOVE_REMOTE;
}
}
else if (m->priv->spamd_header_callback != NULL)
m->priv->spamd_header_callback(m, flags, buf, len);
return EX_OK;
}
static int
_zlib_compress (char *m_msg, int m_msg_len,
unsigned char **zlib_buf, int *zlib_bufsiz, int flags)
{
int rc;
int len, totallen;
#ifndef HAVE_LIBZ
UNUSED_VARIABLE(m_msg);
UNUSED_VARIABLE(m_msg_len);
UNUSED_VARIABLE(zlib_buf);
UNUSED_VARIABLE(zlib_bufsiz);
UNUSED_VARIABLE(rc);
UNUSED_VARIABLE(len);
UNUSED_VARIABLE(totallen);
libspamc_log(flags, LOG_ERR, "spamc not built with zlib support");
return EX_SOFTWARE;
#else
z_stream strm;
UNUSED_VARIABLE(flags);
/* worst-case, according to http://www.zlib.org/zlib_tech.html ;
* same as input, plus 5 bytes per 16k, plus 6 bytes. this should
* be plenty */
*zlib_bufsiz = (int) (m_msg_len * 1.0005) + 1024;
*zlib_buf = (unsigned char *) malloc (*zlib_bufsiz);
if (*zlib_buf == NULL) {
return EX_OSERR;
}
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
rc = deflateInit(&strm, 3);
if (rc != Z_OK) {
return EX_OSERR;
}
strm.avail_in = m_msg_len;
strm.next_in = (unsigned char *) m_msg;
strm.avail_out = *zlib_bufsiz;
strm.next_out = (unsigned char *) *zlib_buf;
totallen = 0;
do {
rc = deflate(&strm, Z_FINISH);
assert(rc != Z_STREAM_ERROR);
len = (size_t) (*zlib_bufsiz - strm.avail_out);
strm.next_out += len;
totallen += len;
} while (strm.avail_out == 0);
*zlib_bufsiz = totallen;
return EX_OK;
#endif
}
int
_append_original_body (struct message *m, int flags)
{
char *cp, *cpend, *bodystart;
int bodylen, outspaceleft, towrite;
/* at this stage, m->out now contains the rewritten headers.
* find and append the raw message's body, up to m->priv->alloced_size
* bytes.
*/
#define CRNLCRNL "\r\n\r\n"
#define CRNLCRNL_LEN 4
#define NLNL "\n\n"
#define NLNL_LEN 2
cpend = m->raw + m->raw_len;
bodystart = NULL;
for (cp = m->raw; cp < cpend; cp++) {
if (*cp == '\r' && cpend - cp >= CRNLCRNL_LEN &&
!strncmp(cp, CRNLCRNL, CRNLCRNL_LEN))
{
bodystart = cp + CRNLCRNL_LEN;
break;
}
else if (*cp == '\n' && cpend - cp >= NLNL_LEN &&
!strncmp(cp, NLNL, NLNL_LEN))
{
bodystart = cp + NLNL_LEN;
break;
}
}
if (bodystart == NULL) {
libspamc_log(flags, LOG_ERR, "failed to find end-of-headers");
return EX_SOFTWARE;
}
bodylen = cpend - bodystart;
outspaceleft = (m->priv->alloced_size-1) - m->out_len;
towrite = (bodylen < outspaceleft ? bodylen : outspaceleft);
/* copy in the body; careful not to overflow */
strncpy (m->out + m->out_len, bodystart, towrite);
m->out_len += towrite;
return EX_OK;
}
int message_filter(struct transport *tp, const char *username,
int flags, struct message *m)
{
char buf[8192];
size_t bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
size_t len;
int sock = -1;
int rc;
char versbuf[20];
float version;
int response;
int failureval = EX_SOFTWARE;
unsigned int throwaway;
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
const SSL_METHOD *meth;
char zlib_on = 0;
unsigned char *zlib_buf = NULL;
int zlib_bufsiz = 0;
unsigned char *towrite_buf;
int towrite_len;
int filter_retry_count;
int filter_retry_sleep;
int filter_retries;
#ifdef SPAMC_HAS_ADDRINFO
struct addrinfo *tmphost;
#else
struct in_addr tmphost;
#endif
int nhost_counter;
assert(tp != NULL);
assert(m != NULL);
if ((flags & SPAMC_USE_ZLIB) != 0) {
zlib_on = 1;
}
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
ctx = _try_ssl_ctx_init(flags);
if (ctx == NULL) {
failureval = EX_OSERR;
goto failure;
}
#else
UNUSED_VARIABLE(ssl);
UNUSED_VARIABLE(ctx);
libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
return EX_SOFTWARE;
#endif
}
m->is_spam = EX_TOOBIG;
if (m->outbuf != NULL)
free(m->outbuf);
m->priv->alloced_size = m->max_len + EXPANSION_ALLOWANCE + 1;
if ((m->outbuf = malloc(m->priv->alloced_size)) == NULL) {
failureval = EX_OSERR;
goto failure;
}
m->out = m->outbuf;
m->out_len = 0;
/* If the spamd filter takes too long and we timeout, then
* retry again. This gets us around a hung child thread
* in spamd or a problem on a spamd host in a multi-host
* setup. If there is more than one destination host
* we move to the next host on each attempt.
*/
/* default values */
filter_retry_sleep = tp->filter_retry_sleep;
filter_retries = tp->filter_retries;
if (filter_retries == 0) {
filter_retries = 1;
}
if (filter_retry_sleep < 0) {
filter_retry_sleep = 1;
}
/* filterloop - Ensure that we run through this at least
* once, and again if there are errors
*/
filter_retry_count = 0;
while ((filter_retry_count==0) ||
((filter_retry_count<tp->filter_retries) && (failureval == EX_IOERR)))
{
if (filter_retry_count != 0){
/* Ensure that the old socket gets closed */
if (sock != -1) {
closesocket(sock);
sock=-1;
}
/* Move to the next host in the list, if nhosts>1 */
if (tp->nhosts > 1) {
tmphost = tp->hosts[0];
/* TODO: free using freeaddrinfo() */
for (nhost_counter = 1; nhost_counter < tp->nhosts; nhost_counter++) {
tp->hosts[nhost_counter - 1] = tp->hosts[nhost_counter];
}
tp->hosts[nhost_counter - 1] = tmphost;
}
/* Now sleep the requested amount */
sleep(filter_retry_sleep);
}
filter_retry_count++;
/* Build spamd protocol header */
if (flags & SPAMC_CHECK_ONLY)
strcpy(buf, "CHECK ");
else if (flags & SPAMC_REPORT_IFSPAM)
strcpy(buf, "REPORT_IFSPAM ");
else if (flags & SPAMC_REPORT)
strcpy(buf, "REPORT ");
else if (flags & SPAMC_SYMBOLS)
strcpy(buf, "SYMBOLS ");
else if (flags & SPAMC_PING)
strcpy(buf, "PING ");
else if (flags & SPAMC_HEADERS)
strcpy(buf, "HEADERS ");
else
strcpy(buf, "PROCESS ");
len = strlen(buf);
if (len + strlen(PROTOCOL_VERSION) + 2 >= bufsiz) {
_use_msg_for_out(m);
return EX_OSERR;
}
strcat(buf, PROTOCOL_VERSION);
strcat(buf, "\r\n");
len = strlen(buf);
towrite_buf = (unsigned char *) m->msg;
towrite_len = (int) m->msg_len;
if (zlib_on) {
if (_zlib_compress(m->msg, m->msg_len, &zlib_buf, &zlib_bufsiz, flags) != EX_OK)
{
_free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
return EX_OSERR;
}
towrite_buf = zlib_buf;
towrite_len = zlib_bufsiz;
}
if (!(flags & SPAMC_PING)) {
if (username != NULL) {
if (strlen(username) + 8 >= (bufsiz - len)) {
_use_msg_for_out(m);
if (zlib_on) {
_free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
}
return EX_OSERR;
}
strcpy(buf + len, "User: ");
strcat(buf + len, username);
strcat(buf + len, "\r\n");
len += strlen(buf + len);
}
if (zlib_on) {
len += snprintf(buf + len, 8192-len, "Compress: zlib\r\n");
}
if ((m->msg_len > SPAMC_MAX_MESSAGE_LEN) || ((len + 27) >= (bufsiz - len))) {
_use_msg_for_out(m);
if (zlib_on) {
_free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
}
return EX_DATAERR;
}
len += snprintf(buf + len, 8192-len, "Content-length: %d\r\n", (int) towrite_len);
}
/* bug 6187, PING needs empty line too, bumps protocol version to 1.5 */
len += snprintf(buf + len, 8192-len, "\r\n");
libspamc_timeout = m->timeout;
libspamc_connect_timeout = m->connect_timeout; /* Sep 8, 2008 mrgus: separate connect timeout */
if (tp->socketpath)
rc = _try_to_connect_unix(tp, &sock);
else
rc = _try_to_connect_tcp(tp, &sock);
if (rc != EX_OK) {
_use_msg_for_out(m);
if (zlib_on) {
_free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
}
return rc; /* use the error code try_to_connect_*() gave us. */
}
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
if (rc != EX_OK) {
failureval = rc;
goto failure;
}
#endif
}
/* Send to spamd */
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
rc = SSL_write(ssl, buf, len);
if (rc <= 0) {
libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
SSL_get_error(ssl, rc));
failureval = EX_IOERR;
goto failure;
}
rc = SSL_write(ssl, towrite_buf, towrite_len);
if (rc <= 0) {
libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
SSL_get_error(ssl, rc));
failureval = EX_IOERR;
goto failure;
}
SSL_shutdown(ssl);
shutdown(sock, SHUT_WR);
#endif
}
else {
full_write(sock, 0, buf, len);
full_write(sock, 0, towrite_buf, towrite_len);
shutdown(sock, SHUT_WR);
}
/* free zlib buffer
* bug 6025: zlib buffer not freed if compression is used
*/
if (zlib_on) {
_free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
}
/* ok, now read and parse it. SPAMD/1.2 line first... */
failureval =
_spamc_read_full_line(m, flags, ssl, sock, buf, &len, bufsiz);
} /* end of filterloop */
if (failureval != EX_OK) {
goto failure;
}
if (sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response) != 2) {
libspamc_log(flags, 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) {
libspamc_log(flags, LOG_ERR, "spamd responded with bad version string '%s'",
versbuf);
failureval = EX_PROTOCOL;
goto failure;
}
if (flags & SPAMC_PING) {
closesocket(sock);
sock = -1;
m->out_len = sprintf(m->out, "SPAMD/%s %d\n", versbuf, response);
m->is_spam = EX_NOTSPAM;
return EX_OK;
}
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, &throwaway) < 0) {
failureval = EX_PROTOCOL;
goto failure;
}
}
len = 0; /* overwrite those headers */
if (flags & SPAMC_CHECK_ONLY) {
closesocket(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 {
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 = full_read_ssl(ssl, (unsigned char *) m->out + m->out_len,
m->priv->alloced_size - m->out_len,
m->priv->alloced_size - m->out_len);
}
else {
len = full_read(sock, 0, m->out + m->out_len,
m->priv->alloced_size - m->out_len,
m->priv->alloced_size - m->out_len);
}
if ((int) len + (int) m->out_len > (m->priv->alloced_size - 1)) {
failureval = EX_TOOBIG;
goto failure;
}
m->out_len += len;
shutdown(sock, SHUT_RD);
closesocket(sock);
sock = -1;
}
libspamc_timeout = 0;
if (m->out_len != m->content_length) {
libspamc_log(flags, LOG_ERR,
"failed sanity check, %d bytes claimed, %d bytes seen",
m->content_length, m->out_len);
failureval = EX_PROTOCOL;
goto failure;
}
if (flags & SPAMC_HEADERS) {
if (_append_original_body(m, flags) != EX_OK) {
goto failure;
}
}
return EX_OK;
failure:
_use_msg_for_out(m);
if (sock != -1) {
closesocket(sock);
}
libspamc_timeout = 0;
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
SSL_free(ssl);
SSL_CTX_free(ctx);
#endif
}
return failureval;
}
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;
assert(trans != NULL);
m.type = MESSAGE_NONE;
/* enforce max_size being unsigned, therefore >= 0 */
if (max_size < 0) {
ret = EX_SOFTWARE;
goto FAIL;
}
m.max_len = (unsigned int) max_size;
ret = message_read(in_fd, flags, &m);
if (ret != EX_OK)
goto FAIL;
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) {
message_cleanup(&m);
return m.is_spam;
}
message_cleanup(&m);
return ret;
FAIL:
if (flags & SPAMC_CHECK_ONLY) {
full_write(out_fd, 1, "0/0\n", 4);
message_cleanup(&m);
return EX_NOTSPAM;
}
else {
message_dump(in_fd, out_fd, &m, flags);
message_cleanup(&m);
return ret;
}
}
int message_tell(struct transport *tp, const char *username, int flags,
struct message *m, int msg_class,
unsigned int tellflags, unsigned int *didtellflags)
{
char buf[8192];
size_t bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
size_t len;
int sock = -1;
int rc;
char versbuf[20];
float version;
int response;
int failureval;
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
assert(tp != NULL);
assert(m != NULL);
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
ctx = _try_ssl_ctx_init(flags);
if (ctx == NULL) {
failureval = EX_OSERR;
goto failure;
}
#else
UNUSED_VARIABLE(ssl);
UNUSED_VARIABLE(ctx);
libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
return EX_SOFTWARE;
#endif
}
m->is_spam = EX_TOOBIG;
if (m->outbuf != NULL)
free(m->outbuf);
m->priv->alloced_size = m->max_len + EXPANSION_ALLOWANCE + 1;
if ((m->outbuf = malloc(m->priv->alloced_size)) == NULL) {
failureval = EX_OSERR;
goto failure;
}
m->out = m->outbuf;
m->out_len = 0;
/* Build spamd protocol header */
strcpy(buf, "TELL ");
len = strlen(buf);
if (len + strlen(PROTOCOL_VERSION) + 2 >= bufsiz) {
_use_msg_for_out(m);
return EX_OSERR;
}
strcat(buf, PROTOCOL_VERSION);
strcat(buf, "\r\n");
len = strlen(buf);
if (msg_class != 0) {
strcpy(buf + len, "Message-class: ");
if (msg_class == SPAMC_MESSAGE_CLASS_SPAM) {
strcat(buf + len, "spam\r\n");
}
else {
strcat(buf + len, "ham\r\n");
}
len += strlen(buf + len);
}
if ((tellflags & SPAMC_SET_LOCAL) || (tellflags & SPAMC_SET_REMOTE)) {
int needs_comma_p = 0;
strcat(buf + len, "Set: ");
if (tellflags & SPAMC_SET_LOCAL) {
strcat(buf + len, "local");
needs_comma_p = 1;
}
if (tellflags & SPAMC_SET_REMOTE) {
if (needs_comma_p == 1) {
strcat(buf + len, ",");
}
strcat(buf + len, "remote");
}
strcat(buf + len, "\r\n");
len += strlen(buf + len);
}
if ((tellflags & SPAMC_REMOVE_LOCAL) || (tellflags & SPAMC_REMOVE_REMOTE)) {
int needs_comma_p = 0;
strcat(buf + len, "Remove: ");
if (tellflags & SPAMC_REMOVE_LOCAL) {
strcat(buf + len, "local");
needs_comma_p = 1;
}
if (tellflags & SPAMC_REMOVE_REMOTE) {
if (needs_comma_p == 1) {
strcat(buf + len, ",");
}
strcat(buf + len, "remote");
}
strcat(buf + len, "\r\n");
len += strlen(buf + len);
}
if (username != NULL) {
if (strlen(username) + 8 >= (bufsiz - len)) {
_use_msg_for_out(m);
return EX_OSERR;
}
strcpy(buf + len, "User: ");
strcat(buf + len, username);
strcat(buf + len, "\r\n");
len += strlen(buf + len);
}
if ((m->msg_len > SPAMC_MAX_MESSAGE_LEN) || ((len + 27) >= (bufsiz - len))) {
_use_msg_for_out(m);
return EX_DATAERR;
}
len += sprintf(buf + len, "Content-length: %d\r\n\r\n", (int) m->msg_len);
if (m->priv->spamc_header_callback != NULL) {
char buf2[1024];
m->priv->spamc_header_callback(m, flags, buf2, 1024);
strncat(buf, buf2, bufsiz - len);
}
libspamc_timeout = m->timeout;
libspamc_connect_timeout = m->connect_timeout; /* Sep 8, 2008 mrgus: separate connect timeout */
if (tp->socketpath)
rc = _try_to_connect_unix(tp, &sock);
else
rc = _try_to_connect_tcp(tp, &sock);
if (rc != EX_OK) {
_use_msg_for_out(m);
return rc; /* use the error code try_to_connect_*() gave us. */
}
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
if (rc != EX_OK) {
failureval = rc;
goto failure;
}
#endif
}
/* Send to spamd */
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
rc = SSL_write(ssl, buf, len);
if (rc <= 0) {
libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
SSL_get_error(ssl, rc));
failureval = EX_IOERR;
goto failure;
}
rc = SSL_write(ssl, m->msg, m->msg_len);
if (rc <= 0) {
libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
SSL_get_error(ssl, rc));
failureval = EX_IOERR;
goto failure;
}
SSL_shutdown(ssl);
shutdown(sock, SHUT_WR);
#endif
}
else {
full_write(sock, 0, buf, len);
full_write(sock, 0, m->msg, m->msg_len);
shutdown(sock, SHUT_WR);
}
/* 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;
}
if (sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response) != 2) {
libspamc_log(flags, 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) {
libspamc_log(flags, LOG_ERR, "spamd responded with bad version string '%s'",
versbuf);
failureval = EX_PROTOCOL;
goto failure;
}
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, didtellflags) < 0) {
failureval = EX_PROTOCOL;
goto failure;
}
}
len = 0; /* overwrite those headers */
shutdown(sock, SHUT_RD);
closesocket(sock);
sock = -1;
libspamc_timeout = 0;
return EX_OK;
failure:
_use_msg_for_out(m);
if (sock != -1) {
closesocket(sock);
}
libspamc_timeout = 0;
if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
SSL_free(ssl);
SSL_CTX_free(ctx);
#endif
}
return failureval;
}
void message_cleanup(struct message *m)
{
assert(m != NULL);
if (m->outbuf != NULL)
free(m->outbuf);
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(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(tp, username, max_size, in_fd, out_fd, flags);
}
/*
* 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;
tp->flags = 0;
tp->retry_sleep = -1;
}
/*
* 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)
{
#ifdef SPAMC_HAS_ADDRINFO
struct addrinfo *tmp;
#else
struct in_addr tmp;
#endif
int i;
int rnum;
assert(tp != 0);
if (tp->nhosts <= 1)
return;
rnum = rand() % tp->nhosts;
while (rnum-- > 0) {
tmp = tp->hosts[0];
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)
{
#ifdef SPAMC_HAS_ADDRINFO
struct addrinfo hints, *res, *addrp;
char port[6];
int origerr;
#else
struct hostent *hp;
char **addrp;
#endif
char *hostlist, *hostname;
int errbits;
#ifdef _WIN32
/* Start Winsock up */
WSADATA wsaData;
int nCode;
if ((nCode = WSAStartup(MAKEWORD(1, 1), &wsaData)) != 0) {
printf("WSAStartup() returned error code %d\n", nCode);
return EX_OSERR;
}
#endif
assert(tp != NULL);
tp->flags = flags;
#ifdef SPAMC_HAS_ADDRINFO
snprintf(port, 6, "%d", tp->port);
memset(&hints, 0, sizeof(hints));
hints.ai_flags = 0;
hints.ai_socktype = SOCK_STREAM;
if ( (flags & SPAMC_USE_INET4) && !(flags & SPAMC_USE_INET6)) {
hints.ai_family = PF_INET;
#ifdef PF_INET6
} else if ((flags & SPAMC_USE_INET6) && !(flags & SPAMC_USE_INET4)) {
hints.ai_family = PF_INET6;
#endif
} else {
hints.ai_family = PF_UNSPEC;
}
#endif
switch (tp->type) {
#ifndef _WIN32
case TRANSPORT_UNIX:
assert(tp->socketpath != 0);
return EX_OK;
#endif
case TRANSPORT_LOCALHOST:
#ifdef SPAMC_HAS_ADDRINFO
/* getaddrinfo(NULL) will look up the loopback address.
* See also bug 5057, ::1 will be tried before 127.0.0.1
* unless overridden (through hints) by a command line option -4
*/
if ((origerr = getaddrinfo(NULL, port, &hints, &res)) != 0) {
libspamc_log(flags, LOG_ERR,
"getaddrinfo for a loopback address failed: %s",
gai_strerror(origerr));
return EX_OSERR;
}
tp->hosts[0] = res;
#else
tp->hosts[0].s_addr = inet_addr("127.0.0.1");
#endif
tp->nhosts = 1;
return EX_OK;
case TRANSPORT_TCP:
if ((hostlist = strdup(tp->hostname)) == NULL)
return EX_OSERR;
/* We want to return the least permanent error, in this bitmask we
* record the errors seen with:
* 0: no error
* 1: EX_TEMPFAIL
* 2: EX_NOHOST
* EX_OSERR will return immediately.
* Bits aren't reset so a check against nhosts is needed to determine
* if something went wrong.
*/
errbits = 0;
tp->nhosts = 0;
/* Start with char offset in front of the string because we'll add
* one in the loop
*/
hostname = hostlist - 1;
do {
char *hostend;
hostname += 1;
hostend = strchr(hostname, ',');
if (hostend != NULL) {
*hostend = '\0';
}
#ifdef SPAMC_HAS_ADDRINFO
if ((origerr = getaddrinfo(hostname, port, &hints, &res))) {
libspamc_log(flags, LOG_DEBUG,
"getaddrinfo(%s) failed: %s",
hostname, gai_strerror(origerr));
switch (origerr) {
case EAI_AGAIN:
errbits |= 1;
break;
case EAI_FAMILY: /*address family not supported*/
case EAI_SOCKTYPE: /*socket type not supported*/
case EAI_BADFLAGS: /*ai_flags is invalid*/
case EAI_NONAME: /*node or service unknown*/
case EAI_SERVICE: /*service not available*/
/* work around Cygwin IPv6 patch - err codes not defined in Windows aren't in patch */
#ifdef HAVE_EAI_ADDRFAMILY
case EAI_ADDRFAMILY: /*no addresses in requested family*/
#endif
#ifdef HAVE_EAI_SYSTEM
case EAI_SYSTEM: /*system error, check errno*/
#endif
#ifdef HAVE_EAI_NODATA
case EAI_NODATA: /*address exists, but no data*/
#endif
case EAI_MEMORY: /*out of memory*/
case EAI_FAIL: /*name server returned permanent error*/
errbits |= 2;
break;
default:
/* should not happen, all errors are checked above */
free(hostlist);
return EX_OSERR;
}
goto nexthost; /* try next host in list */
}
#else
if ((hp = gethostbyname(hostname)) == NULL) {
int origerr = h_errno; /* take a copy before syslog() */
libspamc_log(flags, LOG_DEBUG, "gethostbyname(%s) failed: h_errno=%d",
hostname, origerr);
switch (origerr) {
case TRY_AGAIN:
errbits |= 1;
break;
case HOST_NOT_FOUND:
case NO_ADDRESS:
case NO_RECOVERY:
errbits |= 2;
break;
default:
/* should not happen, all errors are checked above */
free(hostlist);
return EX_OSERR;
}
goto nexthost; /* try next host in list */
}
#endif
/* If we have no hosts at all */
#ifdef SPAMC_HAS_ADDRINFO
if(res == NULL)
#else
if (hp->h_addr_list[0] == NULL
|| hp->h_length != sizeof tp->hosts[0]
|| hp->h_addrtype != AF_INET)
/* no hosts/bad size/wrong family */
#endif
{
errbits |= 1;
goto nexthost; /* try next host in list */
}
/* 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.
*/
#ifdef SPAMC_HAS_ADDRINFO
if(tp->nhosts == TRANSPORT_MAX_HOSTS) {
libspamc_log(flags, LOG_NOTICE,
"hit limit of %d hosts, ignoring remainder",
TRANSPORT_MAX_HOSTS);
break;
}
/* treat all A or AAAA records of each host as one entry */
tp->hosts[tp->nhosts++] = res;
/* alternatively, treat multiple A or AAAA records
of one host as individual entries */
/* for (addrp = res; addrp != NULL; ) {
* tp->hosts[tp->nhosts] = addrp;
* addrp = addrp->ai_next; /-* before NULLing ai_next *-/
* tp->hosts[tp->nhosts]->ai_next = NULL;
* tp->nhosts++;
* }
*/
#else
for (addrp = hp->h_addr_list; *addrp; addrp++) {
if (tp->nhosts == TRANSPORT_MAX_HOSTS) {
libspamc_log(flags, LOG_NOTICE, "hit limit of %d hosts, ignoring remainder",
TRANSPORT_MAX_HOSTS);
break;
}
memcpy(&tp->hosts[tp->nhosts], *addrp, hp->h_length);
tp->nhosts++;
}
#endif
nexthost:
hostname = hostend;
} while (hostname != NULL);
free(hostlist);
if (tp->nhosts == 0) {
if (errbits & 1) {
libspamc_log(flags, LOG_ERR, "could not resolve any hosts (%s): a temporary error occurred",
tp->hostname);
return EX_TEMPFAIL;
}
else {
libspamc_log(flags, LOG_ERR, "could not resolve any hosts (%s): no such host",
tp->hostname);
return EX_NOHOST;
}
}
/* 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;
}
/* oops, unknown transport type */
return EX_OSERR;
}
/*
* transport_cleanup()
*
* Given a "transport" object that says how we're to connect to the
* spam daemon, delete and free any buffers allocated so that it
* can be discarded without causing a memory leak.
*/
void transport_cleanup(struct transport *tp)
{
#ifdef SPAMC_HAS_ADDRINFO
int i;
for(i=0;i<tp->nhosts;i++) {
if (tp->hosts[i] != NULL) {
freeaddrinfo(tp->hosts[i]);
tp->hosts[i] = NULL;
}
}
#endif
}
/*
* register_libspamc_log_callback()
*
* Register a callback handler for libspamc_log to replace the default behaviour.
*/
void register_libspamc_log_callback(void (*function)(int flags, int level, char *msg, va_list args)) {
libspamc_log_callback = function;
}
/*
* register_spamc_header_callback()
*
* Register a callback handler to generate spamc headers for a given message
*/
void register_spamc_header_callback(const struct message *m, void (*func)(struct message *m, int flags, char *buf, int len)) {
m->priv->spamc_header_callback = func;
}
/*
* register_spamd_header_callback()
*
* Register a callback handler to generate spamd headers for a given message
*/
void register_spamd_header_callback(const struct message *m, void (*func)(struct message *m, int flags, const char *buf, int len)) {
m->priv->spamd_header_callback = func;
}
/* --------------------------------------------------------------------------- */
#define LOG_BUFSIZ 1023
void
libspamc_log (int flags, int level, char *msg, ...)
{
va_list ap;
char buf[LOG_BUFSIZ+1];
int len = 0;
va_start(ap, msg);
if ((flags & SPAMC_LOG_TO_CALLBACK) != 0 && libspamc_log_callback != NULL) {
libspamc_log_callback(flags, level, msg, ap);
}
else if ((flags & SPAMC_LOG_TO_STDERR) != 0) {
/* create a log-line buffer */
len = snprintf(buf, LOG_BUFSIZ, "spamc: ");
len += vsnprintf(buf+len, LOG_BUFSIZ-len, msg, ap);
/* avoid buffer overflow */
if (len > (LOG_BUFSIZ-2)) { len = (LOG_BUFSIZ-3); }
len += snprintf(buf+len, LOG_BUFSIZ-len, "\n");
buf[LOG_BUFSIZ] = '\0'; /* ensure termination */
(void) write (2, buf, len);
} else {
vsnprintf(buf, LOG_BUFSIZ, msg, ap);
buf[LOG_BUFSIZ] = '\0'; /* ensure termination */
#ifndef _WIN32
syslog (level, "%s", buf);
#else
(void) level; /* not used. suppress compiler warning */
fprintf (stderr, "%s\n", buf);
#endif
}
va_end(ap);
}
/* --------------------------------------------------------------------------- */
/*
* 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;
/* sprintf instead of snprintf is safe here because it is only a controlled test */
sprintf(inputstr, "%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 */
sprintf(cmpbuf1, "%f", input);
sprintf(cmpbuf2, "%f", output);
if (!strcmp(cmpbuf1, cmpbuf2)) {
return;
}
printf("FAIL: input=%f != output=%f\n", input, output);
}
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");
}
void do_libspamc_unit_tests(void)
{
unit_test_locale_safe_string_to_float();
exit(0);
}
#endif /* LIBSPAMC_UNIT_TESTS */