/////////////// // lightweight connection from qmail to filters e.g. spamassassin // (hi-q filter, get it?) // Hint: For testing, see also hi-test.conf which invokes ./hi-test: // ./hi-q hi-test.conf // TODO: Panic stop should signal all children. // TODO: Possibly: Wait for all kids in parallel? // That's because they might finish out of order. #include #include /* for exit(), getenv() */ #include #include #include /* for fork(), wait() */ #include #include using namespace std; #include #include #include #include #include #include #include // error exit codes, mostly as stated in qmail.c const int ex_good = 0; const int ex_spam = 21; const int ex_syserr = 71; const int ex_comerr = 74; #define bufsize 16384 void panic(const int sts) { // FIXME: stop other children exit(sts); } void slurp(const int inch, const int ouch){ char buf[bufsize]; ssize_t todo; for (;;) { ssize_t got = read(inch, buf, bufsize); if (got == 0) { // EoF break; } if (got < 0) { fprintf(stderr, "hi-q: input error: "); perror(0); panic(ex_comerr); } todo = got; while (todo) { ssize_t sent = write(ouch, buf, todo); if (sent < 0 && errno != EINTR) { fprintf(stderr, "hi-q: output error on fd%d : ", ouch); perror(0); panic(ex_comerr); } todo -= sent; } } } void probe_fd(){ int ii; struct stat buf; for (ii = 0; ii < 16; ii++) { int rslt = fstat(ii, &buf); fprintf(stderr, "fd %2d status %2d", ii, rslt); if (rslt==0) fprintf(stderr, " : %d", (int)buf.st_dev); fprintf(stderr, "\n"); } fprintf(stderr, "============\n"); } void blurb(const int ii, const pid_t* kidpid) { int kidstatus; /*pid_t somekid = */ waitpid(kidpid[ii], &kidstatus, WUNTRACED); if (WIFEXITED(kidstatus)) fprintf(stderr, "kid #%d (%d) exited with status %d\n", ii, kidpid[ii], WEXITSTATUS(kidstatus)); if (WIFSIGNALED(kidstatus)) fprintf(stderr, "kid #%d (%d) killed by signal %d\n", ii, kidpid[ii], WTERMSIG(kidstatus)); } // We are fussy about the argument types because we want // this to compile cleanly under g++ as well as gcc, // and each is strict about different things, such that // one or the other will complain unless everything is // done just right. // This is the way execve really behaves: // the characters are held constant // and the (char*) pointers are held constant: int Execve(char const * fn, char const * const * argv, char const * const * env) { // coerce the arg types to match the unwise declaration in unistd.h : return execve(fn, (char*const*) argv, (char*const*) env); } void usage() { cerr << "Usage:\n" " hi-q filter.conf\n" "or\n" " HI_Q_CONF=filter.conf hi-q\n"; } //////////////////////////////////////// // we have data coming in on fd 0. // and envelope / control information coming in on fd 1. string progname; pid_t mypid; void dump(const string var){ char* str = getenv(var.c_str()); cerr << progname << "[" << mypid << "] " << var; if (str) cerr << " is set to '" << str << "'" << endl; else cerr << " is not set." << endl; } int xclose(int arg){ cerr << "closing " << arg << endl; return close(arg); } extern char** environ; typedef enum {gray, black, qq, fail} moder; class jobber{ public: moder mode; vector cmd; jobber(const moder _mode, const vector _cmd) : mode(_mode), cmd(_cmd) {} jobber(const string _mode, const vector _cmd) : mode(fail), cmd(_cmd){ setmode(_mode); } jobber() : mode(fail), cmd(0) {} void setmode(const string _mode) { if (0) {} else if (_mode == "gray") mode = gray; else if (_mode == "grey") mode = gray; // variant spelling else if (_mode == "black") mode = black; else if (_mode == "qq") mode = qq; else { cerr << "jobber: bad mode: " << _mode << endl; mode = fail; } } }; int main(int argc, char** argv) { progname = *argv; mypid = getpid(); dump("TCPREMOTEIP"); dump("TCPREMOTEHOST"); int verbose(0); int kidstatus; int rslt; int loose_end = 0; typedef vector VS; vector filter; string conf_var = "HI_Q_CONF"; char* auth = getenv("QMAIL_AUTHORIZED"); if (auth && *auth) conf_var = "HI_Q_AUCONF"; char* conf_name; if (argc == 1) { conf_name = getenv(conf_var.c_str()); if (!conf_name) { usage(); exit(1); } } if (argc >= 2) { conf_name = argv[1]; } if (argc >= 3) { if (auth && *auth) conf_name = argv[2]; } if (argc > 3) { usage(); exit(1); } ifstream conf; conf.open(conf_name); if (! conf.good()) { cerr << "hi-q: could not open filter.conf file '" << conf_name << "'" << endl; exit(1); } for (;;) { string line; if (!getline(conf, line).good()) break; istringstream parse(line); jobber job; while (parse.good()){ string token; parse >> token; if (parse.fail()) break; if (token[0] == '#') break; job.cmd.push_back(token); } if (job.cmd.size()) { job.setmode(job.cmd.front()); job.cmd.erase(job.cmd.begin()); } if (job.cmd.size()) filter.push_back(job); } unsigned int nkids = filter.size(); // Check for nothing to do. // This is important, because the "last kid" is a special case. // This makes it safe to assume that nkids-1 is non-negative. if (nkids == 0) exit(0); // nothing to do if (0 && verbose) for (unsigned int ii = 0; ii < nkids; ii++) { cerr << "hi-q filter[" << ii << "] :; "; for (VS::const_iterator token = filter[ii].cmd.begin(); token != filter[ii].cmd.end(); token++){ cerr << *token << " "; } cerr << endl; } vector kidpid(nkids); // indexed by kid number const int rEnd(0); // end of a pipe for reading const int wEnd(1); // end of a pipe for writing int sync[2]; int resync[2]; if (pipe(sync) != 0) cerr << "sync pipe failed" << endl; if (pipe(resync) != 0) cerr << "resync pipe failed" << endl; // At this point, there are some loop invariants; // (a) fd0 is open (standard input) and has the email msg, // ready for the next child to read, and // (b) fd1 is open (nonstandard input) and has envelope information. // We need it to be open, so that pipe() // doesn't choose it. That allows N-1 of the kids // to close it and dup() something useful onto it. map iiofpid; for (unsigned int ii=0; ii < nkids; ii++){ /* loop starting all kids */ int datapipe[2]; int kid_end; //xx fprintf(stderr, "Top of loop %d loose: %d\n", ii, loose_end); if (loose_end) { close(0); dup2(loose_end, 0); close(loose_end); } // Create a pipe, which will be used to connect // this child's fd1 to the next child's fd0 ... // except for the last kid, which reads both fd0 and fd1, // while writing nothing. rslt = pipe(datapipe); if (rslt < 0) { fprintf(stderr, "hi-q: could not create datapipe: "); perror(0); panic(ex_syserr); } //xx fprintf(stderr, "pipe: %d %d\n", datapipe[0], datapipe[1]); // For N-1 kids, the loose end feeds forward. // It will be written by this kid and read by the next kid. // For the last kid, the loose end connects to hi-q. // It will be written by hi-q and read by the last kid. int lastkid = (ii == nkids-1); #define flip(a,b) (lastkid ? b : a) loose_end = datapipe[flip(rEnd, wEnd)]; kid_end = datapipe[flip(wEnd, rEnd)]; kidpid[ii] = fork(); if (kidpid[ii] == -1) { cerr << "hi-q: fork failed : "; perror(0); exit(ex_syserr); } iiofpid[kidpid[ii]] = ii; if (!kidpid[ii]) { /*** child code ***/ pid_t kidgroup(0); // process group for all kids is // equal to pid of kid#0 if (ii) kidgroup = kidpid[0]; if (setpgid(0, kidgroup) != 0) { cerr << "*** kid " << ii << " setpgid failed! " << errno << " ... "; perror(0); } else { // cerr << "*** kid " << ii << " setpgid OK" << endl; } // ... everybody else has to wait for us to get this far ... // ... so that the new process group will be valid ... // Write-a-byte synchronization is released when the *first* guy writes. if (ii == 0) { int junk(1); write(sync[wEnd], &junk, 1); //cerr << "sync sent" << endl; } #if 0 cerr << "kid [" << ii << "] " << getpid() << " kidpid[0]: " << kidpid[0] << " pgid: " << getpgid(0) << " starts" << endl; #endif close(resync[wEnd]); // send resync // ... now we must wait for everybody else, because ... // ... if we do the exec(), the new process group becomes invalid ... // Close synchronization is released when the *last* guy closes. if (ii==0) { int junk; //cerr << "about to read resync" << endl; ssize_t rslt = read(resync[rEnd], &junk, 1); if (rslt < 0 ) { cerr << "bad sync ... " << rslt << endl; // FIXME (maybe?) should this be fatal? } else { // cerr << "back from read resync, good: " << rslt << endl; } } // Now that we are through creating pipes, we don't // need to continue blocking fd1: close(1); close(loose_end); // the reading end is none of this kid's business // except last kid: writing end rslt = dup2(kid_end, 1); // the writing end is stdout for this kid // except last kid: nonstandard input if (rslt < 0) { fprintf(stderr, "hi-q: kid %d: dup2(%d,1) failed: ", ii, kid_end); perror(0); exit(ex_syserr); } close(kid_end); // use fd1 instead now // OK, at this point this kid is set up to read fd0 and write fd1 // (except last kid reads fd1 as well as fd0). //// probe_fd(); int ntok = filter[ii].cmd.size(); const char* prog[1+ntok]; for (int jj = 0; jj < ntok; jj++){ prog[jj] = filter[ii].cmd[jj].c_str(); } prog[ntok] = 0; close(resync[rEnd]); close(sync[rEnd]); close(sync[wEnd]); stringstream convert; convert << getpgid(0); const string grouper("HI_Q_GROUP=" + convert.str()); if (putenv((char*)grouper.c_str()) != 0) { cerr << "putenv failed" << endl; exit(1); } rslt = Execve(prog[0], prog, environ); fprintf(stderr, "hi-q: failed to exec '%s': ", prog[0]); perror(0); exit(ex_syserr); } /*** parent code ***/ if (kidpid[ii] < 0) { fprintf(stderr, "hi-q: failure to fork kid#%d: ", ii); perror(0); panic(ex_syserr); } close(kid_end); // Let kid #0 run a little ways: if (ii==0) { int junk; //cerr << "about to read sync" << endl; ssize_t rslt = read(sync[rEnd], &junk, 1); if (rslt != 1) { cerr << "bad sync ... 1 != " << rslt << endl; } else { //cerr << "back from read sync, good: " << rslt << endl; } } #if 0 cerr << "apparent kid #" << ii << " (" << kidpid[ii] << ") " << endl; #endif } /* end loop starting all kids */ // here with the whole pipeline of kids launched close(resync[wEnd]); // important, so that block gets released close(resync[rEnd]); // less important, just housecleaning close(sync[wEnd]); // more housecleaning close(sync[rEnd]); close(0); // Housecleaning: the reading end of stdin was // delegated to the first child, // so we don't need it. if (verbose) for (unsigned int ii = 0; ii < nkids; ii++) { cerr << "hi-q filter[" << ii << "] " << kidpid[ii] << " :; "; for (VS::const_iterator token = filter[ii].cmd.begin(); token != filter[ii].cmd.end(); token++){ cerr << *token << " "; } cerr << endl; } pid_t special_pid = kidpid[nkids-1]; int alive(nkids-1); // not counting the special kid int best_blame(0); // best reason, even if not a great reason pid_t argbest_blame(-1); // kid# associated with best blame for (;;) { if (alive == 0) break; pid_t somekid = waitpid(-1, &kidstatus, WUNTRACED); if (somekid == special_pid){ // do not decrement the "alive" counter // since that only applies to non-special kids if (WIFEXITED(kidstatus)) { cerr << "hi-q: special kid exited early" << endl; return(ex_syserr); } else if (WIFSIGNALED(kidstatus) && WTERMSIG(kidstatus) != SIGUSR1) { cerr << "hi-q: special kid exited early" << endl; return(ex_syserr); } else { /* paused, not dead */ } continue; } // here if somekid is not the special kid if (WIFEXITED(kidstatus)) { alive--; if (WEXITSTATUS(kidstatus)) { argbest_blame = somekid; best_blame = kidstatus; break; } } else if (WIFSIGNALED(kidstatus)) { alive--; argbest_blame = somekid; best_blame = kidstatus; if (WTERMSIG(kidstatus) != SIGUSR1) break; } else { /* kid is paused, not dead */ /* not a problem */ } } // here if all kids have exited normally // *or* if there is a great reason for quitting early /////////////////// // decode the best reason why the filter-chain terminated if (best_blame) { string short_name(""); int kidno(iiofpid[argbest_blame]); if (WIFEXITED(best_blame)) { int sts = WEXITSTATUS(best_blame); if (sts == 1) { cerr << "hi-q says: kid[" << kidno << "]" << " pid " << argbest_blame << " i.e. '" << filter[kidno].cmd[0] << "'" << " reports spam." << endl; panic(ex_spam); } if (sts != 0) { cerr << "hi-q says: kid " << argbest_blame << " exited with bad status: " << sts << endl; panic(ex_syserr); } else { // should never get here unless exit status was nonzero cerr << "hi-q: should never happen" << endl; panic(ex_syserr); } } else if (WIFSIGNALED(best_blame)) { int sig = WTERMSIG(best_blame); cerr << "hi-q says: kid " << argbest_blame << " was killed by signal " << sig << endl; // if the *best* blame is a kill, that's not normal panic(ex_syserr); } } // Here if all filters agree this is not spam. // Now it is safe to transfer the envelope information: slurp(1, loose_end); close(1); close(loose_end); // now that the envelope information has been transfered, // wait for the last kid in the usual way { for(;;) { waitpid(special_pid, &kidstatus, WUNTRACED); if (WIFEXITED(kidstatus)) { int sts = WEXITSTATUS(kidstatus); cerr << "hi-q says: qq program " << kidpid[nkids-1] << " i.e. '" << filter[nkids-1].cmd[0] << "'" << " returned status " << sts << endl; return sts; } else if (WIFSIGNALED(kidstatus)) { cerr << "hi-q says: qq program " << kidpid[nkids-1] << " i.e. '" << filter[nkids-1].cmd[0] << "'" << " was killed by signal " << WTERMSIG(kidstatus) << endl; return ex_syserr; } else { /* paused, not dead */ } } } }