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