/////////////// // lightweight connection from qmail to filters e.g. spamassassin // (hi-q filter, get it?) // 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 // error exit codes, as stated in qmail.c 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. void dump(const string var){ char* str = getenv(var.c_str()); if (str) cerr << "hi-q: " << var << " is set to '" << str << "'" << endl; else cerr << "hi-q: " << var << " is not set." << endl; } int xclose(int arg){ cerr << "closing " << arg << endl; return close(arg); } extern char** environ; int main(int argc, char** argv) { int verbose(1); int kidstatus; pid_t somekid; int rslt; int loose_end = 0; #ifdef SpareStuff char* slurp2_args[] = {"/home/jsd/hack/slurp2", 0}; char* echo_args[] = {"/bin/echo", "hi there", 0}; char* wc_args[] = {"/usr/bin/wc", 0}; char* cat_args[] = {"/bin/cat", 0}; char* spama_args[] = {"/usr/local/bin/spamassassin", "-e", 0}; char* spamc_args[] = {"/usr/local/bin/spamc", "-Z", "7", 0}; char* qq_args[] = {"/var/qmail/bin/qmail-queue", 0}; const char** joblist[] = { cat_args, slurp2_args, 0 // required: zero terminates the list }; #endif 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); vector job; while (parse.good()){ string token; parse >> token; if (parse.fail()) break; if (token[0] == '#') break; job.push_back(token); } if (job.size()) filter.push_back(job); } unsigned int nkids = filter.size(); if (0 && verbose) for (unsigned int ii = 0; ii < nkids; ii++) { cerr << "hi-q filter[" << ii << "] :; "; for (VS::const_iterator token = filter[ii].begin(); token != filter[ii].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. 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]) { /*** 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; } 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].size(); const char* prog[1+ntok]; for (int jj = 0; jj < ntok; jj++){ prog[jj] = filter[ii][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].begin(); token != filter[ii].end(); token++){ cerr << *token << " "; } cerr << endl; } for (unsigned int ii=0; ii