/* This file is in the public domain. */ #define CLIENTS_PER_PROCESS 64 #define MIN_CLIENT_SLOTS 5 #define DEF_SLEEP 0 #define PARENT_TIMER_INC 900 #define MAXILINELEN 65536 /* LISTEN_ON can be 0, to listen on all available addresses, or a space-separated list of addresses to listen on, suitable for passing to getaddrinfo(). */ #define LISTEN_ON 0 /* Define ERROR_AND_LOG to make it accept connections and log stuff about them, but error out all attempts to actually send anything. ERROR_AND_LOG should be defined to a number, which is used as the SMTP error code and thus normally should be something like 450 or 550. This also shuts off output that would normally go to stdout. */ /*#define ERROR_AND_LOG 550*/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern const char *__progname; #if CLIENTS_PER_PROCESS >= 256 #error "CLIENTS_PER_PROCESS must be less than 256" #endif #if MIN_CLIENT_SLOTS >= CLIENTS_PER_PROCESS #warning "MIN_CLIENT_SLOTS >= CLIENTS_PER_PROCESS probably won't work well" #endif /* * The protocols between the various processes are relatively simple. * * Listeners can be in any of these states: * * LS_LISTENING * The listener is accepting new connections and managing * those it already has. * * LS_FULL * LS_STOPPING * LS_DYING * The listener is managing existing connections but not * listening for new connections. * * When the parent forks a new listener it comes up in state * LS_LISTENING. When a listener acquires a new client or loses an * existing client, it sends the parent an LM_LISTENING message giving * the number of client slots it has available - or, if none, it * switches to LS_FULL and sends LM_FULL (with no following count). * * When the parent wants to kill off a listener, it sends it an LM_STOP * byte. The listener then switches from LS_LISTENING or LS_FULL (as * the case may be) to LS_STOPPING. * * When the parent dies, the listener notices the connection to the * parent break and switches to LS_DYING. This state is just like * LS_STOPPING except that the listener does not try to interact * further with the parent. * * When an LS_STOPPING listener has no clients, it sends the parent an * LM_DYING byte and exits; LS_DYING is the same except that the * listener doesn't send anything before exiting. * * When a listener sees an SMTP command that changes global state, it * sends the parent an LM_STATE message. The second byte is one of * the STATE_* constants, and any later bytes depend on which one. * The parent then echoes this message to all listeners, so they can * change their state correspondingly. * * When an LS_LISTENING listener sees a new connection, it accepts it * and starts speaking SMTP to it. * * The parent tries to keep no fewer than MIN_LISTENERS and no more * than MAX_LISTENERS listeners in state LS_LISTENING. If there are * fewer, it forks a new one; if there are more, it tells one to stop. * * When compiled with DOSTATS defined, the protocol between the * listeners and the parent becomes significantly chattier. In this * case, every connection accept(), every SMTP command, every address * checked, and every DATA body handled, each of these things produces * a message to the parent, for statistics-keeping. (The messages are * SM_ACCEPT, SM_ADDR, SM_BODY, and one message per SMTP command in * the smtpcmds[] array below.) Of course, these are not sent when * the listener is in state LS_DYING. Message bodies not completed * (eg, client drops the connection before the dot) are not counted. * Statistics are printed as part of the response to SIGUSR1. * * The SMTP listener imposes comparatively few checks. It does check * all command lines to ensure they don't contain octets in the 0-31 * or 128-159 ranges, and it imposes a limit of 64K on each SMTP * command line. (It imposes neither of these restrictions on * DATA-phase data.) It requires that commands be syntactic in a few * of the most rudimentary ways. A line that attempts to exceed the * 64K limit produces an abrupt and ungraceful dropping of the * connection; command lines with forbidden octets produce 500 syntax * error replies. * * VRFY and EXPN are implemented, but as dummy routines, the effect of * which is that they are completely disabled. * * Source routes in RCPT commands have all but the part after the colon * silently disacarded before being checked or passed along. * * The state machine for a CLIENT looks like this. * * startup: * CS_SBB * CS_SBB: wait for banner_sleep seconds * 220->client, CS_GREET * CS_GREET: read from client * get HELO: 250->client, CS_HELO * get RSET: 250->client, CS_GREET * get QUIT: 221->client, CS_DEAD * get other: error->client * CS_HELO: read from client * get HELO: 250->client, CS_HELO * get MAIL: 250->client, CS_MAIL * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get other: error->client * CS_MAIL: read from client * get HELO: 250->client, CS_HELO * get DATA: 503->client, CS_MAIL * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get RCPT: save rcpt, 250->client, CS_FAIL * get other: error->client * CS_FAIL: read from client * get HELO: 250->client, CS_HELO * get DATA: 354->client, CS_BDATA * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get RCPT: save rcpt, 250->client, CS_FAIL * get other: error->client * CS_BDATA: copy data client->file; on dot, * error->client, log, CS_HELO * * If ERROR_AND_LOG is defined, the CS_MAIL, CS_FAIL, and CS_BDATA * cannot be reached, and MAIL from CS_HELO returns the ERROR_AND_LOG * error code. * * Some SMTP commands are valid in any "read from client" state: * * get NOOP: 250->client * get VRFY: 553->client * * Also, EHLO counts as HELO. */ /* * There are multiple distinct namespaces here; I use different numbers * for all of them to get slightly better error checking - if, for * example, I use LS_DEAD where I mean CS_DEAD, this sort of numbering * will help catch it. */ /* Listener State */ /* These are used both by the parent, to record a listener's state in the structure for that listener, and by the listener, in ownstate. */ #define LS_LISTENING 1 #define LS_FULL 2 #define LS_STOPPING 3 #define LS_DYING 4 /* LS_DEAD is not truly a listener state; it is used to mark a listener data structure corresponding to a listener that's died. (Or, to put it another way, it's a listener state in the parent, but not in a listener.) */ #define LS_DEAD 5 /* Client State */ #define CS_SBB 6 /* delaying before sending greeting */ #define CS_GREET 7 /* sent greeting, expecting HELO */ #define CS_HELO 8 /* post-HELO, want MAIL */ #define CS_MAIL 9 /* post-MAIL, want first RCPT */ #define CS_FAIL 10 /* collecting RCPTs, don't care about validity */ #define CS_BDATA 11 /* collecting data for to-be-rejected message */ #define CS_DEAD 12 /* transient state during client shutdown */ #define CS_BURIED 13 /* like CS_DEAD only more so */ /* Listener Message */ #define LM_FULL 14 #define LM_LISTENING 15 #define LM_STOP 16 #define LM_DYING 17 #define LM_STATE 18 #define LM_STATS 19 #define LM_STATS_DATA 20 /* never actually sent - used internally */ /* State-change values */ #define STATE_TIMEOUT 21 /* one byte further data, timeout in seconds */ /* Statistics Message */ #define SM_FIRST_STATS (SM_ACCEPT) #define SM_ACCEPT 22 #define SM_ADDR 23 #define SM_BODY 24 #define SM__CMD 25 #define SM_CMD_HELO (SM__CMD+CMD_HELO) #define SM_CMD_EHLO (SM__CMD+CMD_EHLO) #define SM_CMD_MAIL (SM__CMD+CMD_MAIL) #define SM_CMD_RCPT (SM__CMD+CMD_RCPT) #define SM_CMD_DATA (SM__CMD+CMD_DATA) #define SM_CMD_RSET (SM__CMD+CMD_RSET) #define SM_CMD_NOOP (SM__CMD+CMD_NOOP) #define SM_CMD_QUIT (SM__CMD+CMD_QUIT) #define SM_CMD_VRFY (SM__CMD+CMD_VRFY) #define SM_CMD_XCTL (SM__CMD+CMD_XCTL) #define SM_CMD_POST (SM__CMD+CMD_POST) #define SM_LAST_STATS (SM_CMD_POST) #define STATS_N (SM_LAST_STATS+1-SM_FIRST_STATS) /* Switch case pseudo-values. The are designed so you can write things like switch (...) { case DIGITS: ... } */ #define DIGITS '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9' #define UCLETTERS 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z' #define LCLETTERS 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z' /* * Data structures. * * LSOCK is a listening socket. There is one of these for each port on * which the shim accepts conenctions. They are set up by the parent * and inherited by each SMTP worker forked; it's the SMTP workers * that actually use them. * * LISTENER is used by the parent to record information about an SMTP * worker process. (These processes were originally thought of as * listeners for incoming SMTP connections, which is whence the name.) * * CLIENT is used by the SMTP workers to represent an SMTP client * connection and the stuff attached thereto. * * IOQ is an input/output queue; it contains both queued blocks of data * to be written and an input data buffer. * * OB is an output block of data - an IOQ uses these to record output * data. * * RCPT representes a recipient as a local-part and a domain. The * local-part is never nil; the domain is for the bare-postmaster * case. * * MSG encapsulates the state necessary to accumulate listener * messages, since this is wanted in multiple places. */ typedef struct lsock LSOCK; typedef struct listener LISTENER; typedef struct client CLIENT; typedef struct ioq IOQ; typedef struct ob OB; typedef struct rcpt RCPT; typedef struct msg MSG; #define MAX_LM_LEN (1+8+1+128) struct msg { int len; int fill; unsigned char buf[MAX_LM_LEN]; } ; struct rcpt { char *lp; char *dom; } ; #define NORCPT ((RCPT){.lp=0,.dom=0}) struct ob { OB *link; const char *data; char *tofree; int len; int ptr; } ; struct ioq { int fd; OB *q; OB **qt; char ibuf[8192]; int ibfill; int ibptr; } ; struct client { CLIENT *link; unsigned long long int serial;/* overall serial number */ int state; /* one of the CS_* values */ int scstate; /* saved state value when in CS_SCLOSE */ /* lbuf/lalloc/llen form an input line buffer, for input from wherever input is being read from - the client or the server. lalloc is the amount of memory allocated; llen is the amount used. */ unsigned char *lbuf; int lalloc; int llen; unsigned int statstate; /* state for LM_STATS response */ #define CSS_ATNL 0x00000001 int termstate; /* state machine for data termination */ int px; /* poll index */ time_t timeout; /* time at which timeout expires */ struct sockaddr *peeraddr; /* peer's address */ int peeralen; /* length of peeraddr */ char *peertext; /* text form of peeraddr */ struct sockaddr *myaddr; /* my address for this socket */ int myalen; /* length of myaddr */ char *mytext; /* text form of myaddr */ /* values from the SMTP dialogue. helo_as is from the HELO, env_from from the MAIL; each is nil if that hasn't happened yet. rcpt records a RCPT address while the shim holds onto it (which is only until it's been dealt with - eg during starting up a backend connection). */ char *helo_as; char *env_from; RCPT rcpt; struct timeval starttime; /* when connection came up */ struct timeval banner_at; /* when to emit banner */ int data_fd; /* fd of file holding message body */ #define NO_FD (-1) #define BAD_FD (-2) char *data_file; /* name of file holding message body */ unsigned int dubious; /* reasons this message is dubious */ #define DUBIOUS_MAIL_SPACE 0x00000001 /* "MAIL From: <" */ #define DUBIOUS_RCPT_SPACE 0x00000002 /* "RCPT To: <" */ #define DUBIOUS_HELO_SYNTAX 0x00000004 /* HELO arg nonsyntactic */ int nrcpts; /* number of saved recipients */ int arcpts; /* number of recipients rcpts points to */ RCPT *rcpts; /* saved recipients */ int baddata; /* just following a rejected DATA? */ int unrecog; /* unrecognized-command count */ /* When doing I/O, the state is looked at and something is chosen to add to the poll() list. checkfn is set to a function to call if the poll succeeds on that fd. If this is I/O to the client or server, ioq is set to point to either cq or sq (below) and iostr is set to a string for use in log messages, indicating what it's talking to. */ void (*checkfn)(CLIENT *); IOQ *ioq; const char *iostr; char *xid; /* transaction ID, nil if not yet set */ IOQ cq; /* I/O to client */ } ; struct lsock { LSOCK *link; int fd; int px; } ; struct listener { LISTENER *link; pid_t proc; int pipe; int state; int px; MSG msg; int fcs; } ; /* Parent->listener message, in the listener */ static MSG parentmsg; /* myhostname holds my hostname, per gethostname(); it's used various places, wherever this machine's hostname is needed. */ static char myhostname[MAXHOSTNAMELEN]; /* listeners is where the parent keeps its list of the SMTP workers. */ static LISTENER *listeners; /* accs is a list of sockets to accept SMTP connections on. As remarked above, this is set up by the parent at startup and inherited by the SMTP workers, which are the processes that actually use it. */ static LSOCK *accs; /* Next time at which we want to call parent_timeout(). See parent_timeout() for more. */ static unsigned long int parent_timer; /* This records a low-water mark for the number of free client slots. See parent_timeout() for more. */ static int client_minfree; /* List of fds to poll. Used by most processes. pfds is the list itself; apfds is the size of the memory allocated to pfds, with npfds the current number of pollfds set. */ static struct pollfd *pfds = 0; static int apfds = 0; static int npfds; /* Flag to notice SIGUSR1. */ static volatile int got_usr1; /* In an SMTP worker, the pipe between it and the parent. */ static int parentpipe; /* Poll index for parentpipe. */ static int pppx; /* An SMTP worker's own state (LS_*). */ static int ownstate; /* The state value corresponding to our current process title. */ static int titlestate; /* The number of clients we last told our parent we had. */ static int toldclients; /* A listener's list of clients. */ static CLIENT *clients; /* Current sleep before banner. 0 -> no sleep. */ static int banner_sleep; /* Cached getpid() result (<0 -> nothing cached) */ static int cached_pid = -1; /* Next client serial number - used by listeners */ static unsigned long long int client_serial; #ifdef ERROR_AND_LOG /* Stuff that's used only in ERROR_AND_LOG mode. */ /* Text form of ERROR_AND_LOG code */ static const char *error_code; #else /* Stuff that's used only in non-ERROR_AND_LOG mode. */ /* See xid() for what this is. */ static unsigned int xid_serial = 0; #endif /* * List of SMTP commands we recognize. XCTL is a private extension. * POST is not really an SMTP command; we recognize it in order to do * special handling for attempts to talk to us through open Web * proxies. */ static const char *smtpcmds[] = { "HELO", #define CMD_HELO 0 "EHLO", #define CMD_EHLO 1 "MAIL", #define CMD_MAIL 2 "RCPT", #define CMD_RCPT 3 "DATA", #define CMD_DATA 4 "RSET", #define CMD_RSET 5 "NOOP", #define CMD_NOOP 6 "QUIT", #define CMD_QUIT 7 "VRFY", #define CMD_VRFY 8 "XCTL", #define CMD_XCTL 9 "POST", #define CMD_POST 10 0 }; #define CMD__N 11 #ifdef DOSTATS static time_t starttt; static char starttext[256]; static unsigned int counters[STATS_N]; #endif #if SM_LAST_STATS != SM__CMD+CMD__N-1 #error Statistics table disagrees with commands table #endif /* * We really should use CMSG_SPACE, CMSG_LEN, and CMSG_DATA. But we * want this same code to run on both a 1.4T sparc and a 1.6.1 i386, * and 1.4T is before the ABI change for SCM_RIGHTS. * * However, on i386, __cmsg_alignbytes() is 3, so the old way produces * the same data layout as the new. So we use the old way, but we * have these defines against the possibility of changing this in the * future. */ #ifdef OLD_CMSG_INTERFACE #define CMSPACE(x) (sizeof(struct cmsghdr)+(x)) #define CMLEN(x) (sizeof(struct cmsghdr)+(x)) #define CMSKIP(x) (sizeof(struct cmsghdr)) #else #define CMSPACE(x) (CMSG_SPACE((x))) #define CMLEN(x) (CMSG_LEN((x))) /* It's gross to have to do this, but it's forced upon us by the botched design of the the CMSG_* interface. This code is not as portable as I'd like - it depends on using a pointer past the end of an object, on some architectures - but I believe it's the least horrible of the available alternatives. */ #define CMSKIP(x) ((char *)CMSG_DATA((x))-(char *)(x)) #endif /* * It's not possible to do proper const poisoning in a program that * uses writev() without either extreme gyrations like mallocking * copies of every const string, or some kind of const-stripper. (The * abstractly correct way to handle this is to have one struct iovec * variant with non-const iov_base, for read, and one with const * iov_base, for write. That's not possible, for compatability * reasons if nothing else; it simply isn't how the interfaces are * currently designed.) * * We, of course, go the const-stripper route. Here it is. It may be * possible to do better than this, but I don't know of anything * better. This is admittedly not fully conformant C, in that it does * arithmetic on nil pointers, but it's the overall best alternative * I've seen. */ static void *deconst(const void *cvp) { return((((const char *)cvp)-(const char *)0)+(char *)0); } /* * Just like getpid(), except the result is cached, to save syscalls. */ static int mypid(void) { if (cached_pid < 0) cached_pid = getpid(); return(cached_pid); } /* * Some can't-happen has tripped. Gripe and core. */ static void panic_(const char *, int, const char *, ...) __attribute__((__format__(__printf__,3,4),__noreturn__)); static void panic_(const char *file, int line, const char *fmt, ...) #define panic(fmt,args...) panic_(__FILE__,__LINE__,fmt , ## args) { FILE *f; va_list ap; static int w(void *cookie __attribute__((__unused__)), const char *data, int len) { write(2,data,len); #ifndef ERROR_AND_LOG write(1,data,len); #endif return(len); } f = fwopen(0,&w); fprintf(f,"[%d] PANIC (\"%s\", line %d): ",mypid(),file,line); va_start(ap,fmt); vfprintf(f,fmt,ap); va_end(ap); fprintf(f,"\n"); fclose(f); fflush(0); abort(); } /* * Write function for use by msgopen, below. */ static int mow(void *cookie __attribute__((__unused__)), const char *b, int l) { #ifndef ERROR_AND_LOG fwrite(b,1,l,stdout); #endif fwrite(b,1,l,stderr); return(l); } /* * Close function for use by msgopen, below. */ static int moc(void *cookie __attribute__((__unused__))) { putc('\n',stderr); #ifndef ERROR_AND_LOG putc('\n',stdout); #endif return(0); } /* * Return a FILE * that writes to stdout and stderr both; also, print * appropriate prefixes to those streams, and when the FILE * is * closed, append newlines. */ static FILE *msgopen(void) { #ifndef ERROR_AND_LOG printf("[%d] ",mypid()); #endif fprintf(stderr,"%s: ",__progname); return(funopen(0,0,&mow,0,&moc)); } /* * Some serious error has occurred. Print the message to stdout * (prefixed with our PID) and to stderr (prefixed with __progname). * If fatal is set, then exit(1). */ static void serious(int, const char *, va_list) __attribute__((__format__(__printf__,2,0))); static void serious(int fatal, const char *fmt, va_list ap) { FILE *f; f = msgopen(); vfprintf(f,fmt,ap); fclose(f); fflush(0); if (fatal) exit(1); } /* * A fatal error has occurred. This is for errors that aren't * can't-happens but which are definitely fatal, like socketpair() * failing. */ static void fatal(const char *, ...) __attribute__((__format__(__printf__,1,2),__noreturn__)); static void fatal(const char *fmt, ...) { va_list ap; va_start(ap,fmt); serious(1,fmt,ap); va_end(ap); exit(1); /* actually can't be reached, but shut up gcc */ } /* * Some nonfatal but serious error has occurred. This is for serious * errors that we can nevertheless recover from. */ static void nonfatal(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void nonfatal(const char *fmt, ...) { va_list ap; va_start(ap,fmt); serious(0,fmt,ap); va_end(ap); } /* * Wrappers for malloc-family routines, so we can get a trace of malloc * actions. */ static void mlog(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void mlog(const char *fmt, ...) { static int initted = 0; static FILE *mlogf; va_list ap; if (! initted) { int fd; initted = 1; fd = open("shim-log.malloc-log",O_WRONLY|O_APPEND,0); if (fd < 0) { mlogf = 0; } else { mlogf = fdopen(fd,"w"); } } if (! mlogf) return; va_start(ap,fmt); vfprintf(mlogf,fmt,ap); va_end(ap); fflush(mlogf); } static const char *tstamp(void) { struct timeval tv; time_t tt; struct tm *tm; static char rbuf[64]; gettimeofday(&tv,0); tt = tv.tv_sec; tm = localtime(&tt); sprintf(&rbuf[0],"%02d:%02d:%02d.%06lu [%d]",tm->tm_hour,tm->tm_min,tm->tm_sec,(unsigned long int)tv.tv_usec,mypid()); return(&rbuf[0]); } static void *xmalloc(int nb, const char *f, int l) { void *rv; mlog("%s [%s, line %d] malloc(%d) -> ",tstamp(),f,l,nb); rv = malloc(nb); mlog("%p\n",rv); return(rv); } static void *xrealloc(void *data, int nb, const char *f, int l) { void *rv; mlog("%s [%s, line %d] realloc(%p,%d) -> ",tstamp(),f,l,data,nb); rv = realloc(data,nb); mlog("%p\n",rv); return(rv); } static void xfree(void *data, const char *f, int l) { mlog("%s [%s, line %d] free(%p)\n",tstamp(),f,l,data); free(data); } #define malloc(n) xmalloc((n),__FUNCTION__,__LINE__) #define realloc(x,n) xrealloc((x),(n),__FUNCTION__,__LINE__) #define free(x) xfree((x),__FUNCTION__,__LINE__) /* * When something happens that can be tolerated if it happens only * occasionally but should be a fatal error if it happens too often, * we use RATELIMIT(). This keeps a counter; each time the event * happens, it's incremented by "inc", and it's decremented by "dec" * per second, with a clamp to prevent it from going below 0. If it * ever reaches "max", we exit. */ #define RATELIMIT(inc,dec,max) \ do { static int c = 0; static time_t t = 0; ratelimit(&c,&t,(inc),(dec),(max)); } while (0) static void ratelimit(int *cp, time_t *tp, int inc, int dec, int max) { time_t now; if (*tp == 0) { time(tp); *cp = inc; return; } time(&now); if (now != *tp) { inc -= dec * (now - *tp); *tp = now; } inc += *cp; if (inc >= max) exit(1); if (inc < 0) inc = 0; *cp = inc; } /* * Initialize a new IOQ. */ static void ioq_init(IOQ *q, int fd) { q->fd = fd; q->q = 0; q->qt = &q->q; q->ibfill = 0; q->ibptr = 0; } /* * Queue a block of data for output. There are three variants of this, * depending on the exact semantics: * * ioq_oq_point Queues a pointer to the data; the data block * must remain valid until the write completes. * Nothing in particular is done when the block is * fully written. * * ioq_oq_copy A copy is made of the data; the data pointer * passed in need not remain valid once * ioq_oq_copy returns. The copy is freed once it * is completely written. * * ioq_oq_free Queues a pointer to the data, like * ioq_oq_point, but once the write is complete, * frees the data. This is semantically like * ioq_oq_copy followed immediately by free, * except it doesn't actually bother making a * copy. Of course, the data should not be * considered to remain valid after the call. */ static void ioq_oq_point(IOQ *q, const void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) return; b = malloc(sizeof(OB)); b->data = data; b->tofree = 0; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } static void ioq_oq_copy(IOQ *q, const void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) return; b = malloc(sizeof(OB)+len); bcopy(data,b+1,len); b->data = (void *) (b+1); b->tofree = 0; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } static void ioq_oq_free(IOQ *q, void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) { free(data); return; } b = malloc(sizeof(OB)); b->data = data; b->tofree = data; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } /* * Flush all queued output blocks. This disposes of the blocks as if * they were successfully written, but does not actually write * anything anywhere. It's used when a connection is being torn down * in circumstances under which we don't care about queued data * getting flushed (such as after a write error). */ static void ioq_flush(IOQ *q) { OB *b; while (q->q) { b = q->q; q->q = b->link; if (b->tofree) free(b->tofree); free(b); } q->qt = &q->q; } /* * Print to stdout - but not if ERROR_AND_LOG. */ static void printout(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void printout(const char *fmt, ...) { #ifdef ERROR_AND_LOG fmt = fmt; #else va_list ap; va_start(ap,fmt); vprintf(fmt,ap); va_end(ap); #endif } /* * Try to write queued data for a client. Which connection the write * is attempted on depends on the client's ioq pointer. * * This accumulates multiple data blocks into a vector of struct * iovecs, finally calling writev to perform the write. It tries the * write as soon as either it's run out of data blocks or it's * accumulated min(kern.iov_max,64) blocks. If it actually writes * anything, it then flushes that many bytes, including handling any * completely-written blocks. */ static int try_write(CLIENT *c) { static int maxiov = 0; static struct iovec *iov = 0; static int aiov = 0; int niov; OB *b; int w; IOQ *q; if (maxiov == 0) { int mib[2]; size_t vallen; mib[0] = CTL_KERN; mib[1] = KERN_IOV_MAX; vallen = sizeof(maxiov); if (sysctl(&mib[0],2,&maxiov,&vallen,0,0) < 0) fatal("sysctl kern.iov_max: %s",strerror(errno)); if (maxiov > 64) maxiov = 64; } niov = 0; q = c->ioq; b = q->q; while (b && (niov < maxiov)) { if (niov >= aiov) iov = realloc(iov,(aiov=niov+8)*sizeof(*iov)); iov[niov].iov_base = deconst(b->data+b->ptr); iov[niov].iov_len = b->len - b->ptr; niov ++; b = b->link; } w = writev(q->fd,iov,niov); if (w < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return(0); } printout("[%d] write error to %s: %s\n",mypid(),c->iostr,strerror(errno)); return(1); } while ((b=q->q) && (w >= b->len-b->ptr)) { w -= b->len - b->ptr; q->q = b->link; if (b->tofree) free(b->tofree); free(b); } if (! b) q->qt = &q->q; if (w) { if (! b) { nonfatal("write overrun (leftover=%d) to %s",w,c->iostr); return(1); } b->ptr += w; } return(0); } /* * Try to fill an IOQ's input buffer. */ static int ioq_read(IOQ *q) { int r; r = read(q->fd,&q->ibuf[0],sizeof(q->ibuf)); if (r < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return(0); } return(errno); } if (r == 0) return(-1); q->ibfill = r; q->ibptr = 0; return(0); } /* * Send our SMTP greeting banner to a CLIENT's client connection. */ static void send_greeting(CLIENT *c) { ioq_oq_point(&c->cq,"220 ",-1); ioq_oq_point(&c->cq,&myhostname[0],-1); ioq_oq_point(&c->cq," SMTP listener ready.\r\n",-1); } /* * Set a file descriptor nonblocking. Or rather, try to; we ignore * errors from the set attempt. (F_GETFL cannot fail for any reason * that won't make the F_SETFL fail equally hard, so we don't need to * check for F_GETFL failures.) */ static void set_nonblock(int fd) { fcntl(fd,F_SETFL,fcntl(fd,F_GETFL,0)|O_NONBLOCK); } /* * Record that a CLIENT has had something happen, so its inactivity * timeout should be updated. */ static void fresh_timeout(CLIENT *c) { time(&c->timeout); c->timeout += 3600; } /* * Send a message to our parent. We deliberately ignore write errors * here; the only plausible error is EPIPE, which can happen if the * parent dies just as we do something that makes us want to write to * it - but if the parent is gone, doing nothing is the right thing. */ static void tellparent(int nb, ...) { va_list ap; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tellparent: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;ipeeraddr->sa_family) { case AF_INET: return(ntohl(((struct sockaddr_in *)c->peeraddr)->sin_addr.s_addr) == 0x7f000001); break; case AF_INET6: return(IN6_IS_ADDR_LOOPBACK(&((struct sockaddr_in6 *)c->peeraddr)->sin6_addr)); break; } return(0); } /* * Accept a new connection on the given LSOCK. If we get EINTR or * EWOULDBLOCK, ignore it (if there's a real connection, we'll pick it * up next time around the main loop); other errors get reported, and * if they occur too fast, we die. If we succeed, set up a new * CLIENT and toss it into the pot. */ static void accept_acc(LSOCK *a) { int new; struct sockaddr_storage peerss; socklen_t peersslen; struct sockaddr_storage myss; socklen_t mysslen; CLIENT *c; char hnbuf[NI_MAXHOST]; char pnbuf[NI_MAXSERV]; peersslen = sizeof(peerss); new = accept(a->fd,(struct sockaddr *)&peerss,&peersslen); if (new < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } nonfatal("accept: %s",strerror(errno)); RATELIMIT(1,1,60); return; } mysslen = sizeof(myss); if (getsockname(new,(struct sockaddr *)&myss,&mysslen) < 0) { nonfatal("getsockname after accept: %s",strerror(errno)); RATELIMIT(1,1,60); close(new); return; } STATS_RECORD(SM_ACCEPT); set_nonblock(new); c = malloc(sizeof(CLIENT)); /* link done below */ c->serial = client_serial ++; /* state done below */ c->lbuf = 0; c->lalloc = 0; c->llen = 0; /* statstate needs no init */ c->termstate = 0; /* px needs no init */ fresh_timeout(c); c->peeraddr = malloc(peersslen); bcopy(&peerss,c->peeraddr,peersslen); c->peeralen = peersslen; if (getnameinfo((const void *)&peerss,peersslen,&hnbuf[0],NI_MAXHOST,&pnbuf[0],NI_MAXSERV,NI_NUMERICHOST|NI_NUMERICSERV)) { asprintf(&c->peertext,"[getnameinfo failed; %s]",strerror(errno)); } else { asprintf(&c->peertext,"%s/%s",&hnbuf[0],&pnbuf[0]); } c->myaddr = malloc(mysslen); bcopy(&myss,c->myaddr,mysslen); c->myalen = mysslen; if (getnameinfo((const void *)&myss,mysslen,&hnbuf[0],NI_MAXHOST,&pnbuf[0],NI_MAXSERV,NI_NUMERICHOST|NI_NUMERICSERV)) { asprintf(&c->mytext,"[getnameinfo failed; %s]",strerror(errno)); } else { asprintf(&c->mytext,"%s/%s",&hnbuf[0],&pnbuf[0]); } c->helo_as = 0; c->env_from = 0; c->rcpt = NORCPT; gettimeofday(&c->starttime,0); /* banner_at done below */ c->data_fd = NO_FD; c->data_file = 0; c->dubious = 0; c->nrcpts = 0; c->arcpts = 0; c->rcpts = 0; c->baddata = 0; c->unrecog = 0; /* checkfn/ioq/iostr need no init */ c->xid = 0; ioq_init(&c->cq,new); if ((banner_sleep < 1) || client_is_local(c)) { c->state = CS_GREET; send_greeting(c); } else { c->state = CS_SBB; c->banner_at.tv_sec = c->starttime.tv_sec + banner_sleep; c->banner_at.tv_usec = c->starttime.tv_usec; } c->link = clients; clients = c; } /* * Add a file descriptor to the poll vector, showing interest in the * given event(s). */ static int add_pfd(int fd, short int ev) { if (npfds >= apfds) pfds = realloc(pfds,(apfds=npfds+8)*sizeof(struct pollfd)); pfds[npfds] = ((struct pollfd){.fd=fd,.events=ev,.revents=0}); return(npfds++); } /* * Append some data to a client's input line. */ static int iline_append(CLIENT *c, const void *data, int len) { if (c->llen+len > MAXILINELEN) return(1); if (c->llen+len > c->lalloc) c->lbuf = realloc(c->lbuf,c->lalloc=c->llen+len); bcopy(data,c->lbuf+c->llen,len); c->llen += len; return(0); } /* * Make a mallocked NUL-terminated string from a pointer-and-length * block of data. */ static char *blk_to_malloc_str(const void *data, int len) { char *t; if (len < 0) len = strlen(data); t = malloc(len+1); if (len) bcopy(data,t,len); t[len] = '\0'; return(t); } #ifndef ERROR_AND_LOG /* * Pick apart the forward-path from an RCPT command. Returns 0 if all * went well, nonzero on error. On success, stores the local-part and * domain through *lp and *dom. */ static int parse_path(const char *buf, int len, char **lp, char **dom) { int x; int lpbeg; int lpend; int dombeg; int domend; static char get(int i) { return(((i < 0) || (i >= len)) ? '\0' : buf[i]); } static int skip_domain(int x) { if (get(x) == '[') { while (1) { x ++; switch (get(x)) { case '\\': x ++; break; case ']': goto out1; /* break 2, grrr */ break; case '\0': return(-1); break; } } out1:; x ++; } else { /* This is more lenient than 2822's grammar calls for; this code accepts zero-length labels, dots at the beginning or end of the string, and dashes at the beginning or end of labels. */ while (1) { switch (get(x)) { case '-': case '.': case DIGITS: case UCLETTERS: case LCLETTERS: x ++; break; default: goto out2; /* break 2, grrr */ break; } } out2:; } return(x); } /* This routine does not quite parse an RFC2821 "Path"; it parses the part of a path between the < and >. */ /* Ugh. There is no way to skip over a source route without parsing most of it, if only because domains can contain arbitrary characters in their source-text form (via domain -> address-literal -> general-address-literal -> dcontent -> quoted-pair). */ /* Fortunately, whether by chance or design, the grammar is relatively parser-friendly. In particular, minimal lookahead is called for. */ x = 0; /* First, skip any a-d-l: portion. */ if (get(x) == '@') { while (1) { x ++; x = skip_domain(x); if (x < 0) return(1); switch (get(x++)) { case ',': break; case ':': goto out3; /* break 2, grrr */ break; default: return(1); break; } } out3:; } /* What's left must be a mailbox - or an error. */ lpbeg = x; if (get(x) == '"') { /* quoted-string local-part */ while (1) { x ++; switch (get(x)) { case '\\': x ++; break; case '"': goto out4; /* break 2, grrr */ break; case '\0': return(1); break; } } out4:; x ++; } else { /* dot-string local-part */ /* This code is more lenient than 2822's grammar. In particular, 2822 requires that dots not be first or last, that no two dots be adjacent internally, and imposes some constraints on where - can appear. */ while (1) { switch (get(x)) { case '.': case DIGITS: case UCLETTERS: case LCLETTERS: case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+': case '-': case '/': case '=': case '?': case '^': case '_': case '`': case '{': case '|': case '}': case '~': x ++; break; default: goto out5; /* break 2, grrr */ break; } } out5:; } lpend = x; if (get(x) != '@') return(1); x ++; dombeg = x; x = skip_domain(x); if (x < 0) return(1); domend = x; if (x != len) return(1); *lp = blk_to_malloc_str(buf+lpbeg,lpend-lpbeg); *dom = blk_to_malloc_str(buf+dombeg,domend-dombeg); return(0); } #endif /* * Free the strings pointed to by a RCPT. */ static void free_rcpt(RCPT r) { free(r.lp); free(r.dom); } /* * Do common cleanup for RSET and similar commands. Most places that * call this do additional state-epsecific cleanup. */ static void do_rset(CLIENT *c) { int i; free(c->env_from); c->env_from = 0; for (i=c->nrcpts-1;i>=0;i--) free_rcpt(c->rcpts[i]); c->nrcpts = 0; if (c->data_fd >= 0) close(c->data_fd); c->data_fd = NO_FD; c->dubious &= ~(DUBIOUS_MAIL_SPACE|DUBIOUS_RCPT_SPACE); } /* * Convert a digit to its numeric value. */ static int digitval(char) __attribute__((__const__)); static int digitval(char c) { switch (c) { case '0': return(0); break; case '1': return(1); break; case '2': return(2); break; case '3': return(3); break; case '4': return(4); break; case '5': return(5); break; case '6': return(6); break; case '7': return(7); break; case '8': return(8); break; case '9': return(9); break; } return(-1); } /* * Check a (supposed) IPv4 literal for syntax conformance. */ static int syntactic_ipv4_literal(const char *s, int len, int brackets, struct in_addr *ia) { int x; int i; int j; int n; int v; int octet[4]; x = 0; if (brackets) { if (s[x] != '[') return(0); x ++; } for (i=0;i<4;i++) { if (x >= len) return(0); n = 0; for (j=0;(j<3)&&(x 255) return(0); octet[i] = n; if (i < 3) { if ((x >= len) || (s[x] != '.')) return(0); x ++; } else { if (brackets) { if ((x != len-1) || (s[x] != ']')) return(0); x ++; } else { if (x != len) return(0); } } } if (ia) ia->s_addr = (octet[0] * 0x01000000) | (octet[1] * 0x00010000) | (octet[2] * 0x00000100) | octet[3]; return(1); } /* * Check a (supposed) IPv6 literal for syntax conformance. */ static int syntactic_ipv6_literal(const char *s, int len) { int nhex; int nondec; int gotcomp; int x; int x0; int i; if (len < 9) return(0); if (strncasecmp(s,"[ipv6:",6)) return(0); nhex = 0; gotcomp = 0; x = 6; if (s[x] == ':') { if (s[x+1] != ':') return(0); gotcomp = 1; x += 2; } while (1) { x0 = x; nondec = 0; for (i=0;i<4;i++) { if (x >= len) return(0); switch (s[x]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': nondec ++; break; default: goto out; /* break 2 */ break; } x ++; } out:; if (x >= len) return(0); if (i == 0) return(0); if (s[x] == '.') { if (nondec) return(0); if (gotcomp ? (nhex > 4) : (nhex != 6)) return(0); for (;(x 6) : (nhex != 8)) return(0); return(1); } if (s[x] != ':') return(0); x ++; if (x >= len) return(0); if (s[x] == ':') { if (gotcomp) return(0); gotcomp = 1; x ++; } } } /* * Check a (supposed) address literal for syntax conformance. */ static int syntactic_address_literal(const char *s, int len) { int x; for (x=0;(x= len) return(0); if (s[x] == '.') return(syntactic_ipv4_literal(s,len,1,0)); if ((x == 4) && !strncasecmp(s,"ipv6",4)) return(syntactic_ipv6_literal(s,len)); /* 2821 says there can be others, but leaves "dcontent" unspecified, making it impossible to even parse-to-skip any such. */ return(0); } /* * Check a HELO/EHLO argument for syntax conformance. */ static int helo_nonsyntactic(const char *arg, int arglen) { int x; int prevc; if ((arglen > 2) && (arg[0] == '[')) return(!syntactic_address_literal(arg,arglen)); prevc = '.'; for (x=0;xhelo_as); c->helo_as = blk_to_malloc_str(s,l); } /* * Process a HELO/EHLO. (Since we support no ESMTP extensions, our * response is the same for EHLO as for HELO.) */ static void do_helo(CLIENT *c, const char *arg, int arglen) { if (arglen < 1) { arg = ""; arglen = 0; } set_helo_as(c,arg,arglen); if (helo_nonsyntactic(arg,arglen)) c->dubious |= DUBIOUS_HELO_SYNTAX; ioq_oq_point(&c->cq,"250 ",-1); ioq_oq_point(&c->cq,&myhostname[0],-1); ioq_oq_point(&c->cq,"\r\n",-1); do_rset(c); } #ifndef ERROR_AND_LOG /* * Handle RCPT. Perform rudimentary syntax checks and notice the * bare-postmaster special case, then hand the hard work off to * parse_path. */ static int do_rcpt(CLIENT *c, const char *arg, int arglen) { char *lp; char *dom; const char *abeg; int alen; static void syntax(void) { ioq_oq_point(&c->cq,"500 Syntax: RCPT To:\r\n",-1); printout("[%d] %s (%.*s): 500 Syntax: RCPT To:\n",mypid(),c->iostr,c->llen,c->lbuf); } if ( (arglen < 5) || (arg[arglen-1] != '>') || strncasecmp(arg,"to:",3) ) { syntax(); return(1); } abeg = &arg[3]; alen = arglen - 4; while ((alen > 0) && (*abeg == ' ')) { abeg ++; alen --; c->dubious |= DUBIOUS_RCPT_SPACE; } if (*abeg != '<') { syntax(); return(1); } abeg ++; alen --; if ((alen == 10) && !strncasecmp(abeg,"postmaster",10)) { lp = strdup("postmaster"); dom = 0; } else { if (parse_path(abeg,alen,&lp,&dom)) { syntax(); return(1); } } c->rcpt.lp = lp; c->rcpt.dom = dom; return(0); } #endif #ifndef ERROR_AND_LOG /* * Handle MAIL. Perform rudimentary syntax checks, then remember the * sender for passing on to the backend server if/when we open a * connection to it. We don't syntax-check the argument further; we * leave that up to the backend. (If we don't ever talk to the * backend, we don't care if it's invalid, and if we do, syntactically * invalid envelope senders are rare enough we don't mind the extra * cost of bothering the backend.) */ static int do_mail(CLIENT *c, const char *arg, int arglen) { const char *abeg; int alen; static void syntax(void) { ioq_oq_point(&c->cq,"500 Syntax: MAIL From:\r\n",-1); printout("[%d] %s (%.*s): 500 Syntax: MAIL From:\n",mypid(),c->iostr,c->llen,c->lbuf); } if ( (arglen < 7) || (arg[arglen-1] != '>') || strncasecmp(arg,"from:",5) ) { syntax(); return(1); } abeg = &arg[5]; alen = arglen - 6; while ((alen > 0) && (*abeg == ' ')) { abeg ++; alen --; c->dubious |= DUBIOUS_MAIL_SPACE; } if (*abeg != '<') { syntax(); return(1); } abeg ++; alen --; free(c->env_from); c->env_from = blk_to_malloc_str(abeg,alen); return(0); } #endif #ifndef ERROR_AND_LOG /* * Return the xid string for a client. This consists of a base-62 * rendition of a large number formed from * - a per-SMTP-worker serial number * - the SMTP worker's process ID * - a timestamp, in seconds and microseconds */ static char *xid(CLIENT *c) { if (! c->xid) { struct timeval tv; pid_t me; int z; unsigned char nbuf[14]; char tbuf[20]; int tx; int dr; static void ninc(unsigned long long int val, unsigned long long int mul) { int i; for (i=0;i>= 8; } } gettimeofday(&tv,0); me = mypid(); ninc(xid_serial++,0); ninc(me,30000); ninc(tv.tv_sec,0x100000000ULL); ninc(tv.tv_usec,1000000); for (z=sizeof(nbuf);(z>0)&&!nbuf[z-1];z--) ; tx = sizeof(tbuf); tbuf[--tx] = '\0'; while (z > 0) { int i; dr = 0; for (i=z-1;i>=0;i--) { dr = (dr * 256) + nbuf[i]; nbuf[i] = dr / 62; dr %= 62; } if (tx < 1) panic("xid overflow"); tbuf[--tx] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[dr]; if (! nbuf[z-1]) z --; } c->xid = strdup(tbuf+tx); } return(c->xid); } #endif /* * Signal-handler for SIGUSR1. Don't try to do anything here; just set * a flag which is noticed in the main loop. */ static void handle_usr1(int sig __attribute__((__unused__))) { got_usr1 = 1; } /* * Set up SIGUSR1 handling. */ static void setup_usr1(void) { struct sigaction sa; got_usr1 = 0; sa.sa_handler = &handle_usr1; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1,&sa,0); } /* * Ignore SIGPIPE. (We prefer to get EPIPE from writes.) */ static void ignore_pipe(void) { struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE,&sa,0); } /* * Initialize the statistics counters. */ static void stats_init(void) { #ifdef DOSTATS int i; struct tm *tm; time(&starttt); tm = localtime(&starttt); strftime(&starttext[0],sizeof(starttext),"%Y-%m-%d %H:%M:%S",tm); for (i=0;iai_next) { s = socket(ai->ai_family,ai->ai_socktype,ai->ai_protocol); if (s < 0) { nonfatal("socket (af %d): %s",ai->ai_family,strerror(errno)); continue; } v = 1; setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&v,sizeof(v)); /* ignore errors */ if (bind(s,ai->ai_addr,ai->ai_addrlen) < 0) { nonfatal("bind (af %d): %s",ai->ai_family,strerror(errno)); close(s); continue; } set_nonblock(s); listen(s,10); ls = malloc(sizeof(LSOCK)); ls->fd = s; ls->link = accs; accs = ls; } } hints.ai_flags = AI_PASSIVE; hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_addrlen = 0; hints.ai_canonname = 0; hints.ai_addr = 0; hints.ai_next = 0; listen_on = LISTEN_ON; if (listen_on) { const char *b; const char *e; char *on; int n; int l; on = 0; n = 0; b = listen_on; while (1) { while (*b == ' ') b ++; if (! *b) break; for (e=b;*e&&(*e!=' ');e++) ; l = e - b; if (l >= n) { free(on); n = l + 1; on = malloc(n); } bcopy(b,on,l); on[l] = '\0'; add_listeners(on); b = e; } free(on); } else { add_listeners(0); } if (! accs) fatal("no listening sockets"); } /* * Dump out the parent's state. */ static void dump_parent_state(FILE *to) { LISTENER *l; time_t now; time(&now); for (l=listeners;l;l=l->link) { fprintf(to,"listener proc=%d pipe=%d state=",(int)l->proc,l->pipe); switch (l->state) { case LS_LISTENING: fprintf(to,"LISTENING"); break; case LS_FULL: fprintf(to,"FULL"); break; case LS_STOPPING: fprintf(to,"STOPPING"); break; case LS_DYING: fprintf(to,"DYING"); break; case LS_DEAD: fprintf(to,"DEAD"); break; default: fprintf(to,"%d?",l->state); break; } fprintf(to,"\n"); } #ifdef DOSTATS fprintf(to,"Statistics:\n"); fprintf(to,"Connections accepted: %u\n",counters[SM_ACCEPT-SM_FIRST_STATS]); fprintf(to,"Recipients: %u\n",counters[SM_ADDR-SM_FIRST_STATS]); fprintf(to,"Message bodies: %u\n",counters[SM_BODY-SM_FIRST_STATS]); { int i; for (i=0;i= 86400) { fprintf(to,"%lud",up/86400); up %= 86400; } if (up >= 3600) { fprintf(to,"%luh",up/3600); up %= 3600; } if (up >= 60) { fprintf(to,"%lum",up/60); up %= 60; } if (up) fprintf(to,"%lus",up); fprintf(to,")\n"); } #endif } /* * Return a text name for a client state (CS_*). */ static const char *clientstatename(int cs) { static char badbuf[64]; switch (cs) { case CS_GREET: return("GREET"); break; case CS_HELO: return("HELO"); break; case CS_MAIL: return("MAIL"); break; case CS_BDATA: return("BDATA"); break; case CS_FAIL: return("FAIL"); break; case CS_DEAD: return("DEAD"); break; case CS_BURIED: return("BURIED"); break; } sprintf(&badbuf[0],"%d?",cs); return(&badbuf[0]); } /* * Dump out an SMTP worker's internal state. */ static void dump_listener_state(FILE *to) { LSOCK *a; CLIENT *c; time_t now; time(&now); fprintf(to,"state = "); switch (ownstate) { case LS_LISTENING: fprintf(to,"LISTENING"); break; case LS_FULL: fprintf(to,"FULL"); break; case LS_STOPPING: fprintf(to,"STOPPING"); break; case LS_DYING: fprintf(to,"DYING"); break; default: fprintf(to,"%d?",ownstate); break; } fprintf(to,"\n"); if (ownstate != LS_DYING) { for (a=accs;a;a=a->link) { fprintf(to,"listening fd %d\n",a->fd); } } for (c=clients;c;c=c->link) { fprintf(to,"client %s state=%s",c->peertext,clientstatename(c->state)); if (c->helo_as) fprintf(to," helo=%s",c->helo_as); if (c->env_from) fprintf(to," from=%s",c->env_from); fprintf(to," cq.fd=%d",c->cq.fd); fprintf(to," timeout=%d",(int)(c->timeout-now)); fprintf(to,"\n"); } } /* * The client's fd shows writeable. Try to write. On failure, mark * the client very dead (CS_DEAD tries to flush first - wrong here). */ static void check_poll_out(CLIENT *c) { if (pfds[c->px].revents && try_write(c)) c->state = CS_BURIED; } #ifndef ERROR_AND_LOG /* * Return the client's data-file fd. This opens the file if it's not * yet open, leaving the fd in c->data_fd as well as returning it. * Can return BAD_FD, but not NO_FD. */ static int client_data_fd(CLIENT *c) { switch (c->data_fd) { case NO_FD: { struct timeval tv; int serial; char *sp; char fn[64]; gettimeofday(&tv,0); sp = &fn[sprintf(&fn[0],"%lu.%06lu",(unsigned long int)tv.tv_sec,(unsigned long int)tv.tv_usec)]; serial = 0; while (1) { sprintf(sp,".%d",serial++); c->data_fd = open(&fn[0],O_RDWR|O_CREAT|O_EXCL,0600); if (c->data_fd >= 0) { if (unlink(&fn[0]) < 0) { printout("[%d] can't remove body file %s (for %s): %s\n",mypid(),&fn[0],c->peertext,strerror(errno)); close(c->data_fd); c->data_fd = BAD_FD; } else { c->data_file = strdup(&fn[0]); } break; } if (errno == EEXIST) continue; printout("[%d] can't open body file %s (for %s): %s\n",mypid(),&fn[0],c->peertext,strerror(errno)); c->data_fd = BAD_FD; break; } } break; case BAD_FD: break; } return(c->data_fd); } #endif #ifndef ERROR_AND_LOG /* * Save some DATA-phase data. The data buffer passed in here includes * the terminating dot (for the end-of-data call) and has not had * hidden dots stripped, nor CRLFs turned into NLs - it's the data as * it is on the wire. */ static void data_data(CLIENT *c, const void *data, int len) { int fd; int w; if (len < 0) len = strlen(data); fd = client_data_fd(c); if (fd >= 0) { w = write(fd,data,len); if (w < 0) { printout("[%d] write to %s (for %s): %s\n",mypid(),c->data_file,c->peertext,strerror(errno)); close(c->data_fd); c->data_fd = BAD_FD; } else if (w < len) { printout("[%d] write to %s (for %s): wanted %d, wrote %d\n",mypid(),c->data_file,c->peertext,len,w); close(c->data_fd); c->data_fd = BAD_FD; } } } #endif #ifndef ERROR_AND_LOG /* * Convert val to base 26 and put the result in to, which is width * characters long. If the value doesn't fit, a warning is printed. */ static void base26(char *to, unsigned long long int val, int width) { int i; for (i=width-1;i>=0;i--) { to[i] = "abcdefghijklmnopqrstuvwxyz"[val%26]; val /= 26; } if (val) fprintf(stderr,"[%d] base26: overflow (leftover %llu)\n",mypid(),val); } #endif #ifndef ERROR_AND_LOG /* * Log a message body with envelope information. */ static void log_message(CLIENT *c) { int serial; char *np; int fd; int i; int j; struct timeval tv; char bsp[13]; char name[64]; struct stat stb; char rbuf[8192]; gettimeofday(&tv,0); base26(&bsp[0],(tv.tv_sec*1000000ULL)+tv.tv_usec,12); bsp[12] = '\0'; np = &name[0]; bcopy("save/",np,5); np += 5; *np++ = bsp[0]; *np++ = bsp[1]; *np++ = bsp[2]; *np++ = '/'; *np++ = bsp[3]; *np++ = bsp[4]; *np++ = '/'; np[0] = '.'; np[1] = '\0'; /* Ugh. This has to build under compilers without labeled control structure, hence the gotos instead of what would normally be labeled do-while(0)s. */ retry1:; if (stat(&name[0],&stb) >= 0) goto have3; if (errno == ENOENT) { np[-1] = '\0'; retry2:; if (mkdir(&name[0],0755) >= 0) goto have2; switch (errno) { case EEXIST: np[-1] = '/'; goto retry1; break; case ENOENT: np[-4] = '\0'; if (mkdir(&name[0],0755) >= 0) goto have1; switch (errno) { case EEXIST: np[-4] = '/'; goto retry2; break; } goto cant_mkdir; have1:; np[-4] = '/'; if (mkdir(&name[0],0755) >= 0) goto have2; break; } goto cant_mkdir; have2:; np[-1] = '/'; goto have3; } cant_mkdir:; printout("[%d] can't log to %s (for %s): can't mkdir %s: %s\n",mypid(),&bsp[0],c->peertext,&name[0],strerror(errno)); return; have3:; bcopy(&bsp[0],np,12); np += 12; *np++ = '.'; serial = 1; while (1) { sprintf(np,"%d",serial++); fd = open(&name[0],O_WRONLY|O_CREAT|O_EXCL,0644); if (fd >= 0) break; if (errno == EEXIST) continue; printout("[%d] can't log to %s (for %s): %s\n",mypid(),&name[0],c->peertext,strerror(errno)); return; } write(fd,"efrom: ",7); write(fd,c->env_from,strlen(c->env_from)); write(fd,"\n",1); for (i=0;inrcpts;i++) { write(fd,"eto: ",5); write(fd,c->rcpts[i].lp,strlen(c->rcpts[i].lp)); if (c->rcpts[i].dom) { write(fd,"@",1); write(fd,c->rcpts[i].dom,strlen(c->rcpts[i].dom)); } write(fd,"\n",1); } write(fd,"\n",1); lseek(c->data_fd,0,SEEK_SET); while (1) { i = read(c->data_fd,&rbuf[0],sizeof(rbuf)); if (i < 0) { printout("[%d] read from data fd for %s: %s\n",mypid(),c->peertext,strerror(errno)); } if (i <= 0) break; j = write(fd,&rbuf[0],i); if (j < 0) { printout("[%d] write to %s, logging for %s: %s\n",mypid(),&name[0],c->peertext,strerror(errno)); break; } if (j != i) { printout("[%d] write to %s, logging for %s: wanted %d, did %d\n",mypid(),&name[0],c->peertext,i,j); break; } } if (close(fd) < 0) { printout("[%d] closing %s, logging for %s: %s\n",mypid(),&name[0],c->peertext,strerror(errno)); } printout("[%d] message from %s logged to %s\n",mypid(),c->peertext,&name[0]); fd = open("save/.list",O_WRONLY|O_APPEND|O_CREAT|O_EXLOCK,0644); if (fd < 0) { printout("[%d] open save/.list: %s\n",mypid(),strerror(errno)); } else { struct iovec iov[2]; i = strlen(&name[0]); iov[0].iov_base = &name[0]; iov[0].iov_len = i; iov[1].iov_base = deconst("\n"); iov[1].iov_len = 1; j = writev(fd,&iov[0],2); if (j < 0) { printout("[%d] write save/.list: %s\n",mypid(),strerror(errno)); } else if (j != i+1) { printout("[%d] write save/.list: wanted %d, did %d\n",mypid(),i+1,j); } close(fd); } } #endif #ifndef ERROR_AND_LOG /* * We just got the . closing a message body. Check state * and do appropriate things. */ static void body_ends(CLIENT *c) { switch (c->state) { case CS_BDATA: ioq_oq_point(&c->cq,"250 Thank you for feeding the spamtrap.\r\n",-1); log_message(c); do_rset(c); c->state = CS_HELO; break; } } #endif #ifndef ERROR_AND_LOG /* * Save the current recipient. */ static void save_rcpt(CLIENT *c) { if (c->nrcpts >= c->arcpts) c->rcpts = realloc(c->rcpts,(c->arcpts=c->nrcpts+8)*sizeof(RCPT)); c->rcpts[c->nrcpts++] = c->rcpt; c->rcpt = NORCPT; } #endif #ifndef ERROR_AND_LOG /* * Insert X-Shim-Dubious: headers if we have any reason to think the * mail is dubious. */ static void generate_dubious(CLIENT *c) { static const struct { unsigned int bit; const char *desc; } reasons[] = { { DUBIOUS_MAIL_SPACE, "MAILspace" }, { DUBIOUS_RCPT_SPACE, "RCPTspace" }, { DUBIOUS_HELO_SYNTAX, "HELOsyntax" } }; int i; if (c->dubious == 0) return; data_data(c,"X-Shim-Dubious:",-1); for (i=(sizeof(reasons)/sizeof(reasons[0]))-1;i>=0;i--) { if (c->dubious & reasons[i].bit) { data_data(c," ",1); data_data(c,reasons[i].desc,-1); } } data_data(c,"\r\n",-1); } #endif #ifndef ERROR_AND_LOG /* * Create a Received: header and treat it as if it had just been * received on the client connection (thereby inserting it before the * data the client sends). */ static void generate_received(CLIENT *c) { int e; int i; time_t nowtt; struct tm *nowtm; char *t; char hnbuf[NI_MAXHOST]; char tbuf[64]; e = getnameinfo(c->peeraddr,c->peeralen,&hnbuf[0],NI_MAXHOST,0,0,NI_NUMERICHOST); if (e) { data_data(c,"Received: from UNKNOWN (getnameinfo error: ",-1); data_data(c,strerror(e),-1); data_data(c,", sockaddr",-1); for (i=0;ipeeralen;i++) { sprintf(&tbuf[0]," %02x",((const unsigned char *)c->peeraddr)[i]); data_data(c,&tbuf[0],3); } data_data(c,")",-1); } else { data_data(c,"Received: from ",-1); data_data(c,&hnbuf[0],-1); } if (c->helo_as) { data_data(c," (helo: ",-1); t = malloc(strlen(c->helo_as)); i = 0; while (1) { int ch; ch = ((const unsigned char *)c->helo_as)[i]; if (! ch) break; if ((ch < 32) || (ch > 126) || (ch == '(') || (ch == ')')) ch = '?'; t[i] = ch; i ++; } data_data(c,t,i); data_data(c,")",-1); } data_data(c,"\r\n",-1); data_data(c,"\tby ",-1); data_data(c,&myhostname[0],-1); data_data(c," (",-1); data_data(c,c->mytext,-1); data_data(c,")\r\n\twith SMTP id ",-1); data_data(c,xid(c),-1); data_data(c,";\r\n\t",-1); time(&nowtt); nowtm = localtime(&nowtt); sprintf(&tbuf[0],"%02d %.3s %04d %02d:%02d:%02d %c%02ld%02ld (%s, ", nowtm->tm_mday, "JanFebMarAprMayJunJulAugSepOctNovDec"+(nowtm->tm_mon*3), nowtm->tm_year + 1900, nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec, (nowtm->tm_gmtoff<0)?'-':'+', labs(nowtm->tm_gmtoff)/3600, (labs(nowtm->tm_gmtoff)/60)%60, nowtm->tm_zone ); data_data(c,&tbuf[0],-1); nowtm = gmtime(&nowtt); sprintf(&tbuf[0],"%02d:%02d:%02d UTC)", nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec ); data_data(c,&tbuf[0],-1); data_data(c,"\r\n",-1); } #endif #ifndef ERROR_AND_LOG /* * Switch into a data-accepting state. This exists to centralize * things that must be done when starting to accept message data. */ static void begin_data(CLIENT *c, int datastate) { c->state = datastate; generate_dubious(c); generate_received(c); } #endif /* * Tell a client about the global sleep-before-banner state. */ static void tell_state_sbb(CLIENT *c, char sep, int val) { char *dbuf; ioq_oq_point(&c->cq,"250",-1); ioq_oq_copy(&c->cq,&sep,1); ioq_oq_point(&c->cq,"sbb ",-1); asprintf(&dbuf,"%d",val); ioq_oq_free(&c->cq,dbuf,-1); ioq_oq_point(&c->cq,"\r\n",-1); } /* * Write a log line for the session. Do this only if ERROR_AND_LOG. */ static void log_session(CLIENT *c) { #ifdef ERROR_AND_LOG char *t; t = index(c->peertext,'/'); if (! t) return; /* can this happen? */ printf("%.*s %s %lu\n", (int)(t-c->peertext), c->peertext, (c->helo_as && c->helo_as[0]) ? c->helo_as : "NO_HELO_NAME", (unsigned long int)time(0) ); #else c = c; #endif } /* * Handle an SMTP command from a client. First we perform a gross * syntax check, checking that no C0 or C1 control characters appear * in the command (arguably, we should enforce the restriction to * ASCII as well), then extract the command verb and search our table * of commands for it. If it's not there, we return an error, and, if * the client is issuing too many unrecognized commands without * recognized commands (like ratware which blats a whole transaction * at us and ignores all return codes), summarily drop the connection. * * Otherwise, update the statistics counter for this command. Then * check to see if it's a command with no arguments that was given an * argument, or one with an argument that was given none, and return * an error in those cases. Then check for a few commands (like NOOP) * that are valid and do the same thing in all states, and handle * them. Else, do a big switch on the current state and handle things * appropriately for the state. */ static void smtp_command(CLIENT *c) { int i; int cl; int al; int cmd; static int smtpcmdlens[sizeof(smtpcmds)/sizeof(smtpcmds[0])] = { -1 }; if (smtpcmdlens[0] < 0) for (i=0;smtpcmds[i];i++) smtpcmdlens[i] = strlen(smtpcmds[i]); for (i=0;illen;i++) { if ((c->lbuf[i]&0x7f) < 32) { ioq_oq_point(&c->cq,"500 Syntax error: invalid character\r\n",-1); printout("[%d] %s (octet 0x%02x): 500 Syntax error: invalid character\n",mypid(),c->iostr,c->lbuf[i]); return; } } for (i=0;(illen)&&(c->lbuf[i]!=' ');i++) ; cl = i; for (;(illen)&&(c->lbuf[i]==' ');i++) ; al = c->llen - i; for (cmd=0;smtpcmds[cmd];cmd++) if ((cl == smtpcmdlens[cmd]) && !strncasecmp(c->lbuf,smtpcmds[cmd],cl)) break; if (! smtpcmds[cmd]) { c->unrecog ++; if (c->unrecog > (c->baddata?5:25)) { ioq_oq_point(&c->cq,"500-Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); ioq_oq_point(&c->cq,"500-Come back when you learn SMTP\r\n",-1); ioq_oq_point(&c->cq,"500 Goodbye.\r\n",-1); if (c->baddata) { printout("[%d] %s: 500 Bad DATA unrecognized stream\n",mypid(),c->iostr); } else { printout("[%d] %s (%.*s): 500 Come back when you learn SMTP\n",mypid(),c->iostr,cl,c->lbuf); } c->state = CS_DEAD; } else { ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); if (! c->baddata) { printout("[%d] %s (%.*s): 500 Command unrecognized\n",mypid(),c->iostr,cl,c->lbuf); } } return; } else { if (c->unrecog > 10) c->unrecog -= 10; else c->unrecog = 0; c->baddata = 0; } STATS_RECORD(SM__CMD+cmd); switch (cmd) { case CMD_HELO: case CMD_EHLO: /* Special case - we want to accept empty HELOs even though they're a syntax violation */ break; case CMD_MAIL: case CMD_RCPT: case CMD_VRFY: if (i >= c->llen) { ioq_oq_point(&c->cq,"501 Syntax error: ",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq," requires an argument\r\n",-1); printout("[%d] %s (%.*s): 501 Requires an argument\n",mypid(),c->iostr,cl,c->lbuf); return; } break; case CMD_DATA: case CMD_RSET: case CMD_NOOP: case CMD_QUIT: if (i < c->llen) { ioq_oq_point(&c->cq,"501 Syntax error: ",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq," takes no argument\r\n",-1); printout("[%d] %s (%.*s): 501 Takes no argument\n",mypid(),c->iostr,c->llen,c->lbuf); return; } break; case CMD_XCTL: break; } switch (cmd) { case CMD_NOOP: ioq_oq_point(&c->cq,"250 Nothing done.\r\n",-1); return; break; case CMD_VRFY: ioq_oq_point(&c->cq,"553 VRFY disabled to you.\r\n",-1); printout("[%d] %s (%.*s): 553 VRFY disabled\n",mypid(),c->iostr,cl,c->lbuf); return; break; case CMD_XCTL: if (client_is_local(c)) { if ((al == 1) && (c->lbuf[i] == '?')) { tell_state_sbb(c,' ',banner_sleep); } else if ((al > 3) && !strncasecmp(c->lbuf+i,"sbb",3)) { int v; v = atoi(c->lbuf+i+3); if ((v < 0) || (v > 255)) { ioq_oq_point(&c->cq,"504 Value must be 0..255\r\n",-1); } else { banner_sleep = v; printout("[%d] banner sleep = %d [%s]\n",mypid(),v,c->peertext); tellparent(3,LM_STATE,STATE_TIMEOUT,v); ioq_oq_point(&c->cq,"250 OK\r\n",-1); } } else if ((al == 5) && !strncasecmp(c->lbuf+i,"stats",5)) { tellparent(9,LM_STATS, (int)((c->serial )&0xff), (int)((c->serial>> 8)&0xff), (int)((c->serial>>16)&0xff), (int)((c->serial>>24)&0xff), (int)((c->serial>>32)&0xff), (int)((c->serial>>40)&0xff), (int)((c->serial>>48)&0xff), (int)((c->serial>>56)&0xff)); c->statstate = CSS_ATNL; } else { ioq_oq_point(&c->cq,"501 Use `?', `log [off|rej|all]', `sbb N', or `stats'\r\n",-1); } } else { ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); printout("[%d] %s tried bad XCTL (%.*s)\n",mypid(),c->peertext,c->llen,c->lbuf); } return; break; case CMD_POST: ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' recognized as a Web-proxy-abuse signature\r\n",-1); printout("[%d] %s (%.*s): 500 tried to POST\n",mypid(),c->iostr,c->llen,c->lbuf); c->state = CS_DEAD; return; break; } /* The reason the control structure here is upside down relative to what you might conventionally expect (which would be a switch out to command handlers here, with the command handlers checking the state) is that this way the code matches the state machine description more closely. */ #define SEND250() simpleresp(0,"250 OK") switch (c->state) { static void simpleresp(int logit, const char *str) { ioq_oq_point(&c->cq,str,-1); ioq_oq_point(&c->cq,"\r\n",-1); if (logit) printout("[%d] %s (%.*s): %s\n",mypid(),c->iostr,c->llen,c->lbuf,str); } case CS_GREET: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_RSET: SEND250(); break; case CMD_QUIT: log_session(c); simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(1,"503 Must HELO/EHLO first"); break; } break; case CS_HELO: free(c->xid); c->xid = 0; switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); break; case CMD_MAIL: #ifdef ERROR_AND_LOG ioq_oq_point(&c->cq,error_code,-1); ioq_oq_point(&c->cq," Internal error\r\n",-1); printout("[%d] %s (%.*s): %s-rejecting MAIL\n",mypid(),c->iostr,c->llen,c->lbuf,error_code); #else if (! do_mail(c,c->lbuf+i,al)) { SEND250(); c->state = CS_MAIL; } #endif break; case CMD_RSET: SEND250(); break; case CMD_QUIT: log_session(c); simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(1,"503 Must MAIL first"); break; } break; #ifndef ERROR_AND_LOG case CS_MAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_DATA: simpleresp(1,"503 Need an accepted RCPT first"); c->baddata = 1; break; case CMD_RSET: do_rset(c); SEND250(); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { STATS_RECORD(SM_ADDR); save_rcpt(c); SEND250(); c->state = CS_FAIL; } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; case CS_FAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_DATA: simpleresp(0,"354 OK"); begin_data(c,CS_BDATA); break; case CMD_RSET: do_rset(c); SEND250(); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { save_rcpt(c); SEND250(); break; } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; #endif default: panic("impossible state %d",c->state); break; } #undef SEND250 } /* * Return the time in seconds from the client's starttime to now. */ static double sbb_delay(CLIENT *c) { struct timeval now; gettimeofday(&now,0); if (now.tv_usec >= c->starttime.tv_usec) { return( (now.tv_sec - c->starttime.tv_sec) + ((now.tv_usec - c->starttime.tv_usec) * 1e-6) ); } else { return( (now.tv_sec - 1 - c->starttime.tv_sec) + ((now.tv_usec + 1000000 - c->starttime.tv_usec) * 1e-6) ); } } /* * Process an input line. Where it's input from depends on the state. */ static void client_process_line(CLIENT *c) { if ((c->llen < 2) || (c->lbuf[c->llen-1] != '\n') || (c->lbuf[c->llen-2] != '\r')) abort(); c->llen -= 2; c->lbuf[c->llen] = '\0'; switch (c->state) { case CS_SBB: printout("[%d] %s sent data before banner, delay %.6f (%.*s%s)\n",mypid(),c->peertext,sbb_delay(c),(c->llen<32)?c->llen:32,c->lbuf,(c->llen<32)?"":"..."); mlog("%s: SMTP <<< %.*s\n",c->peertext,c->llen,c->lbuf); ioq_oq_point(&c->cq,"500 SMTP dialog phase error\r\n",-1); set_helo_as(c,"**PREBANNER",-1); log_session(c); c->state = CS_DEAD; break; case CS_GREET: case CS_HELO: #ifndef ERROR_AND_LOG case CS_MAIL: case CS_FAIL: #endif mlog("%s: SMTP <<< %.*s\n",c->peertext,c->llen,c->lbuf); smtp_command(c); break; default: nonfatal("client_process_line: state %d",c->state); c->state = CS_DEAD; break; } c->llen = 0; } /* * Accept input that's already in the client's read buffer. Crank * through the terminator state machine appropriate to the client's * state, and, when done, either switch state (for DATA) or handle an * input line (for others). */ static void check_buffered_in(CLIENT *c) { int i; IOQ *q; int ts; q = c->ioq; switch (c->state) { #ifndef ERROR_AND_LOG case CS_BDATA: ts = c->termstate; for (i=q->ibptr;iibfill;i++) { switch (ts) { case 0: /* after CRLF */ switch (q->ibuf[i]) { case '.': ts = 1; break; case '\r': ts = 4; break; default: ts = 3; break; } break; case 1: /* after CRLF dot */ ts = (q->ibuf[i] == '\r') ? 2 : 3; break; case 2: /* after CRLF dot CR */ if (q->ibuf[i] == '\n') { i ++; data_data(c,&q->ibuf[q->ibptr],i-q->ibptr); q->ibptr = i; c->termstate = 0; STATS_RECORD(SM_BODY); body_ends(c); return; } ts = 4; break; case 3: /* middle of line, after nothing special */ if (q->ibuf[i] == '\r') ts = 4; break; case 4: /* after CR */ switch (q->ibuf[i]) { case '\n': ts = 0; break; case '\r': break; default: ts = 3; break; } break; default: abort(); break; } } c->termstate = ts; data_data(c,&q->ibuf[q->ibptr],i-q->ibptr); q->ibptr = i; break; #endif default: ts = c->termstate; for (i=q->ibptr;iibfill;i++) { if (ts) { if (q->ibuf[i] == '\n') { i ++; if (iline_append(c,&q->ibuf[q->ibptr],i-q->ibptr)) { c->state = CS_DEAD; return; } q->ibptr = i; c->termstate = 0; client_process_line(c); return; } ts = 0; } else { if (q->ibuf[i] == '\r') ts = 1; } } if (iline_append(c,&q->ibuf[q->ibptr],i-q->ibptr)) { c->state = CS_DEAD; return; } c->termstate = ts; break; } } /* * The client's input fd - whichever one that is - has become readable. * Read input. (Don't bother actually processing it; let * check_buffered_in handle that next time around the loop.) */ static void check_poll_in(CLIENT *c) { int e; if (pfds[c->px].revents) { e = ioq_read(c->ioq); if (e < 0) { /* EOF */ if (c->state == CS_SBB) printout("[%d] %s dropped connection before banner, delay %.6f\n",mypid(),c->peertext,sbb_delay(c)); log_session(c); c->state = CS_DEAD; } else if (e) { /* error; ECONNRESET isn't worth mentioning, but otherwise... */ if (errno != ECONNRESET) { nonfatal("read error [%s]: %s",c->peertext,strerror(errno)); } log_session(c); c->state = CS_DEAD; } else { fresh_timeout(c); } } } /* * Close the file descriptors for all LSOCKs. Used by SMTP worksers * when the parent dies, to release port 25. (We don't actually need * to set the fd struct elements to anything invalid; the setting of * ownstate to LS_DYING carrise the information that we shouldn't do * anything with them. But it's a cheap way to ensure that even if we * do weird out and try to access them, we don't end up scribbling on * something else by mistake.) */ static void close_accs(void) { LSOCK *a; for (a=accs;a;a=a->link) { close(a->fd); a->fd = -1; } } /* * Initialize a MSG before first use. */ static void msg_init(MSG *m) { m->len = -1; } /* * Process an incoming byte for a MSG. When a complete message is * accumulated, calls (*done)() and may take further action (see * below); on error, calls (*err)() with the message. err may throw * out; done must not. done's return value indicates what msg_input * should do after calling it: * MI_RESET * Reset for the next message as normal. * MI_CONTINUE * Any necessary resetting for more input has been done * already; just continue accumulating input. */ #define MI_RESET 1 #define MI_CONTINUE 2 static void msg_input(MSG *m, unsigned char b, int (*done)(void), void (*err)(const char *, ...)) { if (m->len < 0) { if ((b < 1) || (b > MAX_LM_LEN)) { (*err)("msglen %d",b); return; } m->len = b; m->fill = 0; } else { m->buf[m->fill++] = b; if (m->fill >= m->len) { switch ((*done)()) { case MI_RESET: m->len = -1; break; case MI_CONTINUE: break; default: abort(); break; } } } } /* * Return a completed message's length. To be called only from within * a done function called by msg_input(). */ static int msg_len(MSG *m) { return(m->len); } /* * Return one of a completed message's bytes. To be called only from * within a done function called by msg_input(). */ static unsigned char msg_byte(MSG *m, int n) { return(m->buf[n]); } /* * Some data from the response to LM_STATS has been received. Handle * it. (The whole message is passed in so we can easily get the * serial number of the relevant client.) */ static void stats_response_data(MSG *m) { unsigned long long int serial; CLIENT *c; int i; int i0; unsigned int ch; serial = (m->buf[1] * 0x0000000000000001ULL) | (m->buf[2] * 0x0000000000000100ULL) | (m->buf[3] * 0x0000000000010000ULL) | (m->buf[4] * 0x0000000001000000ULL) | (m->buf[5] * 0x0000000100000000ULL) | (m->buf[6] * 0x0000010000000000ULL) | (m->buf[7] * 0x0001000000000000ULL) | (m->buf[8] * 0x0100000000000000ULL); for (c=clients;c;c=c->link) { if (c->serial != serial) continue; i0 = -1; for (i=0;ibuf[9];i++) { ch = m->buf[10+i]; if (i0 < 0) i0 = i; if (ch == '\n') { if (c->statstate & CSS_ATNL) ioq_oq_point(&c->cq,"250-",-1); if (i > i0) ioq_oq_copy(&c->cq,&m->buf[10+i0],i-i0); ioq_oq_point(&c->cq,"\r\n",-1); i0 = -1; c->statstate |= CSS_ATNL; } } if (i0 >= 0) { if (c->statstate & CSS_ATNL) ioq_oq_point(&c->cq,"250-",-1); if (i > i0) ioq_oq_copy(&c->cq,&m->buf[10+i0],i-i0); c->statstate &= ~CSS_ATNL; } } } /* * The end-of-data marker from the response to LM_STATS has been * received. Handle it. (See stats_resposne_data, above, for more.) */ static void stats_response_done(MSG *m) { unsigned long long int serial; CLIENT *c; serial = (m->buf[1] * 0x0000000000000001ULL) | (m->buf[2] * 0x0000000000000100ULL) | (m->buf[3] * 0x0000000000010000ULL) | (m->buf[4] * 0x0000000001000000ULL) | (m->buf[5] * 0x0000000100000000ULL) | (m->buf[6] * 0x0000010000000000ULL) | (m->buf[7] * 0x0001000000000000ULL) | (m->buf[8] * 0x0100000000000000ULL); for (c=clients;c;c=c->link) { if (c->serial != serial) continue; if (! (c->statstate & CSS_ATNL)) ioq_oq_point(&c->cq,"\r\n",-1); ioq_oq_point(&c->cq,"250 Done.\r\n",-1); } } /* * Read from the parent pipe in an SMTP worker. */ static void parent_read(void) { int i; int r; unsigned char rbuf[64]; r = read(parentpipe,&rbuf[0],sizeof(rbuf)); if (r < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } fatal("parent pipe read: %s",strerror(errno)); } if (r == 0) { ownstate = LS_DYING; close(parentpipe); close_accs(); return; } for (i=0;i= the second. */ static unsigned int tvsub_ms(struct timeval a, struct timeval b) { if (a.tv_usec >= b.tv_usec) { return(((a.tv_sec-b.tv_sec)*1000)+((a.tv_usec-b.tv_usec)/1000)+1); } else { return(((a.tv_sec-b.tv_sec-1)*1000)+((a.tv_usec+1000000-b.tv_usec)/1000)+1); } } /* * A stdio wrapper which prefixes each line with mypid(), [%d] style. */ /* BEGIN pidprefix wrapper internals */ typedef struct pidprefix_priv PIDPREFIX_PRIV; struct pidprefix_priv { FILE *sub; int atbol; } ; static int pidprefix_w(void *pv, const char *buf, int len) { PIDPREFIX_PRIV *p; int i; p = pv; for (i=0;iatbol) fprintf(p->sub,"[%d] ",mypid()); putc(buf[i],p->sub); p->atbol = (buf[i] == '\n'); } return(len); } static int pidprefix_c(void *pv) { free(pv); return(0); } /* END pidprefix wrapper internals */ static FILE *fopen_pidprefix(FILE *sub) { PIDPREFIX_PRIV *p; p = malloc(sizeof(*p)); p->sub = sub; p->atbol = 1; return(funopen(p,0,&pidprefix_w,0,&pidprefix_c)); } /* BEGIN packetized wrapper internals */ static int packetized_w(void *pv, const char *buf, int len0) { int len; unsigned char n; struct iovec iov[2]; len = len0; while (len > 0) { /* The max value here is 128 so that the pseudo-messages involved are never over 255 bytes long. This really doesn't matter, since the pseudo-messages don't really have length bytes, but it lets us keep MAX_LM_LEN below 255, which avoids a compiler warning when comparing an unsigned char to it in msg_input. */ n = (len > 128) ? 128 : len; iov[0].iov_base = &n; iov[0].iov_len = 1; iov[1].iov_base = deconst(buf); iov[1].iov_len = n; writev(*(int *)pv,&iov[0],2); len -= n; buf += n; } return(len0); } static int packetized_c(void *pv) { write(*(int *)pv,"",1); free(pv); return(0); } /* END packetized wrapper internals */ static FILE *fopen_packetized(int fd) { int *v; v = malloc(sizeof(int)); *v = fd; return(funopen(v,0,&packetized_w,0,&packetized_c)); } /* * The SMTP worker main loop. This is called repeatedly by an SMTP * worker. Nothing particularly notable here. Update our process * title if it's changed. Walk the list of clients, checking each * one's state and adding the appropriate stuff to the poll vector. * Possibly shift state based on client count. Do the poll and handle * any I/O it indicates is possible. */ static void listener_tick(void) { LSOCK *a; int n; int nclients; CLIENT *c; CLIENT **cp; IOQ *q; int timeout; struct timeval now; unsigned long int tmo; if (titlestate != ownstate) { switch (ownstate) { case LS_LISTENING: setproctitle("SMTP worker (listening)"); break; case LS_FULL: setproctitle("SMTP worker (full)"); break; case LS_STOPPING: setproctitle("SMTP worker (stopping)"); break; case LS_DYING: setproctitle("SMTP worker (dying)"); break; default: setproctitle("SMTP worker (%d?)",ownstate); break; } titlestate = ownstate; } fflush(0); if (got_usr1) { FILE *f; got_usr1 = 0; f = fopen_pidprefix( #ifdef ERROR_AND_LOG stderr #else stdout #endif ); dump_listener_state(f); fclose(f); return; } timeout = INFTIM; gettimeofday(&now,0); npfds = 0; if (ownstate != LS_DYING) pppx = add_pfd(parentpipe,POLLIN|POLLRDNORM); nclients = 0; for (cp=&clients;(c=*cp);) { if (c->timeout && (c->timeout < now.tv_sec)) c->state = CS_BURIED; switch (c->state) { case CS_SBB: case CS_GREET: case CS_HELO: #ifndef ERROR_AND_LOG case CS_MAIL: case CS_BDATA: case CS_FAIL: #endif if (c->rcpt.lp || c->rcpt.dom) panic("recipient in state %s",clientstatename(c->state)); break; case CS_DEAD: case CS_BURIED: break; default: panic("impossible state %d",c->state); break; } q = &c->cq; switch (c->state) { case CS_SBB: if (tvle(c->banner_at,now)) { c->state = CS_GREET; send_greeting(c); } else { tmo = tvsub_ms(c->banner_at,now); if ((timeout == INFTIM) || (tmo < timeout)) timeout = tmo; } break; case CS_GREET: case CS_HELO: #ifndef ERROR_AND_LOG case CS_MAIL: case CS_FAIL: case CS_BDATA: #endif break; default: fatal("listener_tick: state %d",c->state); case CS_DEAD: if (c->cq.q) break; /* fall through */ case CS_BURIED: do_rset(c); *cp = c->link; free(c->lbuf); free(c->peeraddr); free(c->peertext); free(c->helo_as); /* env_from done by do_rset above */ free_rcpt(c->rcpt); if (c->data_fd >= 0) close(c->data_fd); free(c->data_file); /* rcpts themselves done by do_rset above, but not rcpts vector */ free(c->rcpts); free(c->xid); close(c->cq.fd); ioq_flush(&c->cq); free(c); continue; break; } nclients ++; if (q) { c->ioq = q; c->iostr = c->peertext; if (q->q) { c->checkfn = &check_poll_out; c->px = add_pfd(q->fd,POLLOUT|POLLWRNORM); } else if (q->ibptr < q->ibfill) { c->checkfn = &check_buffered_in; timeout = 0; } else { c->checkfn = &check_poll_in; c->px = add_pfd(q->fd,POLLIN|POLLRDNORM); if (c->timeout) { if (c->timeout < now.tv_sec) panic("impossible c->timeout 1"); if (c->timeout-now.tv_sec > 10000) panic("impossible c->timeout 2"); tmo = (c->timeout + 1 - now.tv_sec) * 1000; if (tmo > 0x70000000) panic("impossible tmo"); if ((timeout == INFTIM) || (tmo < timeout)) timeout = tmo; } } } cp = &c->link; } switch (ownstate) { case LS_LISTENING: if (nclients >= CLIENTS_PER_PROCESS) { ownstate = LS_FULL; tellparent(1,LM_FULL); } else if (nclients != toldclients) { tellparent(2,LM_LISTENING,CLIENTS_PER_PROCESS-nclients); toldclients = nclients; } break; case LS_FULL: if (nclients < CLIENTS_PER_PROCESS) { ownstate = LS_LISTENING; tellparent(2,LM_LISTENING,CLIENTS_PER_PROCESS-nclients); toldclients = nclients; } break; case LS_STOPPING: if (nclients < 1) { tellparent(1,LM_DYING); exit(0); } break; case LS_DYING: if (nclients < 1) exit(0); break; default: abort(); break; } if (ownstate == LS_LISTENING) { for (a=accs;a;a=a->link) a->px = add_pfd(a->fd,POLLIN|POLLRDNORM); } if ((timeout != INFTIM) && ((timeout < 0) || (timeout > 100000000))) panic("impossible timeout"); n = poll(pfds,npfds,timeout); if (n < 0) { if (errno == EINTR) return; fatal("poll: %s",strerror(errno)); exit(1); } /* Do clients before accs - accept_acc can add a new client, and if so, it won't have a checkfn set up yet! Similarly do accs before parentpipe, since reading from parentpipe can change ownstate. */ for (c=clients;c;c=c->link) (*c->checkfn)(c); if (ownstate == LS_LISTENING) for (a=accs;a;a=a->link) if (pfds[a->px].revents) accept_acc(a); if ((ownstate != LS_DYING) && pfds[pppx].revents) parent_read(); } /* * Called by the parent to fork a new SMTP worker. */ static int make_new_listener(void) { pid_t kid; int p[2]; LISTENER *l; if (socketpair(AF_LOCAL,SOCK_STREAM,0,p) < 0) fatal("socketpair: %s",strerror(errno)); set_nonblock(p[0]); set_nonblock(p[1]); kid = fork(); if (kid < 0) fatal("fork: %s",strerror(errno)); if (kid == 0) { cached_pid = -1; while (listeners) { l = listeners; listeners = l->link; close(l->pipe); free(l); } close(p[0]); parentpipe = p[1]; ownstate = LS_LISTENING; titlestate = LS_DEAD; toldclients = -1; client_serial = 0x0123456789abcdefULL; msg_init(&parentmsg); return(1); } close(p[1]); l = malloc(sizeof(LISTENER)); l->proc = kid; l->pipe = p[0]; l->state = LS_LISTENING; l->fcs = CLIENTS_PER_PROCESS; msg_init(&l->msg); l->link = listeners; listeners = l; printout("[%d] forked new listener %d\n",mypid(),(int)kid); return(0); } /* * Send a message to a listener. We deliberately ignore write errors * here; the only plausible error is EPIPE, and simply dropping the * message is the appropriate thing to do. */ static void tell_listener(LISTENER *l, int nb, ...) { va_list ap; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tell_listener: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;ipipe,&buf[0],nb+1); } /* * The equivalent of tell_listener for all listeners. */ static void tell_all_listeners(int nb, ...) { va_list ap; LISTENER *l; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tell_listener: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;ilink) write(l->pipe,&buf[0],nb+1); } /* * Called by the parent to tell an SMTP worker to go away. Just find a * LISTENING worker and send it an LM_STOP message. (We go to the * trouble to find the _oldest_ LISTENING worker just to keep rolling * processes over rather than creating and destroying some while * others last forever.) */ static void stop_a_listener(void) { LISTENER *l; LISTENER *ll; ll = 0; for (l=listeners;l;l=l->link) if (l->state == LS_LISTENING) ll = l; if (ll) { printout("[%d] stopping listener %d\n",mypid(),(int)ll->proc); tell_listener(ll,1,LM_STOP); ll->state = LS_STOPPING; } } /* * Parent read from an SMTP worker. Track state changes and accumulate * stats. */ static void listener_read(LISTENER *l) { __label__ _ret; unsigned char rbuf[64]; int r; int i; auto void killoff(const char *, ...) __attribute__((__format__(__printf__,1,2))); auto void killoff(const char *fmt, ...) { va_list ap; FILE *f; f = msgopen(); fprintf(f,"listener %d: ",(int)l->proc); va_start(ap,fmt); vfprintf(f,fmt,ap); va_end(ap); fclose(f); l->state = LS_DEAD; goto _ret; } r = read(l->pipe,&rbuf[0],sizeof(rbuf)); if (r == 0) killoff("EOF"); if (r < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return; break; } killoff("read: %s",strerror(errno)); } for (i=0;imsg,0)) { default: killoff("message %02x?",msg_byte(&l->msg,0)); break; case LM_FULL: if (msg_len(&l->msg) != 1) killoff("LM_FULL message len %d?",msg_len(&l->msg)); l->state = LS_FULL; break; case LM_LISTENING: if (msg_len(&l->msg) != 2) killoff("LM_LISTENING message len %d?",msg_len(&l->msg)); l->state = LS_LISTENING; l->fcs = msg_byte(&l->msg,1); break; case LM_DYING: if (msg_len(&l->msg) != 1) killoff("LM_DYING message len %d?",msg_len(&l->msg)); l->state = LS_DEAD; return(MI_RESET); break; case LM_STATE: if (msg_len(&l->msg) < 2) killoff("LM_STATE message len %d?",msg_len(&l->msg)); switch (msg_byte(&l->msg,1)) { case STATE_TIMEOUT: if (msg_len(&l->msg) != 3) killoff("LM_STATE STATE_TIMEOUT message len %d?",msg_len(&l->msg)); banner_sleep = msg_byte(&l->msg,2); tell_all_listeners(3,LM_STATE,STATE_TIMEOUT,banner_sleep); break; default: killoff("unknown state change %02x",msg_byte(&l->msg,1)); break; } return(MI_RESET); break; case LM_STATS: { FILE *f; FILE *g; tell_listener(l,9,LM_STATS, msg_byte(&l->msg,1), msg_byte(&l->msg,2), msg_byte(&l->msg,3), msg_byte(&l->msg,4), msg_byte(&l->msg,5), msg_byte(&l->msg,6), msg_byte(&l->msg,7), msg_byte(&l->msg,8) ); f = fopen_packetized(l->pipe); g = fopen_pidprefix(f); dump_parent_state(g); fclose(g); fclose(f); } break; #ifdef DOSTATS case SM_FIRST_STATS ... SM_LAST_STATS: if (msg_len(&l->msg) != 1) killoff("%d message len %d?",msg_byte(&l->msg,0),msg_len(&l->msg)); counters[rbuf[i]-SM_FIRST_STATS] ++; break; #endif } return(MI_RESET); } msg_input(&l->msg,rbuf[i],&done,&killoff); } _ret:; } /* * Called by the parent when the time passes parent_timer. This is * here to prune unnecessary SMTP listeners: if we have had at least * two listeners' worth of free client slots over the whole of the * last interval, we tell one to go away. */ static void parent_timeout(void) { parent_timer += PARENT_TIMER_INC; if (client_minfree >= 2*CLIENTS_PER_PROCESS) stop_a_listener(); client_minfree = CLIENTS_PER_PROCESS * 10; } /* * The parent process's main loop. Check for SIGUSR1, reap dead * children, build a list of SMTP workers, do the poll...nothing * particularly noteworthy. */ static int parent_tick(void) { LISTENER *l; LISTENER **lp; int fcs; int n; struct timeval now; fflush(0); if (got_usr1) { FILE *f; got_usr1 = 0; f = fopen_pidprefix( #ifdef ERROR_AND_LOG stderr #else stdout #endif ); dump_parent_state(f); fclose(f); return(0); } while (1) { pid_t dead; int status; dead = wait3(&status,WNOHANG,0); if (dead <= 0) break; if (WIFEXITED(status)) { if (WEXITSTATUS(status) == 0) { printout("[%d] child %d done\n",mypid(),(int)dead); } else { nonfatal("child %d: exit %d",(int)dead,WEXITSTATUS(status)); } } else if (WIFSIGNALED(status)) { nonfatal("child %d: %s%s",(int)dead,strsignal(WTERMSIG(status)),WCOREDUMP(status)?" (core dumped)":""); } else { nonfatal("child %d: undecodable status %#x",(int)dead,status); } } gettimeofday(&now,0); if (now.tv_sec >= parent_timer) { parent_timeout(); return(0); } npfds = 0; fcs = 0; for (lp=&listeners;(l=*lp);) { switch (l->state) { case LS_LISTENING: fcs += l->fcs; break; case LS_DEAD: close(l->pipe); *lp = l->link; free(l); continue; break; } l->px = add_pfd(l->pipe,POLLIN|POLLRDNORM); lp = &l->link; } if (fcs < MIN_CLIENT_SLOTS) return(make_new_listener()); if (fcs < client_minfree) client_minfree = fcs; n = poll(pfds,npfds,((parent_timer-now.tv_sec)*1000)+100-(now.tv_usec/1000)); if (n < 0) { if (errno == EINTR) return(0); fatal("poll: %s",strerror(errno)); } if (n == 0) return(0); for (l=listeners;l;l=l->link) if (pfds[l->px].revents) listener_read(l); return(0); } /* * Initialize global parent state. */ static void parent_init(void) { struct timeval tv; client_minfree = 0; gettimeofday(&tv,0); parent_timer = tv.tv_sec + PARENT_TIMER_INC - (tv.tv_sec % PARENT_TIMER_INC); banner_sleep = DEF_SLEEP; } #ifdef ERROR_AND_LOG /* * Set up stuff specific to ERROR_AND_LOG mode. */ static void setup_error(void) { char *t; asprintf(&t,"%03d",ERROR_AND_LOG); error_code = t; } #endif /* * main(). Fairly boring. The only thing of note here is that all * processes' main loops are here. This is an attempt to keep the * stack clean, largely for aesthetics. */ int main(void); int main(void) { setup_usr1(); ignore_pipe(); stats_init(); #ifdef ERROR_AND_LOG setup_error(); #endif gethostname(&myhostname[0],sizeof(myhostname)); printout("[%d] startup\n",mypid()); listeners = 0; setup_accs(); parent_init(); setproctitle("parent"); while (! parent_tick()) ; while (1) listener_tick() ; }