Mercurial > sm-archive
diff src/sm-archive.cpp @ 0:616666e2f34c
initial version
author | carl |
---|---|
date | Fri, 10 Mar 2006 10:30:08 -0800 |
parents | |
children | 32b57406b656 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sm-archive.cpp Fri Mar 10 10:30:08 2006 -0800 @@ -0,0 +1,509 @@ +/* + +Copyright (c) 2004, 2005 Carl Byington - 510 Software Group, released +under the GPL version 2 or any later version at your choice available at +http://www.fsf.org/licenses/gpl.txt + +Based on a sample milter Copyright (c) 2000-2003 Sendmail, Inc. and its +suppliers. Inspired by the DCC by Rhyolite Software + +-p port The port through which the MTA will connect to this milter. +-t sec The timeout value. +-c Check the config, and print a copy to stdout. Don't start the + milter or do anything with the socket. +-d increase debug level + +*/ + + +// from sendmail sample +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +#include <sysexits.h> +#include <unistd.h> + +// needed for socket io +#include <sys/ioctl.h> +#include <net/if.h> +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <netdb.h> +#include <sys/socket.h> +#include <sys/un.h> + +// needed for thread +#include <pthread.h> + +// needed for std c++ collections +#include <set> +#include <map> +#include <list> + +// for the dns resolver +#include <netinet/in.h> +#include <arpa/nameser.h> +#include <resolv.h> + +// misc stuff needed here +#include <ctype.h> +#include <syslog.h> +#include <pwd.h> +#include <sys/wait.h> /* header for waitpid() and various macros */ +#include <signal.h> /* header for signal functions */ + +#include "includes.h" + +static char* dnsbl_version="$Id$"; + + +extern "C" { + #include "libmilter/mfapi.h" + sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr); + sfsistat mlfi_envfrom(SMFICTX *ctx, char **argv); + sfsistat mlfi_envrcpt(SMFICTX *ctx, char **argv); + sfsistat mlfi_eom(SMFICTX *ctx); + sfsistat mlfi_abort(SMFICTX *ctx); + sfsistat mlfi_close(SMFICTX *ctx); + void sig_chld(int signo); +} + +int debug_syslog = 0; +bool syslog_opened = false; +bool use_syslog = true; // false to printf +bool loader_run = true; // used to stop the config loader thread +CONFIG *config = NULL; // protected by the config_mutex +int generation = 0; // protected by the config_mutex +const int maxlen = 1000; // used for snprintf buffers + +pthread_mutex_t config_mutex; +pthread_mutex_t syslog_mutex; + + +mlfiPriv::mlfiPriv() { + pthread_mutex_lock(&config_mutex); + pc = config; + pc->reference_count++; + pthread_mutex_unlock(&config_mutex); + mailaddr = NULL; + queueid = NULL; + processed_from = false; +} + +mlfiPriv::~mlfiPriv() { + pthread_mutex_lock(&config_mutex); + pc->reference_count--; + pthread_mutex_unlock(&config_mutex); + reset(true); +} + +void mlfiPriv::reset(bool final) { + processed_from = false; + if (mailaddr) free(mailaddr); + if (queueid) free(queueid); + if (!final) { + mailaddr = NULL; + queueid = NULL; + } +} + +#define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx)) + + +//////////////////////////////////////////////// +// syslog a message +// +void my_syslog(mlfiPriv *priv, char *text) { + char buf[maxlen]; + if (priv) { + snprintf(buf, sizeof(buf), "%s: %s", priv->queueid, text); + text = buf; + } + if (use_syslog) { + pthread_mutex_lock(&syslog_mutex); + if (!syslog_opened) { + openlog("dnsbl", LOG_PID, LOG_MAIL); + syslog_opened = true; + } + syslog(LOG_NOTICE, "%s", text); + pthread_mutex_unlock(&syslog_mutex); + } + else { + printf("%s \n", text); + } +} + +void my_syslog(char *text) { + my_syslog(NULL, text); +} + + +//////////////////////////////////////////////// +// this email address is passed in from sendmail, and will +// always be enclosed in <>. It may have mixed case, just +// as the mail client sent it. We dup the string and convert +// the duplicate to lower case. +// +char *to_lower_string(char *email); +char *to_lower_string(char *email) { + int n = strlen(email)-2; + if (n < 1) return strdup(email); + char *key = strdup(email+1); + key[n] = '\0'; + for (int i=0; i<n; i++) key[i] = tolower(key[i]); + return key; +} + + +//////////////////////////////////////////////// +// start of sendmail milter interfaces +// +sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr) +{ + // allocate some private memory + mlfiPriv *priv = new mlfiPriv; + // save the private data + smfi_setpriv(ctx, (void*)priv); + // continue processing + return SMFIS_CONTINUE; +} + +sfsistat mlfi_envfrom(SMFICTX *ctx, char **from) +{ + mlfiPriv &priv = *MLFIPRIV; + priv.mailaddr = to_lower_string(from[0]); + return SMFIS_CONTINUE; +} + +sfsistat mlfi_envrcpt(SMFICTX *ctx, char **rcpt) +{ + mlfiPriv &priv = *MLFIPRIV; + CONFIG &dc = *priv.pc; + if (!priv.queueid) priv.queueid = strdup(smfi_getsymval(ctx, "i")); + char *rcptaddr = to_lower_string(rcpt[0]); + if (debug_syslog > 1) { + char msg[maxlen]; + snprintf(msg, sizeof(msg), "from <%s> to <%s>", priv.mailaddr, rcptaddr); + my_syslog(&priv, msg); + } + char *target = dc.find_to(rcptaddr); + if (target) smfi_addrcpt(ctx, target); + free(rcptaddr); + if (!processed_from) { + target = dc.find_from(priv.mailaddr); + if (target) smfi_addrcpt(ctx, target); + processed_from = true; + } + return SMFIS_CONTINUE; +} + +sfsistat mlfi_eom(SMFICTX *ctx) +{ + // reset for a new message on the same connection + mlfi_abort(ctx); + return SMFIS_CONTINUE; +} + +sfsistat mlfi_abort(SMFICTX *ctx) +{ + mlfiPriv &priv = *MLFIPRIV; + if (!priv) return SMFIS_CONTINUE; + priv.reset(); + return SMFIS_CONTINUE; +} + +sfsistat mlfi_close(SMFICTX *ctx) +{ + mlfiPriv *priv = MLFIPRIV; + if (!priv) return SMFIS_CONTINUE; + delete priv; + smfi_setpriv(ctx, NULL); + return SMFIS_CONTINUE; +} + +struct smfiDesc smfilter = +{ + "SM-ARCHIVE", // filter name + SMFI_VERSION, // version code -- do not change + SMFIF_ADDRCPT, // flags + mlfi_connect, // connection info filter + NULL, // SMTP HELO command filter + mlfi_envfrom, // envelope sender filter + mlfi_envrcpt, // envelope recipient filter + NULL, // header filter + NULL, // end of header + NULL, // body block filter + mlfi_eom, // end of message + mlfi_abort, // message aborted + mlfi_close, // connection cleanup +}; + + +//////////////////////////////////////////////// +// reload the config +// +CONFIG* new_conf(); +CONFIG* new_conf() { + CONFIG *newc = new CONFIG; + pthread_mutex_lock(&config_mutex); + newc->generation = generation++; + pthread_mutex_unlock(&config_mutex); + if (debug_syslog) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "loading configuration generation %d", newc->generation); + my_syslog(buf); + } + if (load_conf(*newc, "dnsbl.conf")) { + newc->load_time = time(NULL); + return newc; + } + delete newc; + return NULL; +} + + +//////////////////////////////////////////////// +// thread to watch the old config files for changes +// and reload when needed. we also cleanup old +// configs whose reference count has gone to zero. +// +void* config_loader(void *arg); +void* config_loader(void *arg) { + typedef set<CONFIG *> configp_set; + configp_set old_configs; + while (loader_run) { + sleep(180); // look for modifications every 3 minutes + if (!loader_run) break; + CONFIG &dc = *config; + time_t then = dc.load_time; + struct stat st; + bool reload = false; + for (string_set::iterator i=dc.config_files.begin(); i!=dc.config_files.end(); i++) { + char *fn = *i; + if (stat(fn, &st)) reload = true; // file disappeared + else if (st.st_mtime > then) reload = true; // file modified + if (reload) break; + } + if (reload) { + CONFIG *newc = new_conf(); + if (newc) { + // replace the global config pointer + pthread_mutex_lock(&config_mutex); + CONFIG *old = config; + config = newc; + pthread_mutex_unlock(&config_mutex); + if (old) old_configs.insert(old); + } + else { + // failed to load new config + my_syslog("failed to load new configuration"); + system("echo 'failed to load new dnsbl configuration from /etc/dnsbl' | mail -s 'error in /etc/dnsbl configuration' root"); + // update the load time on the current config to prevent complaining every 3 minutes + dc.load_time = time(NULL); + } + } + // now look for old configs with zero ref counts + for (configp_set::iterator i=old_configs.begin(); i!=old_configs.end(); ) { + CONFIG *old = *i; + if (!old->reference_count) { + if (debug_syslog) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "freeing memory for old configuration generation %d", old->generation); + my_syslog(buf); + } + delete old; // destructor does all the work + old_configs.erase(i++); + } + else i++; + } + } + return NULL; +} + + +void usage(char *prog); +void usage(char *prog) +{ + fprintf(stderr, "Usage: %s [-d [level]] [-c] -p sm-sock-addr [-t timeout]\n", prog); + fprintf(stderr, "where sm-sock-addr is for the connection to sendmail\n"); + fprintf(stderr, " and should be one of\n"); + fprintf(stderr, " inet:port@ip-address\n"); + fprintf(stderr, " local:local-domain-socket-file-name\n"); + fprintf(stderr, "-c will load and dump the config to stdout\n"); + fprintf(stderr, "-d will set the syslog message level, currently 0 to 3\n"); +} + + + +void setup_socket(char *sock); +void setup_socket(char *sock) { + unlink(sock); + // sockaddr_un addr; + // memset(&addr, '\0', sizeof addr); + // addr.sun_family = AF_UNIX; + // strncpy(addr.sun_path, sock, sizeof(addr.sun_path)-1); + // int s = socket(AF_UNIX, SOCK_STREAM, 0); + // bind(s, (sockaddr*)&addr, sizeof(addr)); + // close(s); +} + + +/* + * The signal handler function -- only gets called when a SIGCHLD + * is received, ie when a child terminates + */ +void sig_chld(int signo) +{ + int status; + /* Wait for any child without blocking */ + while (waitpid(-1, &status, WNOHANG) > 0) { + // ignore child exit status, we only do this to cleanup zombies + } +} + + +int main(int argc, char**argv) +{ + token_init(); + bool check = false; + bool setconn = false; + int c; + const char *args = "p:t:d:ch"; + extern char *optarg; + + // Process command line options + while ((c = getopt(argc, argv, args)) != -1) { + switch (c) { + case 'p': + if (optarg == NULL || *optarg == '\0') { + fprintf(stderr, "Illegal sendmail socket: %s\n", optarg); + exit(EX_USAGE); + } + if (smfi_setconn(optarg) == MI_FAILURE) { + fprintf(stderr, "smfi_setconn failed\n"); + exit(EX_SOFTWARE); + } + if (strncasecmp(optarg, "unix:", 5) == 0) setup_socket(optarg + 5); + else if (strncasecmp(optarg, "local:", 6) == 0) setup_socket(optarg + 6); + setconn = true; + break; + + case 't': + if (optarg == NULL || *optarg == '\0') { + fprintf(stderr, "Illegal timeout: %s\n", optarg); + exit(EX_USAGE); + } + if (smfi_settimeout(atoi(optarg)) == MI_FAILURE) { + fprintf(stderr, "smfi_settimeout failed\n"); + exit(EX_SOFTWARE); + } + break; + + case 'c': + check = true; + break; + + case 'd': + if (optarg == NULL || *optarg == '\0') debug_syslog = 1; + else debug_syslog = atoi(optarg); + break; + + case 'h': + default: + usage(argv[0]); + exit(EX_USAGE); + } + } + + if (check) { + use_syslog = false; + debug_syslog = 10; + CONFIG *conf = new_conf(); + if (conf) { + conf->dump(); + delete conf; + return 0; + } + else { + return 1; // config failed to load + } + } + + if (!setconn) { + fprintf(stderr, "%s: Missing required -p argument\n", argv[0]); + usage(argv[0]); + exit(EX_USAGE); + } + + if (smfi_register(smfilter) == MI_FAILURE) { + fprintf(stderr, "smfi_register failed\n"); + exit(EX_UNAVAILABLE); + } + + // switch to background mode + if (daemon(1,0) < 0) { + fprintf(stderr, "daemon() call failed\n"); + exit(EX_UNAVAILABLE); + } + + // write the pid + const char *pidpath = "/var/run/dnsbl.pid"; + unlink(pidpath); + FILE *f = fopen(pidpath, "w"); + if (f) { +#ifdef linux + // from a comment in the DCC source code: + // Linux threads are broken. Signals given the + // original process are delivered to only the + // thread that happens to have that PID. The + // sendmail libmilter thread that needs to hear + // SIGINT and other signals does not, and that breaks + // scripts that need to stop milters. + // However, signaling the process group works. + fprintf(f, "-%d\n", (u_int)getpgrp()); +#else + fprintf(f, "%d\n", (u_int)getpid()); +#endif + fclose(f); + } + + // initialize the thread sync objects + pthread_mutex_init(&config_mutex, 0); + pthread_mutex_init(&syslog_mutex, 0); + + // drop root privs + struct passwd *pw = getpwnam("sm-archive"); + if (pw) { + if (setgid(pw->pw_gid) == -1) { + my_syslog("failed to switch to group dnsbl"); + } + if (setuid(pw->pw_uid) == -1) { + my_syslog("failed to switch to user dnsbl"); + } + } + + // load the initial config + config = new_conf(); + if (!config) { + my_syslog("failed to load initial configuration, quitting"); + exit(1); + } + + // only create threads after the fork() in daemon + pthread_t tid; + if (pthread_create(&tid, 0, config_loader, 0)) + my_syslog("failed to create config loader thread"); + if (pthread_detach(tid)) + my_syslog("failed to detach config loader thread"); + + time_t starting = time(NULL); + int rc = smfi_main(); + if ((rc != MI_SUCCESS) && (time(NULL) > starting+5*60)) { + my_syslog("trying to restart after smfi_main()"); + loader_run = false; // eventually the config loader thread will terminate + execvp(argv[0], argv); + } + exit((rc == MI_SUCCESS) ? 0 : EX_UNAVAILABLE); +} +