/* SmAccessD (c) 2000 Stefan Richter * * S*ndmail Access Database Update Daemon v1.0 * * This daemon updates the access database of sendmail[TM] version 8.9.x * or higher whenever a remote POP client logs in. This allows for "SMTP * after POP" (or "POP before SMTP" or "POP authenticated relaying"). * After a certain time, POP client addresses are removed from access_db. * You need a POP daemon that writes client addresses into a named pipe, * e.g. a patched qpopper of Qualcomm, Inc. A patch is available at * http://www.bauwesen.tu-cottbus.de/~richtest/smaccessd/. * * Edit smaccesd.h to suit your site's configuration. * * DISCLAIMER: Redistribution and use of this software in source and * binary forms, with or without modification, are permitted. The * program is provided "as is" without warranty of any kind, either * expressed or implied, including, but not limited to, the implied * warranties of merchantability and fitness for a particular purpose. * The entire risk as to the quality and performance of the program is * with you. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "smaccessd.h" /* some other makemap arguments */ #define MAKEMAP_NAME "makemap" #define MAKEMAP_AUGMENT "-o" #define MAKEMAP_REPLACE "-r" /* read buffer for addresses from FIFO */ #define ADDRESS_READBUFFER 100 /* return code of InetAddr() on failure */ #define INVALID_ADDRESS 0xffffffff /* permission flags of named pipe */ #ifdef WORLDWRITEABLE_FIFO #define FIFO_PERMISSION 0622 #else #define FIFO_PERMISSION 0620 #endif void ParentSignalHandler(int signum); void ChildSignalHandler(int signum); void Report(int pri, const char *format, const char *other, int err); void DiscardText(void); int RemoveFiles(void); int CheckFiles(void); int ReadAccessText(void); int WriteAccessMap(int augment, int clear); void EndlessLoop(void); int InetNtoA(char *str, unsigned long int a0); unsigned long int InetAddr(const char *str); /* buffer for ETC_MAIL_ACCESSTXT file */ char *accesstext; size_t accesstext_len; /* file descriptor of named pipe, -1 if not open */ int fifo_des=-1; /* list of addresses/masks to exclude */ const char *exclude_s[]={ EXCLUDE_ADDRESSES }; /* same in binary form */ unsigned long int exclude_a[sizeof(exclude_s)/sizeof(*exclude_s)]; /* recent POP clients */ typedef struct { /* client address */ unsigned long int a; /* counter of cycles since client logged in the last time */ unsigned char t; /* counter of cycles since address has been reported to syslog * * (0xFF means "not yet written to database nor to syslog") */ unsigned char l; } CACHE_TYPE; CACHE_TYPE cache[CACHE_ENTRIES]; /* maximal value that counters in type CACHE_TYPE can hold */ #define MAX_COUNT 255 /* 0 means connected to parent's tty */ int daemon_detached; /* effective user and group id */ gid_t daemon_euid, daemon_egid; /* process ID of daemon */ pid_t daemon_pid; /* process ID of a running makemap child process (most of the time 0) */ pid_t makemap_pid; /* last modification of access text */ time_t access_mtime; int main(int argc, char **argv) { int i, fail; FILE *handle; /* is another smaccessd running? */ handle = fopen(VAR_RUN__PID, "r"); if (handle) { if (fscanf(handle, "%d", &i)==1) { /* let him die,... */ if (!kill(i, SIGTERM)) { sleep(3); /* ...die, die! */ kill(i, SIGKILL); } } fclose(handle); } /* spawn background process */ daemon_pid = fork(); switch (daemon_pid) { case -1: /*** Error ***/ Report(1, "Cannot fork: %s", NULL, 1); break; case 0: /*** Child ***/ /* whoami */ daemon_euid = geteuid(); daemon_egid = getegid(); daemon_pid = getpid(); /* don't exit if a pipe breaks */ signal(SIGPIPE, SIG_IGN); /* SIGUSR2 is for testing */ signal(SIGUSR2, SIG_IGN); /* default file creation permissions mask */ umask(DEFAULT_UMASK); /* hello world, that's me */ handle = fopen(VAR_RUN__PID, "w"); fail = handle==NULL; if (handle) { fail = fprintf(handle, "%d\n", (int)daemon_pid)<2; fail |= fclose(handle); } if (fail) Report(1, "Cannot write %s: %s", VAR_RUN__PID, 1); /* build binary representation of exclude list */ for (i=0; i*argv; --ident) if (*ident=='/') {++ident; break;} openlog(ident, 0, SYSLOG_FACILITY); } /* disconnect from standard i/o (detach from tty) */ daemon_detached = 1; close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); signal(SIGTERM, ChildSignalHandler); /* clean up and exit */ signal(SIGHUP, ChildSignalHandler); /* reconfigure */ /* everything OK, terminate parent */ if (kill(getppid(), SIGUSR1)) { Report(1, "Cannot kill -USR1 parent: %s", NULL, 1); RemoveFiles(); break; } Report(0, "started with PID %d", (char *)daemon_pid, 0); EndlessLoop(); default: /*** Parent ***/ /* signal SIGUSR1 == child succeeded */ signal(SIGUSR1, ParentSignalHandler); /* wait for either the child terminating or a signal incoming */ if (wait(NULL)==daemon_pid) Report(1, "Background process died", NULL, 0); else Report(1, "No receipt from background process", NULL, 0); } exit(EXIT_FAILURE); } void ParentSignalHandler(int signum) { exit(EXIT_SUCCESS); } void ChildSignalHandler(int signum) { /* block SIGHUP during signal processing */ signal(SIGHUP, SIG_IGN); /* clean up end exit */ if (signum==SIGTERM) { int fail; if (makemap_pid) kill(makemap_pid, SIGTERM); fail = WriteAccessMap(0,1); fail |= RemoveFiles(); Report(0, "Exit after SIGTERM", NULL, 0); exit(fail ? EXIT_FAILURE : EXIT_SUCCESS); } /* reconfigure */ access_mtime = 0; CheckFiles(); ReadAccessText(); WriteAccessMap(0,0); signal(SIGHUP, ChildSignalHandler); } void Report(int pri, const char *format, const char *other, int err) { const char *errst = err?strerror(errno):NULL; if (!other) other = errst; if (daemon_detached) { syslog(pri?SYSLOG_ERROR:SYSLOG_INFO, format, other, errst); } else { FILE *handle = pri?stderr:stdout; fprintf(handle, format, other, errst); fputs(".\n", handle); } } void DiscardText(void) { free(accesstext); accesstext = NULL; accesstext_len = 0; } int RemoveFiles(void) { /* binary OR because of lazy execution of logic OR */ return unlink(VAR_RUN__FIFO) | unlink(VAR_RUN__PID); } int CheckFiles() { struct stat statbuf; FILE *handle; int i, j; i = stat(VAR_RUN__FIFO, &statbuf); /* fifo cannot be stat'ed? */ if (i) { if (errno!=ENOENT) { Report(1, "Cannot stat %s: %s", VAR_RUN__FIFO, 1); return 1; } /* is FIFO no FIFO or has it wrong permissions or owner? */ } else { if ((statbuf.st_mode & 0777) != FIFO_PERMISSION || !S_ISFIFO(statbuf.st_mode) || statbuf.st_uid != daemon_euid || statbuf.st_gid != daemon_egid ) { if (fifo_des!=-1) {close(fifo_des); fifo_des = -1;} if (unlink(VAR_RUN__FIFO)) { Report(1, "Cannot unlink %s: %s", VAR_RUN__FIFO, 1); return 1; } i = 1; } } /* create a new FIFO */ if (i) { if (fifo_des!=-1) {close(fifo_des); fifo_des = -1;} umask(0); if (mknod(VAR_RUN__FIFO, S_IFIFO|FIFO_PERMISSION, 0)) { umask(DEFAULT_UMASK); Report(1, "Cannot create %s: %s", VAR_RUN__FIFO, 1); return 1; } umask(DEFAULT_UMASK); } /* open FIFO */ if (fifo_des==-1) { fifo_des = open(VAR_RUN__FIFO, O_RDWR|O_NONBLOCK|O_TRUNC); if (fifo_des==-1) { Report(1, "Cannot open %s: %s", VAR_RUN__FIFO, 1); return 1; } } /* check PID file */ j = 0; handle = fopen(VAR_RUN__PID, "r"); if (handle) { i = fscanf(handle, "%d", &j); fclose(handle); } if (j==0 || i==0) { Report(1, "Error reading %s", VAR_RUN__PID, 0); return 1; } /* Is j not my PID? If so, is it a plausible PID? * * Can this process be sent a SIGUSR2? If not, does this process exist? */ if (j!=daemon_pid && j>1 && (!kill(j,SIGUSR2) || errno!=ESRCH)) { Report(1, "Is another daemon running (PID %d)? Exiting NOW." "Access database may be corrupted.", (char *)j, 0); exit(EXIT_FAILURE); } return 0; } int ReadAccessText() { struct stat statbuf; FILE *handle; if (stat(ETC_MAIL_ACCESSTXT, &statbuf)) { Report(0, "Cannot stat %s: %s", ETC_MAIL_ACCESSTXT, 1); return 1; } /* return if file seems to be the same old */ if (statbuf.st_mtime==access_mtime) return 0; access_mtime = statbuf.st_mtime; DiscardText(); /* empty file */ if (!statbuf.st_size) return 0; accesstext = malloc(statbuf.st_size+1); if (!accesstext) { Report(1, "Out of heap memory", NULL, 0); return 1; } handle = fopen(ETC_MAIL_ACCESSTXT, "r"); if (!handle) { Report(0, "Cannot read %s: %s", ETC_MAIL_ACCESSTXT, 1); DiscardText(); return 1; } accesstext_len = fread(accesstext, 1, statbuf.st_size, handle); if (ferror(handle)) { Report(0, "Error reading %s", ETC_MAIL_ACCESSTXT, 0); DiscardText(); fclose(handle); return 1; } fclose(handle); return 0; } int WriteAccessMap(int augment, int clear) { int pipedes[2], i, fail; /* set up an anonymous pipe to connect makemap with */ if (pipe(pipedes)) { Report(1, "Cannot create pipe: %s", NULL, 1); return 1; } /* spawn a child process */ makemap_pid = fork(); switch (makemap_pid) { case -1: /*** Error ***/ Report(1, "Cannot fork: %s", NULL, 1); close(pipedes[0]); close(pipedes[1]); return 1; case 0: /*** Child ***/ /* write end of pipe not needed */ close(pipedes[1]); /* hide FIFO from makemap */ if (fifo_des!=-1) close(fifo_des); /* read end of pipe must become stdin */ if (*pipedes!=STDIN_FILENO) { if (dup2(*pipedes, STDIN_FILENO)==-1) { Report(1, "Cannot set up I/O for %s: %s", MAKEMAP_NAME, 1); exit(EXIT_FAILURE); } close(*pipedes); } /* run makemap */ { char *argv[] = { MAKEMAP_NAME, NULL, MAKEMAP_REPLACE, MAKEMAP_MAPTYPE, ETC_MAIL_ACCESS, NULL}; argv[1] = augment ? MAKEMAP_AUGMENT : MAKEMAP_NAME; execv(USR_SBIN_MAKEMAP, augment ? argv : argv+1); } Report(1, "Cannot execute %s: %s", USR_SBIN_MAKEMAP, 1); exit(EXIT_FAILURE); } /* release read end of pipe */ close(*pipedes); fail = 0; /* write access_db clear text source */ if (!augment && accesstext_len) fail = write(pipedes[1],accesstext,accesstext_len) < accesstext_len; /* write POP login entries */ if (!clear) { char str[16]; int sln; write(pipedes[1], "\n", 1); for (i=0; i(TIME_IN_CACHE)) continue; /* if in "augment" mode, care for new or very old entries only */ if (augment && cache[i].l*(TIME_OF_CYCLE)<(TIME_TO_REBUILD)) continue; /* convert binary to string */ sln = InetNtoA(str, cache[i].a); /* if in "augment" mode, write only new entries */ /* write a line like "1.2.3.4RELAY" */ /* on success, sln will become zero */ if (!augment || cache[i].l==MAX_COUNT) { sln = write(pipedes[1],str,sln) - sln + write(pipedes[1],"\tRELAY\n",7) - 7; fail |= sln; } /* mark successfully written new entry */ if (cache[i].l==MAX_COUNT && sln==0) { cache[i].l = 1; continue; } /* report very old entry */ if (cache[i].l*(TIME_OF_CYCLE)>=(TIME_TO_REBUILD)) { cache[i].l = 1; Report(0, "%s still active", str, 0); } } } fail |= close(pipedes[1]); if (fail) Report(1, "Error while writing to pipe", NULL, 0); /* wait for child to finish and check return code */ waitpid(makemap_pid, &i, 0); makemap_pid = 0; if ((WIFEXITED(i) && WEXITSTATUS(i)) || WIFSIGNALED(i)) { Report(1, "%s exited irregularly", MAKEMAP_NAME, 0); fail |= 1; } return fail; } void EndlessLoop(void) { fd_set des_set; unsigned long int a; int i, j, oldest_j, oldest_t; struct timeval pause; #define MINUTES *60 time_t currenttime = time(NULL); time_t nextcheck = currenttime + (TIME_TO_CHECKFILES)MINUTES; time_t nextrebuild = currenttime + (TIME_TO_REBUILD)MINUTES; time_t nextwrite = currenttime + (TIME_PAUSE); time_t nextcycle = currenttime + (TIME_OF_CYCLE)MINUTES; int newaddress=0; ssize_t inp, off=0; char buffer[ADDRESS_READBUFFER+1]; /* initialize SMTP after POP list */ for (i=0; inextwrite) pause.tv_sec = nextwrite; if (pause.tv_sec>nextcheck) pause.tv_sec = nextcheck; if (pause.tv_sec>nextrebuild) pause.tv_sec = nextrebuild; pause.tv_sec -= currenttime; /* sleep at least 1 second */ if (pause.tv_sec<1) pause.tv_sec = 1; /* sleep a bit longer than needed */ pause.tv_usec = 600000; /* listen to the FIFO */ FD_ZERO(&des_set); if (fifo_des!=-1) FD_SET(fifo_des, &des_set); i = select(fifo_des+1, &des_set, NULL, NULL, &pause); currenttime = time(NULL); if (i==-1) { /* on error, take a break */ if (errno!=EINTR) sleep(TIME_PAUSE); /* on error or after signal, begin next cycle */ continue; } /* update counters */ if (currenttime>=nextcycle) { for (j=0; j<(CACHE_ENTRIES); ++j) { /* erase expired address */ if (cache[j].t*(TIME_OF_CYCLE)>TIME_IN_CACHE) { cache[j].a = 0; continue; } if (cache[j].t-1) for (j=0; j<(CACHE_ENTRIES); ++j) { if (cache[j].a==a) { cache[j].t = 0; j = -1; break; } } /* insert */ if (j>-1) { /* we have a new address */ newaddress = 1; /* find oldest entry... */ oldest_j = oldest_t = 0; for (j=0; j<(CACHE_ENTRIES); ++j) { if (cache[j].t>oldest_t) { oldest_t = cache[j].t; oldest_j = j; } } /* ...overwrite it... */ cache[oldest_j].a = a; cache[oldest_j].t = 0; cache[oldest_j].l = MAX_COUNT; /* ...report new entry... */ Report(0, "%s", buffer+off, 0); } /* ...and read next string */ } if (a==INVALID_ADDRESS) { /* shift uninterpretable string to begin of buffer, maybe it will be completed by next read */ for (i=off; iADDRESS_READBUFFER*4/5) off = 0; } else off = 0; } /*** END READ ***/ /* check configuration for modifications */ if (currenttime>=nextcheck) { CheckFiles(); ReadAccessText(); nextcheck += (TIME_TO_CHECKFILES)MINUTES; } /* fall asleep if not allowed to write again */ if (currenttime=nextrebuild) { WriteAccessMap(0,0); nextrebuild += (TIME_TO_REBUILD)MINUTES; nextwrite = currenttime + (TIME_PAUSE); newaddress = 0; } /* add new addresses */ if (newaddress) { WriteAccessMap(1,0); nextwrite = currenttime + (TIME_PAUSE); newaddress = 0; } } /*** for (;;) ***/ } /* similar to inet_ntoa() */ int InetNtoA(char *str, unsigned long int a0) { unsigned long int a1, a2, a3; a3 = a0 & 255; a0 >>= 8; a2 = a0 & 255; a0 >>= 8; a1 = a0 & 255; a0 >>= 8; return sprintf(str, "%lu.%lu.%lu.%lu", a0, a1, a2, a3); } /* similar to inet_addr(), but insists on 4 decimals */ unsigned long int InetAddr(const char *str) { int dots; unsigned long int dec, ret; for (dec=ret=dots=0; *str; ++str) { if (*str=='.' && /* dots must be surrounded by decimals */ *--str>='0' && *str++<='9' && *++str>='0' && *str--<='9') { ++dots; ret <<= 8; dec = 0; continue; } if (*str<='9' && *str>='0') { dec = dec*10 + *str-'0'; if (dec<256) { ret = (ret & 0xffffff00) | dec; continue; } } /* an error occured */ dots = 0; break; } return dots==3 ? ret : INVALID_ADDRESS; } /* end of smaccessd.c */