/*
* Copyright (c) 2007-2008 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 die_flag = 0;
static int
CreateDefaultDir(const char *saPath)
{
if (mkdir(saPath, 0700) == -1) {
SF_SetError("Failed to create %s: %s", saPath, strerror(errno));
return (-1);
}
if (chdir(saPath) == -1) {
SF_SetError("chdir %s: %s", saPath, strerror(errno));
return (-1);
}
return (0);
}
static void
ProcessSetupOK(int fdParent)
{
const char code = '0';
write_ok:
if (write(fdParent, &code, 1) == -1) {
if (errno == EINTR) {
goto write_ok;
}
syslog(LOG_ERR, "write(0) to parentfd: %s", strerror(errno));
exit(1);
}
}
static void
ProcessSetupFailed(int fdParent)
{
const char code = '1';
syslog(LOG_ERR, "spamcheck(%ld): setup failed: %s\n", (long)getpid(),
SF_GetError());
write_fail:
if (write(fdParent, &code, 1) == -1) {
if (errno == EINTR) {
goto write_fail;
}
fprintf(stderr, "write(1) to parentfd: %s", strerror(errno));
exit(1);
}
}
static void
sig_die(int sigraised)
{
die_flag = 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) {
SF_SetError("Suspended filtering");
return (1);
}
return (0);
}
/* Main routine for spamcheck processes. */
static int
CreateSpamCheckProcess(SF_Message *msg, const char *sockPath, struct passwd *pw)
{
extern char **environ;
char saConfigPath[1024];
char saPath[1024];
char code;
int pp[2];
char wbuf[32];
char *cleanenv[1];
struct sigaction sa;
sigset_t allsigs;
struct sockaddr_un unaddr;
my_socklen_t socklen;
int servSock;
pid_t pid;
unsigned int processedMsgs = 0;
fd_set servfds;
if (SuspendedFiltering(pw)) {
return (-1);
}
if (pipe(pp) == -1) {
SF_SetError("pipe: %s", strerror(errno));
return (-1);
}
if ((pid = fork()) == -1) {
SF_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 SF_EnterServerProc().
*/
close(pp[1]);
/* Block until child is ready to process requests. */
if (Read(pp[0], &code, 1) == -1) {
SF_SetError("Failed to read status from spamcheck");
return (-1);
}
if (code != '0') {
SF_SetError("spamcheck process returned error status");
return (-1);
}
close(pp[0]);
return (0);
}
/* Set up the spamcheck process. */
Setproctitle("spamcheck");
openlog("spamcheck", LOG_PID, LOG_LOCAL0);
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sig_die;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
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) {
SF_SetError("socket(AF_UNIX): %s", strerror(errno));
ProcessSetupFailed(pp[1]);
exit(1);
}
unaddr.sun_family = AF_UNIX;
Strlcpy(unaddr.sun_path, sockPath, sizeof(unaddr.sun_path));
socklen = SUN_LEN(&unaddr);
if (bind(servSock, (struct sockaddr *)&unaddr, socklen) == -1 ||
listen(servSock, saSocketBacklog) == -1) {
#if 1
if (errno == EADDRINUSE) {
Debug("Socket exists, returning OK code");
ProcessSetupOK(pp[1]);
close(pp[1]);
close(servSock);
exit(0);
}
#endif
SF_SetError("%s: %s", sockPath, strerror(errno));
ProcessSetupFailed(pp[1]);
close(servSock);
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, saUserDir, sizeof(saPath));
Strlcat(saPath, pw->pw_name, sizeof(saPath));
if (chdir(saPath) == -1) {
if (CreateDefaultDir(saPath) == -1) {
ProcessSetupFailed(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);
SF_SetError("setuid(%d:%d -> %d,%d): %s",
geteuid(), getegid(), pw->pw_uid, pw->pw_gid,
strerror(errno));
ProcessSetupFailed(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) {
if (CreateDefaultDir(saConfigPath) == -1) {
ProcessSetupFailed(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) {
SF_SetError("SA_InitLearner");
ProcessSetupFailed(pp[1]);
exit(1);
}
/* Free the message structure inherited from the parent. */
SF_MessageFree(msg);
/*
* Send an OK message back to the parent, close the pipe and
* start listening for requests on the socket.
*/
ProcessSetupOK(pp[1]);
close(pp[1]);
processedMsgs = 0;
while (!die_flag) {
fd_set rfds;
struct sockaddr_un paddr;
struct timeval tv;
size_t len;
int rv, sock;
char *ep;
Setproctitle("-spamcheck: %u msgs", processedMsgs++);
if (saMaxProcMsgs != 0 && (processedMsgs > saMaxProcMsgs)) {
Debug("Processed %d messages, exiting", processedMsgs);
goto out;
}
socklen = sizeof(paddr);
tryaccept:
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);
goto out;
} else if (rv == -1) {
if (errno == EINTR) {
if (die_flag) { goto out; }
else { goto tryaccept; }
} else {
SF_SetError("select: %s", strerror(errno));
goto fail;
}
}
sock = accept(servSock, (struct sockaddr *)&paddr, &socklen);
if (sock == -1) {
if (errno == EINTR) {
if (die_flag) { goto out; }
else { goto tryaccept; }
} else {
SF_SetError("accept: %s", strerror(errno));
goto fail;
}
}
/* Read new message from the socket. */
msg = SF_MessageNew();
if (Read(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Length: %s", SF_GetError());
goto fail_msg;
}
errno = 0;
len = (size_t)strtoul(wbuf, &ep, 10);
if (*ep != '\0' || errno == ERANGE) {
SF_SetError("Bogus length: %s", wbuf);
goto fail_msg;
}
msg->text_len = len;
if ((msg->text = realloc(msg->text, len+1)) == NULL) {
SF_SetError("Out of memory for rewritten message (%lu)",
(unsigned long)msg->text_len);
msg->text_len = 0;
goto fail_msg;
}
if (Read(sock, msg->text, len) == -1) {
SF_SetError("Message text: %s", SF_GetError());
goto fail_msg;
}
msg->text[len] = '\0';
/* Perform the SpamAssassin test and rewrite the message. */
Debug("Analyzing message (%d bytes)", (int)len);
if (SA_ParseMessage(msg) == -1) {
SF_SetError("SA_ParseMessage");
goto fail_msg;
}
if (SA_CheckMessage(msg) == -1) {
SF_SetError("SA_CheckMessage");
goto fail_msg;
}
if (SA_RewriteMessage(msg) == -1) {
SF_SetError("SA_RewriteMessage");
goto fail_msg;
}
/*
* Write the test results and rewritten message back to
* the client.
*/
snprintf(wbuf, sizeof(wbuf), "%f", msg->status.score);
if (Write(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Score: %s", SF_GetError());
goto fail_msg;
}
snprintf(wbuf, sizeof(wbuf), "%f", msg->status.req_score);
if (Write(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("ReqScore: %s", SF_GetError());
goto fail_msg;
}
snprintf(wbuf, sizeof(wbuf), "%lu",
(unsigned long)msg->text_len);
if (Write(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Length: %s", SF_GetError());
goto fail_msg;
}
if (Write(sock, msg->text, msg->text_len) == -1) {
SF_SetError("Message text: %s", SF_GetError());
goto fail_msg;
}
if (saLearning) {
SA_LearnMessage(msg);
}
SF_MessageFree(msg);
close(sock);
}
out:
Unlink(sockPath);
if (saLearning) {
SA_RebuildLearnerCaches();
SA_FinishLearner();
}
SA_FinishAddrListFactory();
SA_Finish();
close(servSock);
closelog();
exit(0);
fail_msg:
SF_MessageFree(msg);
fail:
syslog(LOG_ERR, "spamcheck failed: %s", SF_GetError());
Unlink(sockPath);
if (saLearning) {
SA_RebuildLearnerCaches();
SA_FinishLearner();
}
SA_FinishAddrListFactory();
SA_Finish();
close(servSock);
closelog();
exit(1);
}
/* Test a message for spam. */
int
SPAM_Check(SF_Message *msg, SF_Recipient *rcpt)
{
char sockPath[1024];
char wbuf[32];
int sock;
uid_t uid;
gid_t gid;
char *ep;
size_t len;
struct passwd *pw;
struct sockaddr_un unaddr;
my_socklen_t socklen;
int retry;
if (saMaxSize > 0 && msg->text_len >= saMaxSize) {
msg->status.score = 0.0;
msg->status.req_score = 10.0;
Debug("Message over limit for spam test (%lu bytes)",
(unsigned long)msg->text_len);
return (0);
}
/* Find the UID/GID corresponding to the recipient address. */
if (LOCAL_GetDefaultRecipientUID(rcpt->addr, &uid, &gid) == -1) {
SF_SetError("Cannot get default UID for %s", rcpt->addr);
return (-1);
}
if ((pw = getpwuid(uid)) == NULL) {
SF_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) {
SF_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) {
SF_SetError("mkdir %s (as %d:%d): %s", sockPath,
geteuid(), getegid(), strerror(errno));
goto fail;
}
if (chown(sockPath, uid, gid) == -1) {
SF_SetError("chown %s: %s", sockPath, strerror(errno));
goto fail;
}
Strlcat(sockPath, "/sock", sizeof(sockPath));
unaddr.sun_family = AF_UNIX;
Strlcpy(unaddr.sun_path, sockPath, sizeof(unaddr.sun_path));
socklen = SUN_LEN(&unaddr);
retry = 0;
tryconn:
if (connect(sock, (struct sockaddr *)&unaddr, socklen) == -1) {
if (retry++ == 0) {
if (CreateSpamCheckProcess(msg, sockPath, pw) == -1) {
goto fail;
}
sleep(1);
goto tryconn;
} else {
/* This should not happen. */
if (retry > 10) {
SF_SetError("Timeout waiting on spamcheck!");
goto fail;
}
Debug("Waiting (%d/10) for process setup", retry);
sleep(1);
}
}
/* Write the message to the spamcheck socket. */
snprintf(wbuf, sizeof(wbuf), "%lu", (unsigned long)msg->text_len);
if (Write(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Length: %s", SF_GetError());
if (errno == ENOTCONN) {
unlink(sockPath);
retry = 0;
goto tryconn;
}
goto fail;
}
if (Write(sock, msg->text, msg->text_len) == -1) {
SF_SetError("Message text: %s", SF_GetError());
goto fail;
}
/* TODO: Timeout... */
/* Read back the result and required scores. */
if (Read(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Score: %s", SF_GetError());
goto fail;
}
errno = 0;
msg->status.score = (float)strtod(wbuf, &ep);
if (*ep != '\0' || errno == ERANGE) {
SF_SetError("Bogus score: %s", wbuf);
goto fail;
}
if (Read(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Required score: %s", SF_GetError());
goto fail;
}
errno = 0;
msg->status.req_score = (float)strtod(wbuf, &ep);
if (*ep != '\0' || errno == ERANGE) {
SF_SetError("Bogus required score: %s", wbuf);
goto fail;
}
/* Read the rewritten message. */
if (Read(sock, wbuf, sizeof(wbuf)) == -1) {
SF_SetError("Length: %s", SF_GetError());
goto fail;
}
errno = 0;
len = (size_t)strtoul(wbuf, &ep, 10);
if (*ep != '\0' || errno == ERANGE) {
SF_SetError("Bogus length: %s", wbuf);
goto fail;
}
msg->text_len = len;
if ((msg->text = realloc(msg->text, len+1)) == NULL) {
SF_SetError("Out of memory for rewritten message (%luB)",
(unsigned long)msg->text_len);
msg->text_len = 0;
goto fail;
}
if (Read(sock, msg->text, len) == -1) {
SF_SetError("Rewritten text: %s", SF_GetError());
goto fail;
}
msg->text[len] = '\0';
Debug("Score = %f/%f (%luB message)",
msg->status.score, msg->status.req_score,
(unsigned long)msg->text_len);
close(sock);
return (0);
fail:
close(sock);
return (-1);
}
void
SPAM_Destroy(void)
{
char path[1024];
DIR *dir;
struct dirent *dent;
/* 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 */