/* * 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 */