/* * @(#)smtprelay.c--Relay smtp protocol with intervention in the DATA phase * @(#)STS/KBS 14feb2001 * * usage: [-xn] filter_path real_server_path ... server args ... */ #include "smtprelay.h" char *iam; /* Global process name (av[0]) */ char msgbuf[4096]; /* General message buffer */ /* Leave this an explicit array--We use sizeof here * and there */ char msg_354[] = "354 Enter Mail, end with '.'"; char msg_451[] = "451 Local processing error--Try again later"; char msg_552_template[] = "552 Message too big (>%d bytes)--Don't try again"; char msg_552[512]; char msg_noop[] = "NOOP"; /* * Funtion defs */ int RelayProtocol(int,int,int,int,char *); char *FilterMessage(char *,char *,int *,char *,char *); int GetArticle(NEWS *, NEWS *, NEWS *, char *, int, int *); void PingServer(NEWS *, NEWS *); void usage(void); /* * Mail buffer for incomming messages. * Do it all in memory, no tmp files * * Put this as the last data def... maybe the system will allocate * memory in the order we declare it and this will create the least * fragmentation and the large buffer, if not used, will only be * unused virtual memory */ int mailbytes; char mailbuf[MAXMESSAGE]; /* * System constant--Ticks per second */ int ticksPerSecond; int main( int ac, char *av[] ) { int serverrfd; /* Server read fd */ int serverwfd; /* Server write fd */ int pipe1[2],pipe2[2]; /* Tmp pipe fd's */ int serverpid; /* Server pid */ /* getopt stuff */ int oc; /* current option char */ int on; /* current string option number */ extern char *optarg; /* stuff for getopt */ extern int optind, optopt; /* stuff for getopt */ iam = av[0]; logId = "smtpr"; setbuf(stderr,NULL); debugLevel = DEFAULTDEBUGLEVEL; on = 0; while(( oc = getopt(ac,av,"x:")) != -1 ) { switch(oc) { case 'x': debugLevel = atoi(optarg); break; case ':': case '?': default: usage(); } } /* * Adjust for remaining arguments */ ac += optind; av += optind; if (ac < 2) { sprintf(msgbuf,"usage: %s filter-path smtp-server-path ...args...",iam); LogMsg(msgbuf); return(1); } /* * Dynamic message contents */ sprintf( msg_552, msg_552_template, MAXMESSAGE ); /* * System clock ticks */ ticksPerSecond = (int)sysconf(_SC_CLK_TCK); dprintf(DEBUG_ACTIVE,(debugout, "Ticks per second = %d\n", ticksPerSecond )); /* * Ignore sigpipe's--We want an error if a pipe reader dies */ signal(SIGPIPE,SIG_IGN); /* * Create two pipes for bi-directional communication with the * server process * * pipe 1 * 0r Server stdin * 1w Relay write to server * pipe 2 * 0r Relay read from server * 1w Server stdout */ if( pipe(pipe1) != 0) { LogError("Can't create pipe 1"); } if( pipe(pipe2) != 0) { LogError("Can't create pipe 2"); } /* * Fork and exec the server * * Fork() * Diddle the pipes to setup stdin/out for the server * Exec the server. */ switch( serverpid = fork() ) { case -1: /* error */ LogError("Couldn't fork"); return(2); default: /* parent */ serverrfd = pipe2[0]; close(pipe2[1]); serverwfd = pipe1[1]; close(pipe1[0]); break; case 0: /* child */ /* * Setup stdin/out from pipes */ close(0); dup(pipe1[0]); close(pipe1[0]); close(pipe1[1]); close(1); dup(pipe2[1]); close(pipe2[0]); close(pipe2[1]); /* * Exec the server (av[1]) * Shift the arguments and use the new av[0] as the path * to execute and pass on av[] as the arg list */ av += 1; execv(av[0],av); LogError(av[0]); return(2); } /* We only fall out of here from a successful parent */ return( RelayProtocol(0,1,serverrfd,serverwfd,av[0]) ); } int RelayProtocol( int netrfd, /* Network read fd (0/stdin) */ int netwfd, /* Network write fd (1/stdout) */ int serverrfd, /* Server read fd */ int serverwfd, /* Server write fd */ char *filter /* Filter path */ ) { NEWS *NI,*NO; /* Network i/o */ NEWS *SI,*SO; /* Server i/o */ int fd; /* Current active fd (from select) */ int minfd, maxfd; /* Minimum/Maximum select fd */ fd_set bmask; /* Base selection mask */ fd_set rmask; /* select mask */ struct timeval tval; /* select timeout */ int selres; /* select result */ char netbuf[513]; /* network buffer */ char *np; /* pointer in netbuf */ int rsize; /* read size */ int wsize; /* write size */ int state_data; /* data state (0=no, 1=msg) */ int state_reset; /* reset sate (0=no, 1=ign svr rsp) */ int ret; /* misc return values */ char *filtermessage; /* Return message from filter routine */ char *mailfrom; /* 'MAIL FROM: ...' value */ char *rcptto; /* 'RCPT TO: names */ int recycle; /* flag for buffered data */ dprintf(DEBUG_FUN,(debugout, "RelayProtocol(netrfd=%d, netwfd=%d, serverrfd=%d, serverwfd=%d)\n", netrfd, netwfd, serverrfd, serverwfd )); /* * Allocate and initialize network and server stream structures */ if( (NI = (NEWS *)calloc(1,sizeof(NEWS))) == NULL) { LogMsg("Out of memory"); return(2); } else { NI->fd = netrfd; NI->nsbp = NI->nsb; NI->nsbs = 0; } if( (NO = (NEWS *)calloc(1,sizeof(NEWS))) == NULL) { LogMsg("Out of memory"); return(2); } else { NO->fd = netwfd; NO->nsbp = NO->nsb; NO->nsbs = 0; } if( (SI = (NEWS *)calloc(1,sizeof(NEWS))) == NULL) { LogMsg("Out of memory"); return(2); } else { SI->fd = serverrfd; SI->nsbp = SI->nsb; SI->nsbs = 0; } if( (SO = (NEWS *)calloc(1,sizeof(NEWS))) == NULL) { LogMsg("Out of memory"); return(2); } else { SO->fd = serverwfd; SO->nsbp = SO->nsb; SO->nsbs = 0; } /* * Setup selection mask and minimum/maximum fd for select */ FD_ZERO(&bmask); FD_SET(NI->fd, &bmask); FD_SET(SI->fd, &bmask); minfd = (NI->fd < SI->fd ? NI->fd : SI->fd); maxfd = (NI->fd > SI->fd ? NI->fd : SI->fd); /* * Other inits */ state_data = 0; /* Not in a 'data' state initially */ rcptto = mailfrom = NULL; while(1) { /* * Setup timeout */ memset(&tval,'\0',sizeof(tval)); tval.tv_sec = SELECT_TIMEOUT; tval.tv_usec = 0; /* * Install the base read mask and wait for something to happen */ memcpy(&rmask, &bmask, sizeof(bmask)); selres = select(maxfd+1,&rmask,0,0,&tval); /* * Errors from select are fatal */ if (selres == -1) { LogError("select()"); return(2); } /* * Timeouts are fatal */ if (selres == 0) { sprintf( msgbuf, "TIMEOUT: Connections inactive %d seconds", SELECT_TIMEOUT ); LogMsg(msgbuf); return(2); } /* * Ok, we got something. * Process any active descriptors. * * Recycle flag is set internally if one of the fd's has * buffered data and needs to be reprocessed. * This happens because the NewsGets() routine reads the * network into a relativly large internal buffer. If * either the server or the connecting host sends more than * one line, NewsGets() will return the first line and likely * will have read some or all of the additional data into * it's buffer. When we detect buffered data, we will set * the recycle flag and do an FD_ISSET so we will reprocess * the additional data. */ recycle = 1; while (recycle) { recycle = 0; for(fd = minfd; fd <= maxfd; fd++) { if (!FD_ISSET(fd, &rmask)) continue; dprintf(DEBUG_ACTIVE,(debugout, "Select on fd %d\n", fd )); if (fd == NI->fd) { /* * DATA FROM NETWORK */ if( NewsGets( NI, netbuf, sizeof(netbuf), &rsize, NEWSGETS_LINE ) != 0 ) { /* * Blow out on any anomolies */ return(2); } dprintf(DEBUG_DETAIL,(debugout, "Read %d bytes from %d [%s]\n", rsize, fd, netbuf )); /* * Buffered data--Setup for recycle */ if (SI->nsbs > 0) { FD_SET(fd,&rmask); recycle++; } /* * Data from the network... Send back to the server */ /* * Check for "data" phase and intercept */ if (strcasecmp(netbuf,"data") == 0) { dlogmsg(DEBUG_ACTIVE,"Acking 'DATA' command"); /* * Ack 'data' locally--Fatal if error */ if(NewsPuts(NO,msg_354,sizeof(msg_354)-1,NEWSPUTS_CRLF)) { return(2); } if( ( ret = GetArticle( NI, SI, SO, mailbuf, sizeof(mailbuf), &mailbytes ) ) != 1 ) { if(ret == 0) { /* * If we overran the buffer, read * the reset of the article and throw * it away then reject the message */ while( ! ( ret = GetArticle( NI, SI, SO, mailbuf, sizeof(mailbuf), &mailbytes ) ) ); /* * Nak the message with a too large */ if( NewsPuts( NO, msg_552, strlen(msg_552), NEWSPUTS_CRLF ) ) { return(2); } /* * Send a reset to the server */ sprintf(netbuf,"RSET"); rsize = strlen(netbuf); state_reset = 1; } else { return(2); /* Other errors */ } } else { /* * Article read ok. */ state_data = 1; sprintf( msgbuf, "Received message (%d bytes)", mailbytes ); dlogmsg(DEBUG_ACTIVE,msgbuf); /* * Ping the server before we start the filter * just so everybody stays awake */ PingServer(SI,SO); /* * Note! the original 'data' command is still in * netbuf so we will fall out and let that get * sent on to the server */ filtermessage = FilterMessage( filter, mailbuf, &mailbytes, mailfrom, rcptto ); dprintf(DEBUG_ACTIVE,(debugout, "Filter: %s\n", filtermessage ? filtermessage : "OK" )); if (filtermessage) { if( NewsPuts( NO, filtermessage, strlen(filtermessage), NEWSPUTS_CRLF ) ) { return(2); } /* * Send a reset to the server */ sprintf(netbuf,"RSET"); rsize = strlen(netbuf); state_reset = 1; } } } /* * Check for 'mail from:' and save the value */ if (strncasecmp(netbuf,"mail from:",10) == 0) { if (mailfrom) { free(mailfrom); } if (rcptto) { /* * Also reset rcptto when we get a mailfrom */ free(rcptto); rcptto = NULL; } mailfrom = strdup(netbuf+10); dprintf(DEBUG_ACTIVE,(debugout, "MAIL FROM: [%s]\n", mailfrom )); } /* * Check for 'rcpt to:' and save the value * * If there is an existing value, append the new one * (multiple addresses) */ else if (strncasecmp(netbuf,"rcpt to:",8) == 0) { if (rcptto) { realloc(rcptto,strlen(rcptto)+strlen(netbuf)-8+3); sprintf(rcptto+strlen(rcptto),", %s",netbuf+8); } else { rcptto = strdup(netbuf+8); } dprintf(DEBUG_ACTIVE,(debugout, "RCPT TO: [%s] -> [%s]\n", netbuf+8, rcptto )); } if(NewsPuts(SO,netbuf,rsize,NEWSPUTS_CRLF)) { return(2); } } else if (fd == SI->fd) { /* * DATA FROM SERVER */ if( NewsGets( SI, netbuf, sizeof(netbuf), &rsize, NEWSGETS_LINE ) != 0 ) { /* * Blow out on any anomolies */ return(2); } dprintf(DEBUG_DETAIL,(debugout, "Read %d bytes from %d [%s]\n", rsize, fd, netbuf )); /* * Buffered data--Setup for recycle */ if (SI->nsbs > 0) { FD_SET(fd,&rmask); recycle++; } /* * If we are in a server reset state, ignore the * server response. * * We are here if we failed the message internally * (virus check, size, etc...) and we had issued * a reset (RSET) to the server. The network isn't * expecting a response at this point so we just throw * it away. */ if (state_reset == 1) { state_reset = 0; continue; } /* * If we are in a 'data' state this should be the * server response to the 'data' comamnd. * If it is an "ok" response (354) then send the * spooled data on to the server. * * Clear the data state. Whatever comes back from * the server will be passed back to the network * as the final message response. */ if (state_data == 1) { dlogmsg(DEBUG_ACTIVE,"Data state 1: forwarding message"); state_data = 0; if (strncmp(netbuf,"354",3) == 0) { /* * Send the message on to the server. * The server's response will go back to the * network in the normal loop. */ if(NewsPuts(SO,mailbuf,mailbytes,NEWSPUTS_NONE)) { return(2); } /* * Don't send anything back now. Just go back * into the normal loop and wait for a server * response */ continue; } /* * If we got a bad return code from the DATA command * (from the server), let it go back to the network * * The network is really waiting for a response to the * message (not the 'DATA' command, which we already * acked) but the codes are close enough that it should * fly. */ } /* * Data from the server... Send back to network */ if(NewsPuts(NO,netbuf,rsize,NEWSPUTS_CRLF)) { return(2); } } else { /* * Unexpected descriptor--Fatal */ sprintf( msgbuf, "Unexpected i/o on socket %d", fd ); LogMsg(msgbuf); return(2); } } } } /* NOTREACHED */ } /* * Filter the message * * Invokes the filter program (must be a full path name) with one arg * mailfrom value * * The current message is sent to the filter on stdin. * The filter is expected to return the message on it's stdout. * * Return codes (from the filter or from this routine) * 90 message is ok * 91 message contains a virus * 99 message is bad (for some reason) * 100 filter failed * * Return from this routine * message response * 250 message is ok * 451 local error--Try later * 452 no storage--try later * 552 mailbox exceeded storage limits--fatal * 554 transaction failed--fatal * */ char * FilterMessage( char *filter, /* Filter path name */ char *message, /* Message buffer */ int *msglength, /* Length of message */ char *mailfrom, /* 'mail from: ' value */ char *rcptto /* 'rcpt to:' value(s) */ ) { int pipe1[2], pipe2[2]; /* Pipes to filter process */ int filterpid; /* Filter pid */ int filterstat; /* filter termination status */ int filterrfd, filterwfd; /* Filter read/write descriptors */ int i; /* misc var */ int wsize; /* write size */ int rsize; /* read size */ char *p; /* misc pointer */ char *rval; /* return value */ static char rbuf[512]; /* return message buffer */ dprintf(DEBUG_FUN,(debugout, "FilterMessage(filter='%s', message=%p, msglength=%d, mailfrom=%s, rcptto=%s)\n", filter, message, *msglength, mailfrom ? mailfrom : "(null)", rcptto ? rcptto : "(null)" )); /* * Create two pipes for bi-directional communication with the * server process * * pipe 1 * 0r Filter stdin * 1w Relay write to filter * pipe 2 * 0r Relay read from filter * 1w Filter stdout */ if( pipe(pipe1) != 0) { LogError("Can't create filter pipe 1"); return(msg_451); } if( pipe(pipe2) != 0) { LogError("Can't create filter pipe 2"); close(pipe1[0]); close(pipe1[1]); return(msg_451); } /* * Fork and exec the filter * * Fork() * Diddle the pipes to setup stdin/out for the server * Exec the server. */ switch( filterpid = fork() ) { case -1: /* error */ LogError("Couldn't fork filter"); rval = msg_451; goto done; default: /* parent */ filterrfd = pipe2[0]; close(pipe2[1]); filterwfd = pipe1[1]; close(pipe1[0]); break; case 0: /* child */ /* * Setup stdin/out from pipes */ close(0); dup(pipe1[0]); close(pipe1[0]); close(pipe1[1]); close(1); dup(pipe2[1]); close(pipe2[0]); close(pipe2[1]); /* * Exec the filter */ execl(filter,filter,mailfrom,rcptto,(char *)0); LogError(filter); exit(100); } /* Good parent if we fall out here */ /* * Send message to filter */ p = message; i = *msglength; while ( i > 0 ) { if ( (wsize = write(filterwfd,p,i)) <= 0 ) { if (wsize == 0) { LogMsg("Unexpected EOF on write to filter"); } else { LogError("Write to filter"); } rval = msg_451; goto done; } p += wsize; i -= wsize; } close(filterwfd); filterwfd = -1; dlogmsg(DEBUG_ACTIVE,"Message sent to filter"); /* * Get the message back from the filter */ p = message; i = MAXMESSAGE; while( i > 0 ) { if ( (rsize = read(filterrfd,p,i)) <= 0 ) { if (rsize == 0) { /* * EOF--ok, break out of read loop */ break; } else { LogError("Read from filter"); } rval = msg_451; goto done; } p += rsize; i -= rsize; } /* * Check if the filtered message is now too big * * Read a single char. If not EOF we exceeded our message * maximum. Read and discard the rest of the message and * return a too-big error. */ i = p - message; /* current size */ rsize = read(filterrfd,msgbuf,1); if (rsize > 0) { /* * Message is too big. Read and discard the rest */ i += rsize; while((rsize = read(filterrfd,msgbuf,sizeof(msgbuf))) > 0) i += rsize; /* UNTESTED */ sprintf( msgbuf, "Message returned from filter is too big (%d > %d)", i, MAXMESSAGE ); LogMsg(msgbuf); rval = msg_552; goto done; } *msglength = i; dprintf(DEBUG_ACTIVE,(debugout, "%d bytes received back from the filter\n", i )); /* * Wait for the filter to exit * * Ignore errors in wait (shouldn't happen) */ if ( (i=waitpid(filterpid,&filterstat,0)) == filterpid ) { if(WIFSIGNALED(filterstat)) { sprintf( msgbuf, "Filter crashed with signal %d", WTERMSIG(filterstat) ); LogMsg(msgbuf); sprintf( rbuf, "451 Local filter error (c%d)--Possibly temporary", WTERMSIG(filterstat) ); rval = rbuf; goto done; } } else { filterstat = 0; } rval = rbuf; switch(WEXITSTATUS(filterstat)) { case 90: /* ok */ rval = NULL; /* No return message--good */ break; case 91: /* virus */ sprintf( rbuf, "554 Message contains a VIRUS!!!--Don't try again" ); break; case 99: /* mail was bad */ sprintf( rbuf, "554 Message was rejected for unspecified reasons--Don't try again" ); break; case 100: /* filter failure (assumed temporary) */ sprintf( rbuf, "451 Local filter error (100)--Possibly temporary" ); break; default: /* other exit codes are fatal */ sprintf( rbuf, "451 Local filter error (%d)--Possibly temporary", WEXITSTATUS(filterstat) ); break; } done: close(pipe1[0]); close(pipe1[1]); close(pipe2[0]); close(pipe2[1]); return(rval); } /* * Make a guess at the network throughput then do controlled size * reads from the netowork. After each read, if multiple reads * are necessare, hit the server with a NOOP command as a keepalive * so it doesn't disconnect during long processing. * * Normal timeout for a response is five minutes (300 seconds). * Read sizes are computed to ping the server every 30 seconds. * This should give us plenty of leeway for varying network loads * and data compressability. */ int GetArticle( NEWS *NI, /* Network input stream */ NEWS *SI, /* Server input stream */ NEWS *SO, /* Server output stream */ char *abuf, /* Article buffer */ int abufsize, /* Article buffer size */ int *asize /* Pointer to storage for art sz--NULL ok*/ ) { int ret; char *ap; /* Current offset in the article buffer */ int as; /* Article size to read */ int rsize; /* Amount read */ int tread; /* Total read */ int ttr; /* Tmp total read */ struct tms tms; /* times() buffer--Not used */ clock_t stime; /* Start time of the read */ clock_t etime; /* End time of the read */ int cps; /* Computed characters per second */ dprintf(DEBUG_FUN,(debugout, "GetArticle(NI=%p, SI=%p, SO=%p, abuf=%p, abufsize=%d, *asize=%p)\n", NI, SI, SO, abuf, abufsize, asize )); tread = 0; ap = abuf; as = 30000; /* Initial estimate 30 sec @ 1000 cps */ do { stime = times(&tms); ttr = 0; /* * Set the read size to the computed 60 second buffer * or whatever is left. Whichever is smaller. */ rsize = abufsize - (ap - abuf); if (rsize > as) rsize = as; /* * Get the article (or part) */ ret = NewsGets( NI, ap, rsize, &ttr, NEWSGETS_ARTICLE ); tread += ttr; ap += ttr; if (ret == 0) { /* * We reached our read limit without finding the end of the * article. If we really totally filled the article buffer * then return--The article is too big. * If we only filled a partial read, compute our throughput, * ping the server, and start a new read based on our * throughput. */ if (tread >= abufsize) { /* * We filled the article buffer without reaching the * end of the article. Pass back NewsGets() return * code */ goto done; } /* * Compute the throughput and the next read size * The read size is computed to finish in one minute * so the net effect will be to ping the server once * every 30 seconds while we are reading the message. */ etime = times(&tms); cps = ( ttr * ticksPerSecond) / (etime - stime); dprintf(DEBUG_ACTIVE,(debugout, "%d bytes in %d ticks = %d cps\n", ttr, etime-stime, cps )); if (cps < 1000) cps = 1000; as = cps * 30; /* * Ping the server to keep it alive * Ignore any errors... Something else will blow up if the * server dies. */ PingServer(SI,SO); } } while (ret == 0); /* * Set the read and pass back the last return code */ done: if (asize) *asize = tread; return(ret); } void PingServer( NEWS *SI, /* Server input stream */ NEWS *SO /* Server output stream */ ) { char netbuf[513]; /* network buffer */ if(NewsPuts(SO,msg_noop,sizeof(msg_noop)-1,NEWSPUTS_CRLF) == 0) { if ( NewsGets( SI, netbuf, sizeof(netbuf), (int *)0, NEWSGETS_LINE ) == 0 ) { dlogmsg(DEBUG_ACTIVE,"Ping server: OK"); } else { dlogmsg(DEBUG_OP,"Ping server: FAIL ON SERVER READ"); } } else { dlogmsg(DEBUG_OP,"Ping server: FAIL ON SERVER WRITE"); } } void usage(void) { sprintf( msgbuf, "usage: %s [-xn] filter-path smtp-server-path ...args...", iam ); LogMsg(msgbuf); exit(1); /* NOTREACHED */ }