/*
* Copyright (c) 2007-2017 Hypertriton, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef HAVE_SA
#include
#include
#include
#include "mailprocd.h"
#include
#include
#include
#include
#include
#include
#include
#include "pathnames.h"
static volatile int SigDIE = 0;
static int
CreateDefaultDir(const char *saPath)
{
if (mkdir(saPath, 0700) == -1) {
MPD_SetError("Failed to create %s: %s", saPath, strerror(errno));
return (-1);
}
if (chdir(saPath) == -1) {
MPD_SetError("chdir %s: %s", saPath, strerror(errno));
return (-1);
}
return (0);
}
static int
SPAM_CheckSignals(void)
{
if (SigDIE) {
MPD_SetErrorS("Spamcheck exit (signal)");
return (1);
}
return (0);
}
static __inline__ int
SPAM_Read(int fd, void *data, size_t len)
{
size_t nread;
ssize_t rv;
for (nread = 0; nread < len; ) {
rv = read(fd, data+nread, len-nread);
if (rv == -1) {
if (errno == EINTR || errno == EAGAIN) {
if (SPAM_CheckSignals()) {
return (-1);
}
continue;
} else {
MPD_SetError("Read error: %s", strerror(errno));
return (-1);
}
} else if (rv == 0) {
MPD_SetErrorS("EOF");
return (-1);
}
nread += rv;
}
return (0);
}
static __inline__ int
SPAM_Write(int fd, const void *data, size_t len)
{
size_t nwrote;
ssize_t rv;
for (nwrote = 0; nwrote < len; ) {
rv = write(fd, data+nwrote, len-nwrote);
if (rv == -1) {
if (errno == EINTR || errno == EAGAIN) {
if (SPAM_CheckSignals()) {
return (-1);
}
continue;
} else {
MPD_SetError("Write error: %s", strerror(errno));
return (-1);
}
} else if (rv == 0) {
MPD_SetErrorS("EOF");
return (-1);
}
nwrote += rv;
}
return (0);
}
/* Write success code back to parent. */
static void
SPAM_WriteOK(int fd)
{
const char code = '0';
if (SPAM_Write(fd, &code, 1) == -1) {
syslog(LOG_ERR, "SPAM_WriteOK: %s", MPD_GetError());
exit(1);
}
}
static void
SPAM_WriteFAIL(int fd)
{
const char code = '1';
syslog(LOG_ERR, "SPAMCHECK: %s", MPD_GetError());
if (SPAM_Write(fd, &code, 1) == -1) {
syslog(LOG_ERR, "SPAM_WriteOK: %s", MPD_GetError());
}
close(fd);
}
static void
SigTERM(int sigraised)
{
exit(1);
}
static int
SuspendedFiltering(const struct passwd *pw)
{
char path[FILENAME_MAX];
struct stat sb;
Strlcpy(path, _PATH_SUSP_INFO, sizeof(path));
Strlcat(path, pw->pw_name, sizeof(path));
Strlcat(path, ".spamcheck", sizeof(path));
if (stat(path, &sb) == 0) {
MPD_SetErrorS("Suspended filtering");
return (1);
}
return (0);
}
/*
* Spawn a spamcheck process and wait for its initialization to complete.
* Invoked by SPAM_Check() in QMGR Worker context.
*/
static int
SPAM_SpawnWorker(MPD_Message *msg, const char *sockPath, struct passwd *pw)
{
extern char **environ;
char saConfigPath[FILENAME_MAX];
char saPath[FILENAME_MAX];
char code;
int pp[2];
char *cleanenv[1];
struct sigaction sa;
sigset_t allsigs;
struct sockaddr_un sun;
my_socklen_t socklen;
int servSock;
pid_t pid;
Uint processedMsgs = 0;
fd_set servfds;
if (SuspendedFiltering(pw)) {
return (-1);
}
if (pipe(pp) == -1) {
MPD_SetError("pipe: %s", strerror(errno));
return (-1);
}
if ((pid = fork()) == -1) {
MPD_SetError("fork: %s", strerror(errno));
return (-1);
}
if (pid > 0) { /* Parent */
/*
* NOTE: We are forked off of a worker process, so there
* is no need to use MPD_EnterServerProc().
*/
close(pp[1]);
/* Block until child is ready to process requests. */
if (QMGR_Read(pp[0], &code, 1) == -1) {
return (-1);
}
if (code != '0') {
MPD_SetError("Spamcheck: Bad code %d", code);
return (-1);
}
close(pp[0]);
return (0);
}
/* Set up the spamcheck process. */
Setproctitle("spamcheck");
openlog("spamcheck", LOG_PID, LOG_LOCAL0);
sigemptyset(&sa.sa_mask);
sa.sa_handler = SIG_DFL;
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
sa.sa_handler = SIG_IGN;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);
sigfillset(&sa.sa_mask);
sa.sa_handler = SigTERM;
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
close(pp[0]);
close(STDIN_FILENO);
/* Create our Unix socket. */
if ((servSock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
MPD_SetError("socket(AF_UNIX): %s", strerror(errno));
SPAM_WriteFAIL(pp[1]);
exit(1);
}
sun.sun_family = AF_UNIX;
Strlcpy(sun.sun_path, sockPath, sizeof(sun.sun_path));
socklen = SUN_LEN(&sun);
if (bind(servSock, (struct sockaddr *)&sun, socklen) == -1 ||
listen(servSock, saSocketBacklog) == -1) {
#if 1
if (errno == EADDRINUSE) {
Debug("Socket exists, returning OK code");
SPAM_WriteOK(pp[1]);
close(pp[1]);
close(servSock);
exit(0);
}
#endif
MPD_SetError("%s: %s", sockPath, strerror(errno));
close(servSock);
SPAM_WriteFAIL(pp[1]);
exit(1);
}
chown(sockPath, pw->pw_uid, pw->pw_gid);
chmod(sockPath, 0700);
FD_ZERO(&servfds);
FD_SET(servSock, &servfds);
/* Create the SpamAssassin database directory if needed */
Strlcpy(saPath, pw->pw_dir, sizeof(saPath));
Strlcat(saPath, _PATH_SACONF, sizeof(saPath));
if (chdir(saPath) == -1 &&
CreateDefaultDir(saPath) == -1) {
SPAM_WriteFAIL(pp[1]);
exit(1);
}
chown(saPath, pw->pw_uid, pw->pw_gid);
chmod(saPath, 0700);
/* Clear the environment and drop privileges. */
sigfillset(&allsigs);
sigprocmask(SIG_BLOCK, &allsigs, NULL);
environ = cleanenv;
cleanenv[0] = NULL;
setenv("USER", pw->pw_name, 1);
setenv("LOGNAME", pw->pw_name, 1);
setenv("HOME", pw->pw_dir, 1);
setenv("SHELL", mpdSafeShell, 1);
setenv("PATH", mpdSafePath, 1);
if (setegid(pw->pw_gid) < 0 || setgid(pw->pw_gid) < 0 ||
seteuid(pw->pw_uid) < 0 || setuid(pw->pw_uid) < 0) {
sigprocmask(SIG_UNBLOCK, &allsigs, NULL);
MPD_SetError("setuid(%d:%d -> %d,%d): %s",
geteuid(), getegid(), pw->pw_uid, pw->pw_gid,
strerror(errno));
SPAM_WriteFAIL(pp[1]);
exit(1);
}
sigprocmask(SIG_UNBLOCK, &allsigs, NULL);
/* Create ~/.spamassassin if it does not exist yet. */
Strlcpy(saConfigPath, pw->pw_dir, sizeof(saConfigPath));
Strlcat(saConfigPath, saUserConfDir, sizeof(saConfigPath));
if (chdir(saConfigPath) == -1 &&
CreateDefaultDir(saConfigPath) == -1) {
SPAM_WriteFAIL(pp[1]);
exit(1);
}
Strlcat(saConfigPath, "/", sizeof(saConfigPath));
Strlcat(saConfigPath, saUserPrefs, sizeof(saConfigPath));
/* Set up SpamAssassin for this user. */
SA_ReadScoreOnlyConfig(saConfigPath);
SA_SignalUserChange(pw->pw_name, pw->pw_dir, saPath);
if (saLearning &&
SA_InitLearner() == -1) {
MPD_SetErrorS("SA_InitLearner");
SPAM_WriteFAIL(pp[1]);
exit(1);
}
/* Free the message structure inherited from the parent. */
MPD_MessageFree(msg);
/*
* Send an OK message back to the parent, close the pipe and
* start listening for requests on the socket.
*/
SPAM_WriteOK(pp[1]);
close(pp[1]);
processedMsgs = 0;
for (;;) {
char wrScore[32], wrReqScore[32], wrTextLen[32];
struct sockaddr_un paddr;
struct iovec vec[4];
struct timeval tv;
int rv, sock;
fd_set rfds;
char *ep;
Setproctitle("spamcheck (%u msgs)", processedMsgs++);
if (saMaxProcMsgs != 0 && (processedMsgs > saMaxProcMsgs)) {
Debug("Processed %d messages, exiting", processedMsgs);
break;
}
socklen = sizeof(paddr);
rfds = servfds;
tv.tv_sec = saMaxIdle;
tv.tv_usec = 0;
rv = select(servSock+1, &rfds, NULL, NULL, &tv);
if (rv == 0) {
/* Debug("Idle for >%d seconds, exiting", saMaxIdle); */
break;
} else if (rv == -1) {
if (errno == EINTR) {
if (SPAM_CheckSignals()) {
break;
}
continue;
} else {
MPD_SetError("select: %s", strerror(errno));
goto fail;
}
}
try_accept:
sock = accept(servSock, (struct sockaddr *)&paddr, &socklen);
if (sock == -1) {
if (errno == EINTR || errno == EAGAIN) {
if (SPAM_CheckSignals()) {
break;
}
goto try_accept;
} else {
MPD_SetError("accept: %s", strerror(errno));
goto fail;
}
}
/* Read new message from the socket. */
if ((msg = MPD_MessageNew()) == NULL) {
goto fail_msg;
}
if (SPAM_Read(sock, wrTextLen, sizeof(wrTextLen)) == -1) {
goto fail_msg;
}
errno = 0;
msg->text_len = (size_t)strtoul(wrTextLen, &ep, 10);
if (*ep != '\0' || errno == ERANGE ||
msg->text_len > MESSAGE_SIZE_MAX) {
MPD_SetError("Bad textLen: %s", wrTextLen);
goto fail_msg;
}
if ((msg->text = malloc(msg->text_len+1)) == NULL) {
MPD_SetErrorS("Out of memory");
goto fail_msg;
}
if (SPAM_Read(sock, msg->text, msg->text_len) == -1) {
goto fail_msg;
}
msg->text[msg->text_len] = '\0';
/* Perform the SpamAssassin test and rewrite the message. */
/* Debug("Analyzing message (%luK)", msg->text_len/1024); */
if (SA_ParseMessage(msg) == -1) {
MPD_SetErrorS("SA_ParseMessage");
goto fail_msg;
}
if (SA_CheckMessage(msg) == -1) {
MPD_SetErrorS("SA_CheckMessage");
goto fail_msg;
}
if (SA_RewriteMessage(msg) == -1) {
MPD_SetErrorS("SA_RewriteMessage");
goto fail_msg;
}
/*
* Write back results as well as the transformed message
* contents.
*/
snprintf(wrScore, sizeof(wrScore), "%f", msg->status.score);
snprintf(wrReqScore, sizeof(wrReqScore), "%f", msg->status.req_score);
snprintf(wrTextLen, sizeof(wrTextLen), "%lu", msg->text_len);
vec[0].iov_base = wrScore; vec[0].iov_len = sizeof(wrScore);
vec[1].iov_base = wrReqScore; vec[1].iov_len = sizeof(wrReqScore);
vec[2].iov_base = wrTextLen; vec[2].iov_len = sizeof(wrTextLen);
vec[3].iov_base = msg->text; vec[3].iov_len = msg->text_len;
try_writev:
if (writev(sock, vec, 4) == -1) {
if (errno == EINTR || errno == EAGAIN) {
if (SPAM_CheckSignals()) {
break;
}
goto try_writev;
}
MPD_SetError("Results writev: %s", strerror(errno));
goto fail_msg;
}
if (saLearning) {
SA_LearnMessage(msg);
}
MPD_MessageFree(msg);
close(sock);
if (SPAM_CheckSignals())
break;
}
if (SigDIE) {
syslog(LOG_WARNING, "spamcheck exited (signal)");
}
Unlink(sockPath);
if (saLearning) {
SA_RebuildLearnerCaches();
SA_FinishLearner();
}
SA_FinishAddrListFactory();
SA_Finish();
close(servSock);
closelog();
exit(0);
fail_msg:
MPD_MessageFree(msg);
fail:
syslog(LOG_ERR, "spamcheck failed: %s", MPD_GetError());
Unlink(sockPath);
if (saLearning) {
SA_RebuildLearnerCaches();
SA_FinishLearner();
}
SA_FinishAddrListFactory();
SA_Finish();
close(servSock);
closelog();
exit(1);
}
/* Test a message for spam. Invoke from QMGR Worker process. */
int
SPAM_Check(MPD_Message *msg, MPD_Recipient *rcpt)
{
char rdScore[32], rdReqScore[32], rdTextLen[32], *ep;
char sockPath[FILENAME_MAX];
struct iovec vec[4];
uid_t uid;
gid_t gid;
struct passwd *pw;
struct sockaddr_un sun;
my_socklen_t socklen;
int retry, sock;
if (saMaxSize > 0 && msg->text_len >= saMaxSize) {
msg->status.score = 0.0;
msg->status.req_score = 10.0;
Debug("Ignoring %luK message (over %luK)", msg->text_len/1024,
saMaxSize/1024);
return (0);
}
/* Find the UID/GID corresponding to the recipient address. */
if (LOCAL_GetDefaultRecipientUID(rcpt->addr, &uid, &gid) == -1) {
return (-1);
}
if ((pw = getpwuid(uid)) == NULL) {
MPD_SetError("Bad UID for %s (%ld)", rcpt->addr, (long)uid);
return (-1);
}
/*
* Create a spamcheck process if this user does not already have
* one running and set up the communication channel.
*/
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
MPD_SetError("socket(AF_UNIX): %s", strerror(errno));
return (-1);
}
Strlcpy(sockPath, mpdSocketDir, sizeof(sockPath));
Strlcat(sockPath, pw->pw_name, sizeof(sockPath));
if (mkdir(sockPath, 0700) == -1 && errno != EEXIST) {
MPD_SetError("mkdir %s (as %d:%d): %s", sockPath,
geteuid(), getegid(), strerror(errno));
goto fail;
}
if (chown(sockPath, uid, gid) == -1) {
MPD_SetError("chown %s: %s", sockPath, strerror(errno));
goto fail;
}
Strlcat(sockPath, "/sock", sizeof(sockPath));
sun.sun_family = AF_UNIX;
Strlcpy(sun.sun_path, sockPath, sizeof(sun.sun_path));
socklen = SUN_LEN(&sun);
retry = 0;
try_connect:
if (connect(sock, (struct sockaddr *)&sun, socklen) == -1) {
if (errno == EINTR || errno == EAGAIN) {
if (QMGR_CheckSignals()) {
goto fail;
}
goto try_connect;
} else if (errno == ECONNREFUSED || errno == ENOENT) {
if (retry++ == 0) {
if (SPAM_SpawnWorker(msg, sockPath, pw) == -1) {
goto fail;
}
goto try_connect;
} else { /* should not happen */
if (retry > 10) {
MPD_SetErrorS("SPAMCHECK setup timeout");
goto fail;
}
sleep(1);
}
} else {
MPD_SetError("spamcheck connect: %s", strerror(errno));
goto fail;
}
}
/*
* Write message length and contents.
*/
snprintf(rdTextLen, sizeof(rdTextLen), "%lu", msg->text_len);
vec[0].iov_base = rdTextLen;
vec[0].iov_len = sizeof(rdTextLen);
vec[1].iov_base = msg->text;
vec[1].iov_len = msg->text_len;
if (QMGR_Writev(sock, vec, 2) == -1) {
if (errno == ENOTCONN) { /* Stale socket? */
unlink(sockPath);
retry = 0;
goto try_connect;
} else {
goto fail;
}
}
/*
* Read back the score, threshold, message length and data.
*/
vec[0].iov_base = rdScore;
vec[0].iov_len = sizeof(rdScore);
vec[1].iov_base = rdReqScore;
vec[1].iov_len = sizeof(rdReqScore);
vec[2].iov_base = rdTextLen;
vec[2].iov_len = sizeof(rdTextLen);
if (QMGR_Readv(sock, vec, 3) == -1)
goto fail;
/* Parse score; required score; length. */
errno = 0;
msg->status.score = (float)strtod(rdScore, &ep);
if (*ep != '\0' || errno == ERANGE) {
MPD_SetErrorS("Bad score");
goto fail;
}
errno = 0;
msg->status.req_score = (float)strtod(rdReqScore, &ep);
if (*ep != '\0' || errno == ERANGE) {
MPD_SetErrorS("Bad reqScore");
goto fail;
}
errno = 0;
msg->text_len = (size_t)strtoul(rdTextLen, &ep, 10);
if (*ep != '\0' || errno == ERANGE || msg->text_len > MESSAGE_SIZE_MAX) {
MPD_SetErrorS("Bad textLen");
goto fail;
}
/* Message data */
if ((msg->text = realloc(msg->text, msg->text_len+1)) == NULL) {
MPD_SetErrorS("Out of memory");
goto fail;
}
if (QMGR_Read(sock, msg->text, msg->text_len) == -1) {
MPD_SetError("Message read: %s", MPD_GetError());
goto fail;
}
msg->text[msg->text_len] = '\0';
Debug("Score = %f/%f (%luK message)", msg->status.score,
msg->status.req_score, msg->text_len/1024);
close(sock);
return (0);
fail:
close(sock);
return (-1);
}
void
SPAM_Destroy(void)
{
char path[FILENAME_MAX];
struct dirent *dent;
DIR *dir;
/* Unlink any socket that may have been left over. */
if ((dir = opendir(mpdSocketDir)) != NULL) {
while ((dent = readdir(dir)) != NULL) {
if (dent->d_name[0] == '.') {
continue;
}
Strlcpy(path, mpdSocketDir, sizeof(path));
Strlcat(path, dent->d_name, sizeof(path));
Strlcat(path, "/sock", sizeof(path));
unlink(path);
Strlcpy(path, mpdSocketDir, sizeof(path));
Strlcat(path, dent->d_name, sizeof(path));
rmdir(path);
}
closedir(dir);
} else {
syslog(LOG_ERR, "%s: %s", mpdSocketDir, strerror(errno));
}
}
#endif /* HAVE_SA */