/* $Id: fdm.c,v 1.163 2007/09/28 18:17:31 nicm Exp $ */
/*
* Copyright (c) 2006 Nicholas Marriott <nicm@users.sourceforge.net>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <grp.h>
#include <limits.h>
#include <paths.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "fdm.h"
#if defined(__OpenBSD__) && defined(DEBUG)
const char *malloc_options = "AFGJPRX";
#endif
void sighandler(int);
void usage(void);
struct conf conf;
volatile sig_atomic_t sigint;
volatile sig_atomic_t sigterm;
void
sighandler(int sig)
{
switch (sig) {
case SIGINT:
sigint = 1;
break;
case SIGTERM:
sigterm = 1;
break;
}
}
double
get_time(void)
{
struct timeval tv;
if (gettimeofday(&tv, NULL) != 0)
fatal("gettimeofday failed");
return (tv.tv_sec + tv.tv_usec / 1000000.0);
}
void
fill_info(const char *home)
{
struct passwd *pw;
uid_t uid;
char host[MAXHOSTNAMELEN];
uid = getuid();
if (conf.info.valid && conf.info.last_uid == uid)
return;
conf.info.valid = 1;
conf.info.last_uid = uid;
if (conf.info.uid != NULL) {
xfree(conf.info.uid);
conf.info.uid = NULL;
}
if (conf.info.user != NULL) {
xfree(conf.info.user);
conf.info.user = NULL;
}
if (conf.info.home != NULL) {
xfree(conf.info.home);
conf.info.home = NULL;
}
if (conf.info.host == NULL) {
if (gethostname(host, sizeof host) != 0)
fatal("gethostname failed");
conf.info.host = xstrdup(host);
getaddrs(host, &conf.info.fqdn, &conf.info.addr);
}
if (home != NULL && *home != '\0')
conf.info.home = xstrdup(home);
xasprintf(&conf.info.uid, "%lu", (u_long) uid);
pw = getpwuid(uid);
if (pw != NULL) {
if (conf.info.home == NULL) {
if (pw->pw_dir != NULL && *pw->pw_dir != '\0')
conf.info.home = xstrdup(pw->pw_dir);
else
conf.info.home = xstrdup(".");
}
if (pw->pw_name != NULL && *pw->pw_name != '\0')
conf.info.user = xstrdup(pw->pw_name);
}
endpwent();
if (conf.info.user == NULL) {
conf.info.user = xstrdup(conf.info.uid);
log_warnx("can't find name for user %lu", (u_long) uid);
}
}
void
dropto(uid_t uid)
{
struct passwd *pw;
gid_t gid;
if (uid == (uid_t) -1 || uid == 0)
return;
pw = getpwuid(uid);
if (pw == NULL) {
errno = ESRCH;
fatal("getpwuid failed");
}
gid = pw->pw_gid;
endpwent();
if (setgroups(1, &gid) != 0)
fatal("setgroups failed");
if (setresgid(gid, gid, gid) != 0)
fatal("setresgid failed");
if (setresuid(uid, uid, uid) != 0)
fatal("setresuid failed");
}
int
check_incl(const char *name)
{
u_int i;
if (ARRAY_EMPTY(&conf.incl))
return (1);
for (i = 0; i < ARRAY_LENGTH(&conf.incl); i++) {
if (account_match(ARRAY_ITEM(&conf.incl, i), name))
return (1);
}
return (0);
}
int
check_excl(const char *name)
{
u_int i;
if (ARRAY_EMPTY(&conf.excl))
return (0);
for (i = 0; i < ARRAY_LENGTH(&conf.excl); i++) {
if (account_match(ARRAY_ITEM(&conf.excl, i), name))
return (1);
}
return (0);
}
int
use_account(struct account *a, char **cause)
{
if (!check_incl(a->name)) {
if (cause != NULL)
xasprintf(cause, "account %s is not included", a->name);
return (0);
}
if (check_excl(a->name)) {
if (cause != NULL)
xasprintf(cause, "account %s is excluded", a->name);
return (0);
}
/*
* If the account is disabled and no accounts are specified on the
* command line (whether or not it is included if there are is already
* confirmed), then skip it.
*/
if (a->disabled && ARRAY_EMPTY(&conf.incl)) {
if (cause != NULL)
xasprintf(cause, "account %s is disabled", a->name);
return (0);
}
return (1);
}
__dead void
usage(void)
{
fprintf(stderr,
"usage: %s [-klmnqv] [-a name] [-D name=value] [-f conffile]"
" [-u user] [-x name] [fetch|poll]\n", __progname);
exit(1);
}
int
main(int argc, char **argv)
{
int opt, lockfd, status, res;
u_int i;
enum fdmop op = FDMOP_NONE;
const char *errstr, *proxy = NULL, *s;
char tmp[BUFSIZ], *ptr, *strs, *user = NULL, *lock = NULL;
long n;
struct utsname un;
struct passwd *pw;
struct stat sb;
time_t tt;
struct account *a;
pid_t pid;
struct children children, dead_children;
struct child *child;
struct io *rio;
struct iolist iol;
double tim;
struct sigaction act;
struct msg msg;
struct msgbuf msgbuf;
size_t off;
struct strings macros;
struct child_fetch_data *cfd;
#ifdef DEBUG
struct rule *r;
struct action *t;
struct cache *cache;
#endif
log_open(stderr, LOG_MAIL, 0);
memset(&conf, 0, sizeof conf);
TAILQ_INIT(&conf.accounts);
TAILQ_INIT(&conf.rules);
TAILQ_INIT(&conf.actions);
TAILQ_INIT(&conf.caches);
conf.max_size = DEFMAILSIZE;
conf.timeout = DEFTIMEOUT;
conf.lock_types = LOCK_FLOCK;
conf.impl_act = DECISION_NONE;
conf.purge_after = 0;
conf.file_umask = DEFUMASK;
conf.file_group = -1;
conf.queue_high = -1;
conf.queue_low = -1;
conf.def_user = -1;
conf.strip_chars = xstrdup(DEFSTRIPCHARS);
ARRAY_INIT(&conf.incl);
ARRAY_INIT(&conf.excl);
ARRAY_INIT(¯os);
while ((opt = getopt(argc, argv, "a:D:f:klmnqu:vx:")) != EOF) {
switch (opt) {
case 'a':
ARRAY_ADD(&conf.incl, xstrdup(optarg));
break;
case 'D':
ARRAY_ADD(¯os, optarg);
break;
case 'f':
conf.conf_file = xstrdup(optarg);
break;
case 'k':
conf.keep_all = 1;
break;
case 'l':
conf.syslog = 1;
break;
case 'm':
conf.allow_many = 1;
break;
case 'n':
conf.check_only = 1;
break;
case 'u':
user = optarg;
break;
case 'v':
if (conf.debug != -1)
conf.debug++;
break;
case 'q':
conf.debug = -1;
break;
case 'x':
ARRAY_ADD(&conf.excl, xstrdup(optarg));
break;
default:
usage();
}
}
argc -= optind;
argv += optind;
if (conf.check_only) {
if (argc != 0)
usage();
} else {
if (argc != 1)
usage();
if (strncmp(argv[0], "poll", strlen(argv[0])) == 0)
op = FDMOP_POLL;
else if (strncmp(argv[0], "fetch", strlen(argv[0])) == 0)
op = FDMOP_FETCH;
else
usage();
}
/* Check the user. */
if (user != NULL) {
pw = getpwnam(user);
if (pw == NULL) {
endpwent();
n = strtonum(user, 0, UID_MAX, &errstr);
if (errstr != NULL) {
if (errno == ERANGE) {
log_warnx("invalid uid: %s", user);
exit(1);
}
} else
pw = getpwuid((uid_t) n);
if (pw == NULL) {
log_warnx("unknown user: %s", user);
exit(1);
}
}
conf.def_user = pw->pw_uid;
endpwent();
}
/* Set debug level and start logging to syslog if necessary. */
if (conf.syslog)
log_open(NULL, LOG_MAIL, conf.debug);
else
log_open(stderr, LOG_MAIL, conf.debug);
tt = time(NULL);
log_debug("version is: %s " BUILD ", started at: %.24s", __progname,
ctime(&tt));
/* And the OS version. */
if (uname(&un) == 0) {
log_debug2("running on: %s %s %s %s", un.sysname, un.release,
un.version, un.machine);
} else
log_debug2("uname: %s", strerror(errno));
/* Save the home dir and misc user info. */
fill_info(getenv("HOME"));
log_debug2("user is: %s, home is: %s", conf.info.user, conf.info.home);
/* Find the config file. */
if (conf.conf_file == NULL) {
/* If no file specified, try ~ then /usr/local/etc. */
xasprintf(&conf.conf_file, "%s/%s", conf.info.home, CONFFILE);
if (access(conf.conf_file, R_OK) != 0) {
xfree(conf.conf_file);
conf.conf_file = xstrdup(SYSCONFFILE);
}
}
log_debug2("loading configuration from %s", conf.conf_file);
if (stat(conf.conf_file, &sb) == -1) {
log_warn("%s", conf.conf_file);
exit(1);
}
if (geteuid() != 0 && (sb.st_mode & (S_IROTH|S_IWOTH)) != 0)
log_warnx("%s: world readable or writable", conf.conf_file);
if (parse_conf(conf.conf_file, ¯os) != 0) {
log_warn("%s", conf.conf_file);
exit(1);
}
ARRAY_FREE(¯os);
log_debug2("configuration loaded");
/* Sort out queue limits. */
if (conf.queue_high == -1)
conf.queue_high = DEFMAILQUEUE;
if (conf.queue_low == -1) {
conf.queue_low = conf.queue_high * 3 / 4;
if (conf.queue_low >= conf.queue_high)
conf.queue_low = conf.queue_high - 1;
}
/* Set the umask. */
umask(conf.file_umask);
/* Figure out default user. */
if (conf.def_user == (uid_t) -1) {
conf.def_user = geteuid();
if (conf.def_user == 0) {
log_warnx("no default user specified");
exit(1);
}
}
/* Print proxy info. */
if (conf.proxy != NULL) {
switch (conf.proxy->type) {
case PROXY_HTTP:
proxy = "HTTP";
break;
case PROXY_HTTPS:
proxy = "HTTPS";
break;
case PROXY_SOCKS5:
proxy = "SOCKS5";
break;
}
log_debug2("using proxy: %s on %s:%s", proxy,
conf.proxy->server.host, conf.proxy->server.port);
}
/* Print some locking info. */
*tmp = '\0';
if (conf.lock_types == 0)
strlcpy(tmp, "none", sizeof tmp);
else {
if (conf.lock_types & LOCK_FCNTL)
strlcat(tmp, "fcntl ", sizeof tmp);
if (conf.lock_types & LOCK_FLOCK)
strlcat(tmp, "flock ", sizeof tmp);
if (conf.lock_types & LOCK_DOTLOCK)
strlcat(tmp, "dotlock ", sizeof tmp);
}
log_debug2("locking using: %s", tmp);
/* Initialise and print headers and domains. */
if (conf.headers == NULL) {
conf.headers = xmalloc(sizeof *conf.headers);
ARRAY_INIT(conf.headers);
ARRAY_ADD(conf.headers, xstrdup("to"));
ARRAY_ADD(conf.headers, xstrdup("cc"));
}
strs = fmt_strings("", conf.headers);
log_debug2("headers are: %s", strs);
xfree(strs);
if (conf.domains == NULL) {
conf.domains = xmalloc(sizeof *conf.domains);
ARRAY_INIT(conf.domains);
ARRAY_ADD(conf.domains, xstrdup(conf.info.host));
if (conf.info.fqdn != NULL) {
ptr = xstrdup(conf.info.fqdn);
ARRAY_ADD(conf.domains, ptr);
}
if (conf.info.addr != NULL) {
xasprintf(&ptr, "\\[%s\\]", conf.info.addr);
ARRAY_ADD(conf.domains, ptr);
}
}
strs = fmt_strings("", conf.domains);
log_debug2("domains are: %s", strs);
xfree(strs);
/* Print the other settings. */
*tmp = '\0';
off = 0;
if (conf.allow_many)
off = strlcat(tmp, "allow-multiple, ", sizeof tmp);
if (conf.no_received)
off = strlcat(tmp, "no-received, ", sizeof tmp);
if (conf.keep_all)
off = strlcat(tmp, "keep-all, ", sizeof tmp);
if (conf.del_big)
off = strlcat(tmp, "delete-oversized, ", sizeof tmp);
if (conf.verify_certs)
off = strlcat(tmp, "verify-certificates, ", sizeof tmp);
if (sizeof tmp > off && conf.purge_after > 0) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"purge-after=%u, ", conf.purge_after);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"maximum-size=%zu, ", conf.max_size);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"timeout=%d, ", conf.timeout / 1000);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"default-user=%lu, ", (u_long) conf.def_user);
}
if (sizeof tmp > off && conf.impl_act != DECISION_NONE) {
if (conf.impl_act == DECISION_DROP)
s = "drop";
else if (conf.impl_act == DECISION_KEEP)
s = "keep";
else
s = "none";
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"unmatched-mail=%s, ", s);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"file-umask=%o%o%o, ", MODE(conf.file_umask));
}
if (sizeof tmp > off && conf.file_group != (gid_t) -1) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"file-group=%lu, ", (u_long) conf.file_group);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"queue-high=%u, queue-low=%u, ", conf.queue_high,
conf.queue_low);
}
if (sizeof tmp > off && conf.lock_file != NULL) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"lock-file=\"%s\", ", conf.lock_file);
}
if (sizeof tmp > off) {
off += xsnprintf(tmp + off, (sizeof tmp) - off,
"strip-characters=\"%s\", ", conf.strip_chars);
}
if (off >= 2) {
tmp[off - 2] = '\0';
log_debug2("options are: %s", tmp);
}
/* Save and print tmp dir. */
s = getenv("TMPDIR");
if (s == NULL || *s == '\0')
s = _PATH_TMP;
else {
if (stat(s, &sb) == -1 || !S_ISDIR(sb.st_mode)) {
log_warn("%s", s);
s = _PATH_TMP;
}
}
conf.tmp_dir = xstrdup(s);
while ((ptr = strrchr(conf.tmp_dir, '/')) != NULL) {
if (ptr == conf.tmp_dir || ptr[1] != '\0')
break;
*ptr = '\0';
}
log_debug2("using tmp directory: %s", conf.tmp_dir);
/* If -n, bail now, otherwise check there is something to work with. */
if (conf.check_only)
exit(0);
if (TAILQ_EMPTY(&conf.accounts)) {
log_warnx("no accounts specified");
exit(1);
}
if (op == FDMOP_FETCH && TAILQ_EMPTY(&conf.rules)) {
log_warnx("no rules specified");
exit(1);
}
/* Check for child user if root. */
if (geteuid() == 0) {
pw = getpwnam(CHILDUSER);
if (pw == NULL) {
log_warnx("can't find user: %s", CHILDUSER);
exit(1);
}
conf.child_uid = pw->pw_uid;
conf.child_gid = pw->pw_gid;
endpwent();
}
/* Set up signal handlers. */
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGINT);
sigaddset(&act.sa_mask, SIGTERM);
act.sa_flags = SA_RESTART;
act.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &act, NULL) < 0)
fatal("sigaction failed");
if (sigaction(SIGUSR1, &act, NULL) < 0)
fatal("sigaction failed");
if (sigaction(SIGUSR2, &act, NULL) < 0)
fatal("sigaction failed");
act.sa_handler = sighandler;
if (sigaction(SIGINT, &act, NULL) < 0)
fatal("sigaction failed");
if (sigaction(SIGTERM, &act, NULL) < 0)
fatal("sigaction failed");
/* Check lock file. */
lock = conf.lock_file;
if (lock == NULL) {
if (geteuid() == 0)
lock = xstrdup(SYSLOCKFILE);
else
xasprintf(&lock, "%s/%s", conf.info.home, LOCKFILE);
}
if (*lock != '\0' && !conf.allow_many) {
lockfd = xcreate(lock, O_WRONLY, -1, -1, S_IRUSR|S_IWUSR);
if (lockfd == -1 && errno == EEXIST) {
log_warnx("already running (%s exists)", lock);
exit(1);
} else if (lockfd == -1) {
log_warn("%s: open", lock);
exit(1);
}
close(lockfd);
}
conf.lock_file = lock;
SSL_library_init();
SSL_load_error_strings();
#ifdef DEBUG
COUNTFDS("parent");
#endif
/* Start the children and build the array. */
ARRAY_INIT(&children);
ARRAY_INIT(&dead_children);
child = NULL;
TAILQ_FOREACH(a, &conf.accounts, entry) {
if (!use_account(a, NULL))
continue;
cfd = xmalloc(sizeof *cfd);
cfd->account = a;
cfd->op = op;
cfd->children = &children;
child = child_start(&children, conf.child_uid, child_fetch,
parent_fetch, cfd);
log_debug2("parent: child %ld (%s) started", (long) child->pid,
a->name);
}
if (ARRAY_EMPTY(&children)) {
log_warnx("no accounts found");
res = 1;
goto out;
}
#ifndef NO_SETPROCTITLE
setproctitle("parent");
#endif
log_debug2("parent: started, pid is %ld", (long) getpid());
tim = get_time();
res = 0;
ARRAY_INIT(&iol);
while (!ARRAY_EMPTY(&children)) {
if (sigint || sigterm)
break;
/* Fill the io list. */
ARRAY_CLEAR(&iol);
for (i = 0; i < ARRAY_LENGTH(&children); i++) {
child = ARRAY_ITEM(&children, i);
ARRAY_ADD(&iol, child->io);
}
/* Poll the io list. */
n = io_polln(
ARRAY_DATA(&iol), ARRAY_LENGTH(&iol), &rio, INFTIM, NULL);
switch (n) {
case -1:
fatalx("child socket error");
case 0:
fatalx("child socket closed");
}
while (!ARRAY_EMPTY(&children)) {
/* Check all children for pending privsep messages. */
for (i = 0; i < ARRAY_LENGTH(&children); i++) {
child = ARRAY_ITEM(&children, i);
if (privsep_check(child->io))
break;
}
if (i == ARRAY_LENGTH(&children))
break;
/* And handle them if necessary. */
if (privsep_recv(child->io, &msg, &msgbuf) != 0)
fatalx("privsep_recv error");
log_debug3("parent: got message type %d, id %u from "
"child %ld", msg.type, msg.id, (long) child->pid);
if (child->msg(child, &msg, &msgbuf) == 0)
continue;
/* Child has said it is ready to exit, tell it to. */
memset(&msg, 0, sizeof msg);
msg.type = MSG_EXIT;
if (privsep_send(child->io, &msg, NULL) != 0)
fatalx("privsep_send error");
/* Wait for the child. */
if (waitpid(child->pid, &status, 0) == -1)
fatal("waitpid failed");
if (WIFSIGNALED(status)) {
res = 1;
log_debug2("parent: child %ld got signal %d",
(long) child->pid, WTERMSIG(status));
} else if (!WIFEXITED(status)) {
res = 1;
log_debug2("parent: child %ld didn't exit"
"normally", (long) child->pid);
} else {
if (WEXITSTATUS(status) != 0)
res = 1;
log_debug2("parent: child %ld returned %d",
(long) child->pid, WEXITSTATUS(status));
}
io_close(child->io);
io_free(child->io);
child->io = NULL;
ARRAY_REMOVE(&children, i);
ARRAY_ADD(&dead_children, child);
}
}
ARRAY_FREE(&iol);
/* Free the dead children. */
for (i = 0; i < ARRAY_LENGTH(&dead_children); i++) {
child = ARRAY_ITEM(&dead_children, i);
if (child->data != NULL)
xfree(child->data);
xfree(child);
}
ARRAY_FREE(&dead_children);
if (sigint || sigterm) {
act.sa_handler = SIG_IGN;
if (sigaction(SIGINT, &act, NULL) < 0)
fatal("sigaction failed");
if (sigaction(SIGTERM, &act, NULL) < 0)
fatal("sigaction failed");
if (sigint)
log_warnx("parent: caught SIGINT. stopping");
else if (sigterm)
log_warnx("parent: caught SIGTERM. stopping");
/* Kill the children. */
for (i = 0; i < ARRAY_LENGTH(&children); i++) {
child = ARRAY_ITEM(&children, i);
kill(child->pid, SIGTERM);
io_close(child->io);
io_free(child->io);
xfree(child);
}
ARRAY_FREE(&children);
/* And wait for them. */
for (;;) {
if ((pid = wait(&status)) == -1) {
if (errno == ECHILD)
break;
fatal("wait failed");
}
log_debug2("parent: child %ld killed", (long) pid);
}
res = 1;
}
tim = get_time() - tim;
log_debug2("parent: finished, total time %.3f seconds", tim);
out:
if (!conf.allow_many && *conf.lock_file != '\0')
unlink(conf.lock_file);
#ifdef DEBUG
COUNTFDS("parent");
/* Free everything. */
while (!TAILQ_EMPTY(&conf.caches)) {
cache = TAILQ_FIRST(&conf.caches);
TAILQ_REMOVE(&conf.caches, cache, entry);
free_cache(cache);
}
while (!TAILQ_EMPTY(&conf.accounts)) {
a = TAILQ_FIRST(&conf.accounts);
TAILQ_REMOVE(&conf.accounts, a, entry);
free_account(a);
}
while (!TAILQ_EMPTY(&conf.rules)) {
r = TAILQ_FIRST(&conf.rules);
TAILQ_REMOVE(&conf.rules, r, entry);
free_rule(r);
}
while (!TAILQ_EMPTY(&conf.actions)) {
t = TAILQ_FIRST(&conf.actions);
TAILQ_REMOVE(&conf.actions, t, entry);
free_action(t);
}
xfree(conf.info.home);
xfree(conf.info.user);
xfree(conf.info.uid);
xfree(conf.info.host);
if (conf.info.fqdn != NULL)
xfree(conf.info.fqdn);
if (conf.info.addr != NULL)
xfree(conf.info.addr);
xfree(conf.conf_file);
xfree(conf.lock_file);
xfree(conf.tmp_dir);
xfree(conf.strip_chars);
free_strings(conf.domains);
ARRAY_FREEALL(conf.domains);
free_strings(conf.headers);
ARRAY_FREEALL(conf.headers);
free_strings(&conf.incl);
free_strings(&conf.excl);
xmalloc_report(getpid(), "parent");
#endif
exit(res);
}
syntax highlighted by Code2HTML, v. 0.9.1