/* * QScan virus filter for Qmail/Sophos antivirus * Jedi/Sector One */ #include #include "qscan.h" #include "qscan_p.h" #ifdef WITH_DMALLOC # include #endif static void write_log(const int priority, const char * const format, ...) { va_list args; va_start(args, format); openlog("qscan", LOG_NDELAY | LOG_PID, LOG_MAIL); vsyslog(priority, format, args); closelog(); va_end(args); } static void perror_log(const char * const msg) { write_log(LOG_ERR, "%s : %s", msg, strerror(errno)); } static int spawn_ripmime(void) { if (pipe(ripmime_pipe) != 0) { perror_log("pipe"); return -1; } if ((ripmime_pid = fork()) == (pid_t) -1) { perror_log("fork"); (void) close(ripmime_pipe[0]); (void) close(ripmime_pipe[1]); return -1; } if (ripmime_pid == (pid_t) 0) { if (close(ripmime_pipe[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } if (ripmime_pipe[0] != 0) { if (dup2(ripmime_pipe[0], 0) == -1) { perror_log("dup2"); exit(EXIT_FAILURE); } if (close(ripmime_pipe[0]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } } (void) execlp(RIPMIME, RIPMIME, RIPMIME_ARGS, ripmime_destdir, (char *) NULL); perror_log("execlp"); exit(EXIT_FAILURE); } if (close(ripmime_pipe[0]) != 0) { perror_log("close"); return -1; } return 0; } static int spawn_qq(void) { if (ripmime_pid <= (pid_t) 0) { perror_log("need ripmime before qq"); return -1; } if (pipe(qq_pipe) != 0) { perror_log("pipe"); return -1; } if (pipe(qq_pipe_envelope) != 0) { perror_log("pipe"); return -1; } if ((qq_pid = fork()) == (pid_t) -1) { perror_log("fork"); (void) close(qq_pipe[0]); (void) close(qq_pipe[1]); (void) close(qq_pipe_envelope[0]); (void) close(qq_pipe_envelope[1]); return -1; } if (qq_pid == (pid_t) 0) { if (close(ripmime_pipe[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } if (close(qq_pipe[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } if (qq_pipe[0] != 0) { if (dup2(qq_pipe[0], 0) == -1) { perror_log("dup2"); exit(EXIT_FAILURE); } if (close(qq_pipe[0]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } } if (close(qq_pipe_envelope[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } if (qq_pipe_envelope[0] != 1) { if (dup2(qq_pipe_envelope[0], 1) == -1) { perror_log("dup2"); exit(EXIT_FAILURE); } (void) close(qq_pipe_envelope[1]); } (void) execlp(QMAIL_QUEUE, QMAIL_QUEUE, (char *) NULL); perror_log("execlp"); exit(EXIT_FAILURE); } if (close(qq_pipe[0]) != 0) { perror_log("close"); return -1; } return 0; } static int find_concurrency(void) { DIR *dir; const struct dirent *entry; int concurrency = 0; if ((dir = opendir(RIPMIME_DESTDIR_BASE)) == NULL) { perror_log("unable to opendir() the ripmime base directory"); return -1; } while ((entry = readdir(dir)) != NULL) { if (entry->d_name == NULL || *(entry->d_name) == '.') { continue; } concurrency++; if (concurrency >= INT_MAX) { break; } } if (closedir(dir) != 0) { perror_log("unable to closedir() the ripmime base directory"); return -1; } return concurrency; } static int cleanup_destdir(void) { DIR *dir; const struct dirent *entry; const char *found; int ret = 0; if (chdir(ripmime_destdir) != 0) { perror_log("unable to chdir to ripmime_destdir"); return -1; } if ((dir = opendir(".")) == NULL) { perror_log("unable to opendir() ripmime_destdir"); return -1; } while ((entry = readdir(dir)) != NULL) { if ((found = entry->d_name) == NULL || (found[0] == '.' && (found[1] == 0 || (found[1] == '.' && found[2] == 0)))) { continue; } if (unlink(found) != 0) { ret = -1; } } if (closedir(dir) != 0) { perror_log("unable to closedir() ripmime_destdir"); return -1; } return ret; } static int has_attachments(void) { DIR *dir; const struct dirent *entry; const char *found; struct stat st; int ret = 0; if (chdir(ripmime_destdir) != 0) { perror_log("unable to chdir() to ripmime_destdir"); return -1; } if ((dir = opendir(".")) == NULL) { perror_log("unable to opendir() ripmime_destdir"); return -1; } while ((entry = readdir(dir)) != NULL) { if ((found = entry->d_name) == NULL || (found[0] == '.' && (found[1] == 0 || (found[1] == '.' && found[2] == 0)))) { continue; } if (lstat(found, &st) != 0) { write_log(LOG_ERR, "unable to lstat [%s]", found); ret = -1; } else if (!S_ISREG(st.st_mode) || (st.st_mode & S_IRUSR) == (mode_t) 0) { write_log(LOG_ERR, "bad file [%s] to scan", found); if (unlink(found) != 0) { perror_log("unable to unlink a bad file"); } ret = -1; } else if (ret >= 0 && ret < INT_MAX) { ret++; } } if (closedir(dir) != 0) { perror_log("unable to closedir() ripmime_destdir"); return -1; } return ret; } static void wait_everybody_and_die(const int ret) { int status; pid_t pid; if (*ripmime_destdir != 0) { if (cleanup_destdir() != 0) { perror_log("error while cleaning up destdir before dying"); } if (rmdir(ripmime_destdir) != 0) { perror_log("error while removing destdir before dying"); } } if (ripmime_pipe[1] > 1) { (void) close(ripmime_pipe[1]); } if (qq_pipe[1] > 1) { (void) close(qq_pipe[1]); } if (qq_pipe_envelope[1] > 1) { (void) close(qq_pipe_envelope[1]); } if (av_pipe[0] > 1) { (void) close(av_pipe[0]); } (void) close(0); (void) close(1); for(;;) { if ((pid = wait(&status)) == (pid_t) -1 && errno != EINTR) { exit(ret); } } } static int spawn_antivirus(char ** const foundvirus) { char line[AV_LINE_MAX]; FILE *avfp; int status; pid_t pid; int ret = 0; if (foundvirus == NULL) { perror_log("foundvirus is a NULL pointer"); return -1; } *foundvirus = NULL; if (pipe(av_pipe) != 0) { perror_log("pipe"); return -1; } if ((av_pid = fork()) == (pid_t) -1) { perror_log("fork"); (void) close(av_pipe[0]); (void) close(av_pipe[1]); return -1; } if (av_pid == (pid_t) 0) { if (av_pipe[1] != 1) { if (dup2(av_pipe[1], 1) == -1) { perror_log("dup2"); exit(EXIT_FAILURE); } if (close(av_pipe[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } } if (close(av_pipe[0]) != 0 || close(qq_pipe[1]) != 0 || close(qq_pipe_envelope[1]) != 0) { perror_log("close"); exit(EXIT_FAILURE); } if (chdir(ripmime_destdir) != 0) { perror_log("ripmime_destdir vanished, won't scan"); exit(EXIT_FAILURE); } (void) nice(SWEEP_NICE); (void) alarm(SWEEP_TIMEOUT); (void) execlp(SWEEP, SWEEP, SWEEP_ARGS, ripmime_destdir, (char *) NULL); perror_log("execlp"); exit(EXIT_FAILURE); } if (close(av_pipe[1]) != 0) { perror_log("close"); wait_everybody_and_die(99); } if ((avfp = fdopen(av_pipe[0], "r")) == NULL) { perror_log("fdopen"); wait_everybody_and_die(99); } while (fgets(line, (int) sizeof line, avfp) != NULL) { register char *virus; if ((virus = strstr(line, SWEEP_VIRUS_PREFIX)) != NULL) { register char *crlf; if (ret < INT_MAX) { ret++; } if (*foundvirus != NULL) { continue; } while ((crlf = strrchr(line, '\n')) != NULL || (crlf = strrchr(line, '\r')) != NULL) { *crlf = 0; } *virus = 0; virus = strrchr(line, ' '); *foundvirus = strdup(++virus); } } if (fclose(avfp) != 0) { perror_log("close"); } (void) close(av_pipe[0]); while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR); if (pid != av_pid) { if (close(qq_pipe[1]) != 0) { perror_log("close qq pipe"); } if (close(qq_pipe_envelope[1]) != 0) { perror_log("close envelope pipe"); } if (pid == qq_pid) { perror_log("we were waiting for the scanner to die, qq died first"); } else { perror_log("unexpected pid died"); } while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR); if (pid == av_pid) { perror_log("we were waiting for qq to die, but scanner died"); } else { perror_log("unexpected pid died"); } wait_everybody_and_die(99); } return ret; } static void delay_seconds(const time_t seconds) { #ifdef HAVE_NANOSLEEP struct timespec ts; ts.tv_sec = seconds; ts.tv_nsec = 0L; (void) nanosleep(&ts, NULL); #elif defined(HAVE_USLEEP) # ifdef USLEEP_RETURNS_INT if (usleep((unsigned long) seconds * 1000000UL) != 0) { if (errno == EINVAL) # endif { unsigned long wr = (unsigned long) seconds; do { (void) usleep(999999UL); wr--; } while (wr > 0UL); } # ifdef USLEEP_RETURNS_INT } # endif #else (void) sleep((unsigned int) seconds); #endif } static int viruscan_destdir(void) { char *foundvirus; int nb_attachments; int avret; int ret = 0; int tries = SCAN_MAX_RETRIES; if ((nb_attachments = has_attachments()) < 0) { perror_log("error while checking for attachments"); return -1; } if (nb_attachments == 0) { if (rmdir(ripmime_destdir) != 0) { perror_log("unable to remove ripmime_destdir, supposed to be empty"); if (cleanup_destdir() != 0) { perror_log("tried to cleanup destdir, but still got troubles"); } if (rmdir(ripmime_destdir) != 0) { perror_log("last chance to unlink ripmime_destdir failed"); } } *ripmime_destdir = 0; return 0; } while (find_concurrency() >= SCAN_CONCURRENCY) { if (tries <= 0) { write_log(LOG_INFO, "max scanning concurrency reached, " "intentionnaly delaying"); ret = -1; goto clean; } tries--; delay_seconds((time_t) SCAN_DELAY_BETWEEN_RETRIES); } if ((avret = spawn_antivirus(&foundvirus)) < 0) { perror_log("antivirus failure"); ret = -1; goto clean; } else if (avret > 0) { write_log(LOG_INFO, "%s in a mail from [%s]", foundvirus, getenv("TCPREMOTEIP")); if (avret > 1) { write_log(LOG_INFO, "%d other virus %s found in the same mail from [%s]", avret - 1, avret > 2 ? "were" : "was", getenv("TCPREMOTEIP")); } ret = 1; } clean: if (cleanup_destdir() != 0) { perror_log("destdir cleanup after virus checking failed"); } if (rmdir(ripmime_destdir) != 0) { perror_log("unlinking ripmime_destdir failed after virus checking"); } *ripmime_destdir = 0; return ret; } static int create_destdir_name(void) { (void) snprintf(ripmime_destdir, sizeof ripmime_destdir, RIPMIME_DESTDIR_BASE "/%lu_%lu", (unsigned long) now, (unsigned long) getpid()); return 0; } static int create_destdir(void) { if (create_destdir_name() != 0) { perror_log("unable to create the destdir name"); return -1; } if (mkdir(ripmime_destdir, (mode_t) 0750) != 0) { if (chdir(ripmime_destdir) != 0) { perror_log("unable to create/access ripmime_destdir"); return -1; } if (cleanup_destdir() != 0) { perror_log("unable to clean up destdir"); if (rmdir(ripmime_destdir) != 0) { perror_log("unable to remote destdir"); } *ripmime_destdir = 0; return -1; } } return 0; } static int wait_ripmime(void) { int status; pid_t pid; while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR); if (pid != ripmime_pid) { if (close(qq_pipe[1]) != 0) { perror_log("close qq pipe"); } if (close(qq_pipe_envelope[1]) != 0) { perror_log("close envelope pipe"); } if (pid == qq_pid) { perror_log("we were waiting for ripmime to die, but qq died first"); } else { perror_log("unexpected pid died"); } while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR); if (pid == ripmime_pid) { perror_log("we were waiting for qq to die, but ripmime died"); } else { perror_log("unexpected pid died"); } wait_everybody_and_die(99); } return 0; } static int safe_write(const int fd, const void *buf_, size_t count) { ssize_t written; register const char *buf = (const char *) buf_; while (count > (size_t) 0U) { for (;;) { if ((written = write(fd, buf, count)) <= (ssize_t) 0) { if (errno == EAGAIN) { sleep(1); } else if (errno != EINTR) { return -1; } continue; } break; } buf += written; count -= written; } return 0; } static int add_received(void) { static const char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; char date[256]; struct tm *tm; if ((tm = gmtime(&now)) == NULL || tm->tm_mon < 0 || tm->tm_mon > 11) { return -1; } (void) snprintf(date, sizeof date, "Received: from qmail by qscan (mail filter); " "%d %s %d %02d:%02d:%02d +0000\n", tm->tm_mday, months[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); return safe_write(qq_pipe[1], date, strlen(date)); } static void timeout(int sig) { (void) sig; wait_everybody_and_die(52); } static void check_bypass(void) { if (getenv("QS_BYPASS") == NULL && getenv("TCPREMOTEIP") != NULL) { return; } (void) execlp(QMAIL_QUEUE, QMAIL_QUEUE, (char *) NULL); exit(99); } static int inject_message(void) { char buf[MSG_BUFSIZE]; ssize_t len; ssize_t ripmime_written; ssize_t qq_written; ssize_t written; for (;;) { if ((len = read(0, buf, sizeof buf)) < (ssize_t) 0) { if (errno == EINTR) { continue; } perror_log("read() troubles when reading the message feed"); return -1; } if (len == (ssize_t) 0) { return 0; } ripmime_written = qq_written = (ssize_t) 0; do { if ((written = write(ripmime_pipe[1], buf + ripmime_written, (size_t) (len - ripmime_written))) < (ssize_t) 0) { if (errno == EINTR) { continue; } perror_log("write() troubles when feeding ripmime"); return -1; } ripmime_written += written; if ((written = write(qq_pipe[1], buf + qq_written, (size_t) (len - qq_written))) < (ssize_t) 0) { if (errno == EINTR) { continue; } perror_log("write() troubles when feeding qq"); return -1; } qq_written += written; } while (ripmime_written < len || qq_written < len); } } static int inject_envelope(void) { char buf[ENVELOPE_BUFSIZE]; ssize_t len; for (;;) { if ((len = read(1, buf, sizeof buf)) < (ssize_t) 0) { if (errno == EINTR) { continue; } perror_log("read() troubles when reading the envelope"); return -1; } if (len == (ssize_t) 0) { return 0; } if (safe_write(qq_pipe_envelope[1], buf, (size_t) len) != 0) { perror_log("write() troubles when feeding the envelope"); return -1; } } } int main(void) { int nbviruses; (void) signal(SIGALRM, timeout); (void) alarm(QSCAN_TIMEOUT); now = time(NULL); check_bypass(); if (create_destdir() != 0) { perror_log("unable to create the destination directory"); return 1; } if (spawn_ripmime() != 0) { perror_log("unable to spawn ripmime"); return 1; } if (spawn_qq() != 0) { perror_log("unable to spawn qq"); return 1; } if (add_received() != 0) { perror_log("unable to add the received line"); wait_everybody_and_die(99); } if (inject_message() != 0) { perror_log("unable to inject message"); wait_everybody_and_die(99); } if (close(ripmime_pipe[1]) != 0) { perror_log("close ripmime pipe"); wait_everybody_and_die(99); } if (wait_ripmime() != 0) { perror_log("troubles when waiting for ripmime"); wait_everybody_and_die(99); } if ((nbviruses = viruscan_destdir()) < 0) { write_log(LOG_INFO, "virus scanning delayed"); wait_everybody_and_die(99); } if (nbviruses != 0) { write_log(LOG_INFO, "delivery from [%s] refused", getenv("TCPREMOTEIP")); wait_everybody_and_die(39); } if (inject_envelope() != 0) { perror_log("unable to inject the envelope to qq"); wait_everybody_and_die(99); } if (close(qq_pipe[1]) != 0) { perror_log("close qq pipe"); wait_everybody_and_die(99); } if (close(qq_pipe_envelope[1]) != 0) { perror_log("close envelope pipe"); wait_everybody_and_die(99); } wait_everybody_and_die(0); return 0; }