Add sources for C account creation programs
authorMichael Spang <mspang@csclub.uwaterloo.ca>
Mon, 10 Dec 2007 05:25:14 +0000 (00:25 -0500)
committerMichael Spang <mspang@csclub.uwaterloo.ca>
Mon, 10 Dec 2007 08:15:44 +0000 (03:15 -0500)
21 files changed:
src/.gitignore [new file with mode: 0644]
src/Makefile [new file with mode: 0644]
src/addclub.c [new file with mode: 0644]
src/addhomedir.c [new file with mode: 0644]
src/addhomedir.h [new file with mode: 0644]
src/addmember.c [new file with mode: 0644]
src/common.c [new file with mode: 0644]
src/common.h [new file with mode: 0644]
src/config-test.c [new file with mode: 0644]
src/config.c [new file with mode: 0644]
src/config.h [new file with mode: 0644]
src/kadm.c [new file with mode: 0644]
src/kadm.h [new file with mode: 0644]
src/krb5.c [new file with mode: 0644]
src/krb5.h [new file with mode: 0644]
src/ldap.c [new file with mode: 0644]
src/ldap.h [new file with mode: 0644]
src/parser.c [new file with mode: 0644]
src/parser.h [new file with mode: 0644]
src/util.c [new file with mode: 0644]
src/util.h [new file with mode: 0644]

diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644 (file)
index 0000000..d02cf79
--- /dev/null
@@ -0,0 +1,4 @@
+*.o
+/addmember
+/addclub
+/config-test
diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..ac13cd6
--- /dev/null
@@ -0,0 +1,20 @@
+CFLAGS ?= -ggdb -Wall -O2
+CFLAGS += -I../include
+LDAP := -lldap
+KADM := $(shell krb5-config --libs krb5 kadm-client)
+
+LIBCEO := util.o common.o config.o parser.o ldap.o krb5.o kadm.o addhomedir.o
+
+all: addmember addclub
+
+clean:
+       rm -f addmember addclub config-test *.o
+
+addmember: $(LIBCEO) addmember.o
+       $(CC) $(LDFLAGS) $(LDAP) $(KADM) $^ -o $@
+
+addclub: $(LIBCEO) addclub.o
+       $(CC) $(LDFLAGS) $(LDAP) $(KADM) $^ -o $@
+
+config-test: config-test.o parser.o util.o
+       $(CC) $(LDFLAGS) $^ -o $@
diff --git a/src/addclub.c b/src/addclub.c
new file mode 100644 (file)
index 0000000..6bae715
--- /dev/null
@@ -0,0 +1,181 @@
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <ctype.h>
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <libgen.h>
+#include <syslog.h>
+
+#include "util.h"
+#include "common.h"
+#include "config.h"
+#include "ldap.h"
+#include "krb5.h"
+#include "kadm.h"
+#include "addhomedir.h"
+
+char *prog = NULL;
+char *user = NULL;
+int privileged = 0;
+
+static int force = 0;
+static int no_notify = 0;
+
+static char *name = NULL;
+static char *userid = NULL;
+
+static struct option opts[] = {
+    { "force", 0, NULL, 'f' },
+    { "no-notify", 0, NULL, 'q' },
+    { NULL, 0, NULL, '\0' },
+};
+
+static void usage() {
+    fprintf(stderr, "Usage: %s userid clubname\n", prog);
+    exit(2);
+}
+
+void addclub() {
+    int krb_ok, user_ok, group_ok, home_ok, quota_ok;
+    int id;
+    char homedir[1024];
+
+    logmsg("adding uid=%s cn=%s by %s", userid, name, user);
+
+    if (setreuid(0, 0))
+        fatalpe("setreuid");
+
+    if (!force && getpwnam(userid) != NULL)
+        deny("user %s already exists", userid);
+
+    ceo_krb5_init();
+    ceo_ldap_init();
+    ceo_kadm_init();
+
+    if (ceo_user_exists(userid))
+        deny("user %s already exists in LDAP", userid);
+
+    if ((id = ceo_new_uid(member_min_id, member_max_id)) <= 0)
+        fatal("no available uids in range [%d, %d]", member_min_id, member_max_id);
+
+    snprintf(homedir, sizeof(homedir), "%s/%s", club_home, userid);
+
+    krb_ok = ceo_del_princ(userid);
+    if (!krb_ok)
+        logmsg("successfully cleared principal for %s", userid);
+
+    user_ok = krb_ok || ceo_add_user(userid, users_base, "club", name, homedir,
+            club_shell, id, NULL);
+    if (!user_ok)
+        logmsg("successfully created account for %s", userid);
+
+    group_ok = user_ok || ceo_add_group(userid, groups_base, id);
+    if (!group_ok)
+        logmsg("successfully created group for %s", userid);
+
+    home_ok = user_ok || ceo_create_home(homedir, id, id);
+    if (!home_ok)
+        logmsg("successfully created home directory for %s", userid);
+
+    quota_ok = user_ok || ceo_set_quota(quota_prototype, id);
+    if (!quota_ok)
+        logmsg("successfully set quota for %s", userid);
+
+    logmsg("done uid=%s", userid);
+
+    if (!no_notify && !user_ok) {
+        int pid;
+        int hkp[2];
+        FILE *hkf;
+        int status;
+
+        if (pipe(hkp))
+            errorpe("pipe");
+
+        fflush(stdout);
+        fflush(stderr);
+
+        pid = fork();
+
+        if (!pid) {
+            fclose(stdout);
+            fclose(stderr);
+            close(hkp[1]);
+            dup2(hkp[0], 0);
+            exit(execl(notify_hook, notify_hook, prog, user, userid, name, NULL));
+        }
+
+        hkf = fdopen(hkp[1], "w");
+
+        if (group_ok)
+            fprintf(hkf, "failed to create group\n");
+        if (home_ok)
+            fprintf(hkf, "failed to create home directory\n");
+        if (quota_ok)
+            fprintf(hkf, "failed to set quota\n");
+        if (!group_ok && !home_ok && !quota_ok)
+            fprintf(hkf, "all failures went undetected\n");
+
+        fclose(hkf);
+
+        waitpid(pid, &status, 0);
+
+        if (WIFEXITED(status) && WEXITSTATUS(status))
+            logmsg("hook %s exited with status %d", notify_hook, WEXITSTATUS(status));
+        else if (WIFSIGNALED(status))
+            logmsg("hook %s killed by signal %d", notify_hook, WTERMSIG(status));
+    }
+
+    ceo_kadm_cleanup();
+    ceo_ldap_cleanup();
+    ceo_krb5_cleanup();
+
+    return !krb_ok && !user_ok && !group_ok && !home_ok && quota_ok;
+}
+
+int main(int argc, char *argv[]) {
+    int opt;
+
+    openlog(prog, 0, LOG_AUTHPRIV);
+
+    configure();
+
+    prog = basename(argv[0]);
+    user = ceo_get_user();
+    privileged = ceo_get_privileged();
+
+    while ((opt = getopt_long(argc, argv, "", opts, NULL)) != -1) {
+        switch (opt) {
+            case 'f':
+                if (!privileged)
+                    deny("not privileged enough to force");
+                force = 1;
+                break;
+            case 'q':
+                if (!privileged)
+                    deny("not privileged enough to suppress notifications");
+                no_notify = 1;
+                break;
+            case '?':
+                usage();
+                break;
+            default:
+                fatal("error parsing arguments");
+        }
+    }
+
+    if (argc - optind != 2)
+        usage();
+
+    userid = argv[optind++];
+    name = argv[optind++];
+
+    addclub();
+
+    return 0;
+}
diff --git a/src/addhomedir.c b/src/addhomedir.c
new file mode 100644 (file)
index 0000000..c2ad7fa
--- /dev/null
@@ -0,0 +1,133 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <pwd.h>
+#include <fcntl.h>
+
+#include "addhomedir.h"
+#include "util.h"
+#include "config.h"
+
+int ceo_create_home(char *homedir, uid_t uid, gid_t gid) {
+    int mask;
+    DIR *skel;
+    struct dirent *skelent;
+
+    mask = umask(0);
+
+    if (mkdir(homedir, homedir_mode)) {
+        errorpe("failed to create %s", homedir);
+        return -1;
+    }
+
+    skel = opendir(skeleton_dir);
+    if (!skel) {
+        errorpe("failed to open %s", skeleton_dir);
+        return -1;
+    }
+
+    while ((skelent = readdir(skel))) {
+        struct stat sb;
+        char src[PATH_MAX], dest[PATH_MAX];
+
+        if (!strcmp(skelent->d_name, ".") || !strcmp(skelent->d_name, ".."))
+            continue;
+
+        snprintf(src, sizeof(src), "%s/%s", skeleton_dir, skelent->d_name);
+        snprintf(dest, sizeof(dest), "%s/%s", homedir, skelent->d_name);
+        lstat(src, &sb);
+
+        if (sb.st_uid || sb.st_gid) {
+            warn("not creating %s due to ownership", dest);
+            continue;
+        }
+
+        if (S_ISREG(sb.st_mode)) {
+            int bytes;
+            char buf[4096];
+
+            int srcfd = open(src, O_RDONLY);
+            if (srcfd == -1) {
+                warnpe("open: %s", src);
+                continue;
+            }
+
+            int destfd = open(dest, O_WRONLY|O_CREAT|O_EXCL, sb.st_mode & 0777);
+            if (destfd == -1) {
+                warnpe("open: %s", dest);
+                close(srcfd);
+                continue;
+            }
+
+            for (;;) {
+                bytes = read(srcfd, buf, sizeof(buf));
+                if (!bytes)
+                    break;
+                if (bytes < 0) {
+                    warnpe("read");
+                    break;
+                }
+                if (write(destfd, buf, bytes) < 0) {
+                    warnpe("write");
+                    break;
+                }
+            }
+
+            if (fchown(destfd, uid, gid))
+                errorpe("chown: %s", dest);
+
+            close(srcfd);
+            close(destfd);
+        } else if (S_ISDIR(sb.st_mode)) {
+            if (mkdir(dest, sb.st_mode & 0777)) {
+                warnpe("mkdir: %s", dest);
+                continue;
+            }
+            if (chown(dest, uid, gid))
+                errorpe("chown: %s", dest);
+        } else if (S_ISLNK(sb.st_mode)) {
+            char lnkdest[PATH_MAX];
+            int bytes;
+            bytes = readlink(src, lnkdest, sizeof(lnkdest));
+            lnkdest[bytes] = '\0';
+            if (bytes == -1) {
+                warnpe("readlink: %s", src);
+                continue;
+            }
+            if (symlink(lnkdest, dest)) {
+                warnpe("symlink: %s", dest);
+                continue;
+            }
+            if (lchown(dest, uid, gid))
+                errorpe("lchown: %s", dest);
+        } else {
+            warn("not creating %s", dest);
+        }
+    }
+
+    closedir(skel);
+
+    if (chown(homedir, uid, gid))
+        errorpe("failed to chown %s", homedir);
+
+    umask(mask);
+
+    return 0;
+}
+
+int ceo_set_quota(char *proto, int id) {
+    char user[128];
+    char *sqargs[] = { "setquota", "-a", "-p", proto, NULL, NULL };
+
+    snprintf(user, sizeof(user), "%d", id);
+    sqargs[4] = user;
+
+    if (spawnv("/usr/sbin/setquota", sqargs)) {
+        error("failed to set quota for %s", user);
+        return -1;
+    }
+
+    return 0;
+}
diff --git a/src/addhomedir.h b/src/addhomedir.h
new file mode 100644 (file)
index 0000000..50f7062
--- /dev/null
@@ -0,0 +1,4 @@
+#include <sys/types.h>
+
+int ceo_create_home(char *, uid_t, gid_t);
+int ceo_set_quota(char *, int);
diff --git a/src/addmember.c b/src/addmember.c
new file mode 100644 (file)
index 0000000..382e19f
--- /dev/null
@@ -0,0 +1,196 @@
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <ctype.h>
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <libgen.h>
+#include <syslog.h>
+
+#include "util.h"
+#include "common.h"
+#include "config.h"
+#include "ldap.h"
+#include "krb5.h"
+#include "kadm.h"
+#include "addhomedir.h"
+
+char *prog = NULL;
+char *user = NULL;
+int privileged = 0;
+
+static int force = 0;
+static int no_notify = 0;
+
+static int use_stdin = 0;
+
+static char *name = NULL;
+static char *userid = NULL;
+static char *program = NULL;
+static char password[1024];
+
+static struct option opts[] = {
+    { "force", 0, NULL, 'f' },
+    { "no-notify", 0, NULL, 'q' },
+    { "stdin", 0, NULL, 's' },
+    { NULL, 0, NULL, '\0' },
+};
+
+static void usage() {
+    fprintf(stderr, "Usage: %s userid realname [program]\n", prog);
+    exit(2);
+}
+
+void addmember() {
+    int krb_ok, user_ok, group_ok, home_ok, quota_ok;
+    int id;
+    char homedir[1024];
+
+    logmsg("adding uid=%s cn=%s program=%s by %s", userid, name, program, user);
+
+    if (setreuid(0, 0))
+        fatalpe("setreuid");
+
+    if (!force && getpwnam(userid) != NULL)
+        deny("user %s already exists", userid);
+
+    if (ceo_read_password(password, sizeof(password), use_stdin))
+        return;
+
+    ceo_krb5_init();
+    ceo_ldap_init();
+    ceo_kadm_init();
+
+    if (ceo_user_exists(userid))
+        deny("user %s already exists in LDAP", userid);
+
+    if ((id = ceo_new_uid(member_min_id, member_max_id)) <= 0)
+        fatal("no available uids in range [%d, %d]", member_min_id, member_max_id);
+
+    snprintf(homedir, sizeof(homedir), "%s/%s", member_home, userid);
+
+    krb_ok = ceo_del_princ(userid);
+    krb_ok = krb_ok || ceo_add_princ(userid, password);
+    if (!krb_ok)
+        logmsg("successfully created principal for %s", userid);
+
+    user_ok = krb_ok || ceo_add_user(userid, users_base, "member", name, homedir,
+            member_shell, id, "program", program, NULL);
+    if (!user_ok)
+        logmsg("successfully created account for %s", userid);
+
+    group_ok = user_ok || ceo_add_group(userid, groups_base, id);
+    if (!group_ok)
+        logmsg("successfully created group for %s", userid);
+
+    home_ok = user_ok || ceo_create_home(homedir, id, id);
+    if (!home_ok)
+        logmsg("successfully created home directory for %s", userid);
+
+    quota_ok = user_ok || ceo_set_quota(quota_prototype, id);
+    if (!quota_ok)
+        logmsg("successfully set quota for %s", userid);
+
+    logmsg("done uid=%s", userid);
+
+    if (!no_notify && !user_ok) {
+        int pid;
+        int hkp[2];
+        FILE *hkf;
+        int status;
+
+        if (pipe(hkp))
+            errorpe("pipe");
+
+        fflush(stdout);
+        fflush(stderr);
+
+        pid = fork();
+
+        if (!pid) {
+            fclose(stdout);
+            fclose(stderr);
+            close(hkp[1]);
+            dup2(hkp[0], 0);
+            exit(execl(notify_hook, notify_hook, prog, user, userid, name, program, NULL));
+        }
+
+        hkf = fdopen(hkp[1], "w");
+
+        if (group_ok)
+            fprintf(hkf, "failed to create group\n");
+        if (home_ok)
+            fprintf(hkf, "failed to create home directory\n");
+        if (quota_ok)
+            fprintf(hkf, "failed to set quota\n");
+        if (!group_ok && !home_ok && !quota_ok)
+            fprintf(hkf, "all failures went undetected\n");
+
+        fclose(hkf);
+
+        waitpid(pid, &status, 0);
+
+        if (WIFEXITED(status) && WEXITSTATUS(status))
+            logmsg("hook %s exited with status %d", notify_hook, WEXITSTATUS(status));
+        else if (WIFSIGNALED(status))
+            logmsg("hook %s killed by signal %d", notify_hook, WTERMSIG(status));
+    }
+
+    ceo_kadm_cleanup();
+    ceo_ldap_cleanup();
+    ceo_krb5_cleanup();
+
+    return !krb_ok && !user_ok && !group_ok && !home_ok && quota_ok;
+}
+
+int main(int argc, char *argv[]) {
+    int opt;
+
+    openlog(prog, 0, LOG_AUTHPRIV);
+
+    configure();
+
+    prog = basename(argv[0]);
+    user = ceo_get_user();
+    privileged = ceo_get_privileged();
+
+    while ((opt = getopt_long(argc, argv, "", opts, NULL)) != -1) {
+        switch (opt) {
+            case 's':
+                use_stdin = 1;
+                break;
+            case 'f':
+                if (!privileged)
+                    deny("not privileged enough to force");
+                force = 1;
+                break;
+            case 'q':
+                if (!privileged)
+                    deny("not privileged enough to suppress notifications");
+                no_notify = 1;
+                break;
+            case '?':
+                usage();
+                break;
+            default:
+                fatal("error parsing arguments");
+        }
+    }
+
+    if (argc - optind != 2 && argc - optind != 3)
+        usage();
+
+    userid = argv[optind++];
+    name = argv[optind++];
+
+    if (argc - optind)
+        program = argv[optind++];
+
+    addmember();
+
+    return 0;
+}
diff --git a/src/common.c b/src/common.c
new file mode 100644 (file)
index 0000000..24edc90
--- /dev/null
@@ -0,0 +1,58 @@
+#include <unistd.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include "common.h"
+#include "util.h"
+#include "config.h"
+
+int ceo_get_privileged() {
+    int uid = getuid();
+
+    // root is privileged
+    if (!uid)
+        return 1;
+
+    if (privileged_group) {
+        struct group *privgrp = getgrnam(privileged_group);
+        int pgid;
+        gid_t grps[128];
+        int count, i;
+        if (!privgrp)
+            return 0;
+        pgid = privgrp->gr_gid;
+
+        count = getgroups(sizeof(grps), grps);
+        for (i = 0; i < count; i++)
+            if (grps[i] == pgid)
+                return 1;
+    }
+
+    return 0;
+}
+
+char *ceo_get_user() {
+    struct passwd *pwent = getpwuid(getuid());
+    if (pwent == NULL)
+        fatal("could not determine user");
+    return xstrdup(pwent->pw_name);
+}
+
+void ceo_notify_hook(int argc, ...) {
+    va_list args;
+    char **argv;
+    int i = 0;
+
+    va_start(args, argc);
+
+    argv = (char **)xmalloc(sizeof(char *) * (argc + 1));
+
+    while (i < argc)
+        argv[i++] = va_arg(args, char *);
+
+    argv[i++] = NULL;
+    spawnv(notify_hook, argv);
+
+    va_end(args);
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644 (file)
index 0000000..cd17722
--- /dev/null
@@ -0,0 +1,3 @@
+int ceo_get_privileged();
+char *ceo_get_user();
+void ceo_notify_hook(int, ...);
diff --git a/src/config-test.c b/src/config-test.c
new file mode 100644 (file)
index 0000000..5e0f0d9
--- /dev/null
@@ -0,0 +1,21 @@
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "parser.h"
+#include "util.h"
+
+void config_var(const char *name, const char *value) {
+    printf("%s = \"%s\"\n", name, value);
+}
+
+int main(int argc, char *argv[]) {
+    if (argc < 2) {
+        fprintf(stderr, "usage: %s filename\n\n", argv[0]);
+        exit(1);
+    }
+
+    config_parse(argv[1]);
+
+    return 0;
+}
diff --git a/src/config.c b/src/config.c
new file mode 100644 (file)
index 0000000..13b4219
--- /dev/null
@@ -0,0 +1,96 @@
+#include <stdlib.h>
+#include <limits.h>
+
+#include "config.h"
+#include "parser.h"
+#include "util.h"
+
+#define DEF_STR NULL
+#define DEF_LONG LONG_MIN
+
+char *server_url = DEF_STR;
+
+char *users_base = DEF_STR;
+char *groups_base = DEF_STR;
+
+char *skeleton_dir = DEF_STR;
+char *quota_prototype = DEF_STR;
+char *homedir_prefix = DEF_STR;
+
+char *member_home = DEF_STR;
+char *member_shell = DEF_STR;
+long member_min_id = DEF_LONG;
+long member_max_id = DEF_LONG;
+
+char *club_home = DEF_STR;
+char *club_shell = DEF_STR;
+long club_min_id = DEF_LONG;
+long club_max_id = DEF_LONG;
+
+char *notify_hook = DEF_STR;
+
+char *realm = DEF_STR;
+
+char *admin_principal = DEF_STR;
+char *admin_keytab = DEF_STR;
+
+char *admin_bind_userid = DEF_STR;
+char *admin_bind_keytab = DEF_STR;
+
+char *sasl_realm = DEF_STR;
+char *sasl_mech = DEF_STR;
+
+char *privileged_group = DEF_STR;
+
+long homedir_mode = DEF_LONG;
+long homedir_min_uid = DEF_LONG;
+
+static char *strvarnames[] = { "server_url", "users_base", "admin_principal",
+    "admin_keytab", "skeleton_dir", "quota_prototype", "homedir_prefix",
+    "member_home", "member_shell", "club_home", "club_shell", "realm",
+    "admin_bind_userid", "admin_bind_keytab", "groups_base",
+    "privileged_group", "notify_hook", "sasl_realm", "sasl_mech" };
+static char **strvars[] = { &server_url, &users_base, &admin_principal,
+    &admin_keytab, &skeleton_dir, &quota_prototype, &homedir_prefix,
+    &member_home, &member_shell, &club_home, &club_shell, &realm,
+    &admin_bind_userid, &admin_bind_keytab, &groups_base,
+    &privileged_group, &notify_hook, &sasl_realm, &sasl_mech };
+
+static char *longvarnames[] = { "member_min_id", "member_max_id",
+    "homedir_mode", "homedir_min_uid", "club_min_id", "club_max_id" };
+static long *longvars[] = { &member_min_id, &member_max_id, &homedir_mode,
+    &homedir_min_uid, &club_min_id, &club_max_id };
+
+void config_var(char *var, char *val) {
+    int i;
+
+    for (i = 0; i < sizeof(strvars)/sizeof(*strvars); i++) {
+        if (!strcmp(var, strvarnames[i])) {
+            if (!strvars[i])
+                free(strvars[i]);
+            *strvars[i] = xstrdup(val);
+        }
+    }
+
+    for (i = 0; i < sizeof(longvars)/sizeof(*longvars); i++) {
+        if (!strcmp(var, longvarnames[i])) {
+            *longvars[i] = config_long(var, val);
+        }
+    }
+}
+
+void configure() {
+    int i;
+
+    config_parse(CONFIG_FILE);
+
+    for (i = 0; i < sizeof(strvars)/sizeof(*strvars); i++) {
+        if (*strvars[i] == DEF_STR)
+            badconf("undefined string variable: %s", strvarnames[i]);
+    }
+
+    for (i = 0; i < sizeof(longvars)/sizeof(*longvars); i++) {
+        if (*longvars[i] == DEF_LONG)
+            badconf("undefined long variable: %s", longvarnames[i]);
+    }
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644 (file)
index 0000000..3cce5ea
--- /dev/null
@@ -0,0 +1,42 @@
+#define CONFIG_FILE "/etc/csc/accounts.cf"
+
+extern char *server_url;
+extern char *users_base;
+extern char *groups_base;
+
+extern char *admin_bind_dn;
+extern char *admin_bind_pw;
+
+extern char *skeleton_dir;
+extern char *quota_prototype;
+extern char *homedir_prefix;
+
+extern char *member_home;
+extern char *member_shell;
+extern long member_min_id;
+extern long member_max_id;
+
+extern char *club_home;
+extern char *club_shell;
+extern long club_min_id;
+extern long club_max_id;
+
+extern char *notify_hook;
+
+extern long homedir_mode;
+extern long homedir_min_uid;
+
+extern char *realm;
+
+extern char *admin_principal;
+extern char *admin_keytab;
+
+extern char *admin_bind_userid;
+extern char *admin_bind_keytab;
+
+extern char *sasl_realm;
+extern char *sasl_mech;
+
+extern char *privileged_group;
+
+void configure();
diff --git a/src/kadm.c b/src/kadm.c
new file mode 100644 (file)
index 0000000..0fd05b6
--- /dev/null
@@ -0,0 +1,66 @@
+#include <kadm5/admin.h>
+
+#include "kadm.h"
+#include "krb5.h"
+#include "util.h"
+#include "config.h"
+
+extern char *prog;
+
+static void *handle;
+
+void ceo_kadm_init() {
+    krb5_error_code retval;
+    kadm5_config_params params;
+    memset((void *) &params, 0, sizeof(params));
+
+    retval = kadm5_init_with_skey(admin_principal, admin_keytab,
+                KADM5_ADMIN_SERVICE, &params, KADM5_STRUCT_VERSION,
+                KADM5_API_VERSION_2, &handle);
+    if (retval) {
+        com_err(prog, retval, "while initializing kadm5");
+        exit(1);
+    }
+}
+
+void ceo_kadm_cleanup() {
+    kadm5_destroy(handle);
+}
+
+int ceo_add_princ(char *user, char *password) {
+    krb5_error_code retval;
+    kadm5_principal_ent_rec princ;
+    memset((void *) &princ, 0, sizeof(princ));
+
+    if ((retval = krb5_parse_name(context, user, &princ.principal))) {
+        com_err(prog, retval, "while parsing principal name");
+        return retval;
+    }
+
+    if ((retval = kadm5_create_principal(handle, &princ, KADM5_PRINCIPAL, password))) {
+        com_err(prog, retval, "while creating principal");
+        return retval;
+    }
+
+    krb5_free_principal(context, princ.principal);
+    return 0;
+}
+
+int ceo_del_princ(char *user) {
+    krb5_error_code retval;
+    krb5_principal princ;
+
+    if ((retval = krb5_parse_name(context, user, &princ))) {
+        com_err(prog, retval, "while parsing principal name");
+        return retval;
+    }
+
+    retval = kadm5_delete_principal(handle, princ);
+    if (retval && retval != KADM5_UNK_PRINC) {
+        com_err(prog, retval, "while deleting principal");
+        return retval;
+    }
+
+    krb5_free_principal(context, princ);
+    return 0;
+}
diff --git a/src/kadm.h b/src/kadm.h
new file mode 100644 (file)
index 0000000..b46c41e
--- /dev/null
@@ -0,0 +1,5 @@
+void ceo_kadm_init();
+void ceo_kadm_cleanup();
+
+int ceo_add_princ(char *, char *);
+int ceo_del_princ(char *);
diff --git a/src/krb5.c b/src/krb5.c
new file mode 100644 (file)
index 0000000..a41cafc
--- /dev/null
@@ -0,0 +1,149 @@
+#include <stdio.h>
+#include <krb5.h>
+#include <syslog.h>
+
+#include "krb5.h"
+#include "util.h"
+#include "config.h"
+
+extern char *prog;
+
+krb5_context context;
+
+static void com_err_hk(const char *whoami, long code, const char *fmt, va_list args) {
+    char message[4096];
+    char *msgp = message;
+
+    msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s ", error_message(code));
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args);
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    *msgp++ = '\n';
+    *msgp++ = '\0';
+
+    syslog(LOG_ERR, "%s", message);
+    fprintf(stderr, "%s: %s", whoami, message);
+}
+
+void ceo_krb5_init() {
+    krb5_error_code retval;
+
+    set_com_err_hook(com_err_hk);
+
+    retval = krb5_init_context(&context);
+    if (retval) {
+        com_err(prog, retval, "while initializing krb5");
+        exit(1);
+    }
+
+    retval = krb5_set_default_realm(context, realm);
+    if (retval) {
+        com_err(prog, retval, "while setting default realm");
+        exit(1);
+    }
+}
+
+void ceo_krb5_auth(char *principal, char *ktname) {
+    krb5_error_code retval;
+    krb5_creds creds;
+    krb5_principal princ;
+    krb5_keytab keytab;
+    krb5_ccache cache;
+    krb5_get_init_creds_opt options;
+
+    krb5_get_init_creds_opt_init(&options);
+    memset(&creds, 0, sizeof(creds));
+
+    if ((retval = krb5_parse_name(context, principal, &princ))) {
+        com_err(prog, retval, "while resolving user %s", admin_bind_userid);
+        exit(1);
+    }
+
+    if ((retval = krb5_cc_default(context, &cache))) {
+        com_err(prog, retval, "while resolving credentials cache");
+        exit(1);
+    }
+
+    if ((retval = krb5_kt_resolve(context, ktname, &keytab))) {
+        com_err(prog, retval, "while resolving keytab %s", admin_bind_keytab);
+        exit(1);
+    }
+
+    if ((retval = krb5_get_init_creds_keytab(context, &creds, princ, keytab, 0, NULL, &options))) {
+        com_err(prog, retval, "while getting initial credentials");
+        exit(1);
+    }
+
+    if ((retval = krb5_cc_initialize(context, cache, princ))) {
+        com_err(prog, retval, "while initializing credentials cache");
+        exit(1);
+    }
+
+    if ((retval = krb5_cc_store_cred(context, cache, &creds))) {
+        com_err(prog, retval, "while storing credentials");
+        exit(1);
+    }
+
+    krb5_free_cred_contents(context, &creds);
+    krb5_kt_close(context, keytab);
+    krb5_free_principal(context, princ);
+    krb5_cc_close(context, cache);
+}
+
+void ceo_krb5_deauth() {
+    krb5_error_code retval;
+    krb5_ccache cache;
+
+    if ((retval = krb5_cc_default(context, &cache))) {
+        com_err(prog, retval, "while resolving credentials cache");
+        exit(1);
+    }
+
+    if ((retval = krb5_cc_destroy(context, cache))) {
+        com_err(prog, retval, "while destroying credentials cache");
+        exit(1);
+    }
+}
+
+void ceo_krb5_cleanup() {
+    krb5_free_context(context);
+}
+
+int ceo_read_password(char *password, unsigned int size, int use_stdin) {
+    int tries = 0;
+    unsigned int len;
+
+    do {
+        if (use_stdin) {
+            if (fgets(password, size, stdin) == NULL)
+                fatal("eof while reading password");
+
+            size = strlen(password);
+
+            if (password[size - 1] == '\n')
+                password[size - 1] = '\0';
+        } else {
+            len = size;
+            int retval = krb5_read_password(context, "New password", "Confirm password", password, &len);
+            if (retval == KRB5_LIBOS_PWDINTR) {
+                error("interrupted");
+                return -1;
+            } else if (retval == KRB5_LIBOS_BADPWDMATCH) {
+                fputs("Passwords do not match.\n", stderr);
+            } else if (!password || !*password) {
+                fputs("Please enter a password.\n", stderr);
+            }
+        }
+    } while (++tries < 3 && !*password);
+
+    if (!*password) {
+        error("maximum tries exceeded reading password");
+        return -1;
+    }
+
+    return 0;
+}
diff --git a/src/krb5.h b/src/krb5.h
new file mode 100644 (file)
index 0000000..3f1a01c
--- /dev/null
@@ -0,0 +1,11 @@
+#include <krb5.h>
+
+extern krb5_context context;
+
+void ceo_krb5_init();
+void ceo_krb5_cleanup();
+
+void ceo_krb5_auth(char *, char *);
+void ceo_krb5_deauth();
+
+int ceo_read_password(char *, unsigned int, int);
diff --git a/src/ldap.c b/src/ldap.c
new file mode 100644 (file)
index 0000000..bd07cff
--- /dev/null
@@ -0,0 +1,286 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <grp.h>
+#include <sasl/sasl.h>
+#include <ldap.h>
+#include <krb5.h>
+
+#include "ldap.h"
+#include "krb5.h"
+#include "config.h"
+#include "util.h"
+
+extern char *prog;
+
+LDAP *ld;
+
+static void ldap_fatal(char *msg) {
+    int errnum;
+    char *errstr, *detail;
+
+    ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &errnum);
+    ldap_get_option(ld, LDAP_OPT_ERROR_STRING, &detail);
+
+    errstr = ldap_err2string(errnum);
+
+    if (detail && *detail)
+        fatal("%s: %s (%d): %s", msg, errstr, errnum, detail);
+    else
+        fatal("%s: %s (%d)", msg, errstr, errnum);
+}
+
+static void ldap_err(char *msg) {
+    int errnum;
+    char *errstr, *detail;
+
+    ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &errnum);
+    ldap_get_option(ld, LDAP_OPT_ERROR_STRING, &detail);
+
+    errstr = ldap_err2string(errnum);
+
+    if (detail && *detail)
+        error("%s: %s (%d): %s", msg, errstr, errnum, detail);
+    else
+        error("%s: %s (%d)", msg, errstr, errnum);
+}
+
+int ceo_add_group(char *cn, char *basedn, int no) {
+    if (!cn || !basedn)
+        fatal("addgroup: Invalid argument");
+
+    LDAPMod *mods[8];
+    int i = -1;
+    int ret = 0;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "objectClass";
+    char *objectClasses[] = { "top", "group", "posixGroup", NULL };
+    mods[i]->mod_values = objectClasses;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "cn";
+    char *uids[] = { cn, NULL };
+    mods[i]->mod_values = uids;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "gidNumber";
+    char idno[16];
+    snprintf(idno, sizeof(idno), "%d", no);
+    char *gidNumbers[] = { idno, NULL };
+    mods[i]->mod_values = gidNumbers;
+
+    mods[++i] = NULL;
+
+    char dn[1024];
+    snprintf(dn, sizeof(dn), "cn=%s,%s", cn, basedn);
+
+    if (ldap_add_s(ld, dn, mods) != LDAP_SUCCESS) {
+        ldap_err("addgroup");
+        ret = -1;
+    }
+
+    i = 0;
+    while (mods[i])
+        free(mods[i++]);
+
+    return ret;
+}
+
+int ceo_add_user(char *uid, char *basedn, char *objclass, char *cn, char *home, char *shell, int no, ...) {
+    va_list args;
+
+    if (!uid || !basedn || !cn || !home || !shell)
+        fatal("adduser: Invalid argument");
+
+    LDAPMod *mods[16];
+    int i = -1;
+    int ret = 0;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "objectClass";
+    char *objectClasses[] = { "top", "account", "posixAccount", "shadowAccount", NULL, NULL };
+    if (objclass != NULL)
+        objectClasses[4] = objclass;
+    mods[i]->mod_values = objectClasses;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "uid";
+    char *uids[] = { uid, NULL };
+    mods[i]->mod_values = uids;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "cn";
+    char *cns[] = { cn, NULL };
+    mods[i]->mod_values = cns;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "uidNumber";
+    char idno[16];
+    snprintf(idno, sizeof(idno), "%d", no);
+    char *uidNumbers[] = { idno, NULL };
+    mods[i]->mod_values = uidNumbers;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "gidNumber";
+    mods[i]->mod_values = uidNumbers;
+
+    mods[++i] = xmalloc(sizeof(LDAPMod));
+    mods[i]->mod_op = LDAP_MOD_ADD;
+    mods[i]->mod_type = "homeDirectory";
+    char *homeDirectory[] = { home, NULL };
+    mods[i]->mod_values = homeDirectory;
+
+    va_start(args, no);
+    char *attr;
+    while ((attr = va_arg(args, char *))) {
+        char *val = va_arg(args, char *);
+
+        if (!val || !*val)
+            continue;
+
+        if (i == sizeof(mods) / sizeof(*mods) - 2) {
+            error("too many attributes");
+            return -1;
+        }
+
+        mods[++i] = xmalloc(sizeof(LDAPMod));
+        mods[i]->mod_op = LDAP_MOD_ADD;
+        mods[i]->mod_type = attr;
+        char *vals[] = { val, NULL };
+        mods[i]->mod_values = vals;
+    }
+
+    mods[++i] = NULL;
+
+    char dn[1024];
+    snprintf(dn, sizeof(dn), "uid=%s,%s", uid, basedn);
+
+    if (ldap_add_s(ld, dn, mods) != LDAP_SUCCESS) {
+        ldap_err("adduser");
+        ret = -1;
+    }
+
+    i = 0;
+    while (mods[i])
+        free(mods[i++]);
+
+    return ret;
+}
+
+int ceo_new_uid(int min, int max) {
+    char filter[64];
+    char *attrs[] = { LDAP_NO_ATTRS, NULL };
+    LDAPMessage *res;
+    int i;
+
+    for (i = min; i <= max; i++) {
+        // id taken due to passwd
+        if (getpwuid(i) != NULL)
+            continue;
+
+        // id taken due to group
+        if (getgrgid(i) != NULL)
+            continue;
+
+        snprintf(filter, sizeof(filter), "(|(uidNumber=%d)(gidNumber=%d))", i, i);
+        if (ldap_search_s(ld, users_base, LDAP_SCOPE_SUBTREE, filter, attrs, 1, &res) != LDAP_SUCCESS) {
+            ldap_err("firstuid");
+            return -1;
+        }
+
+        int count = ldap_count_entries(ld, res);
+        ldap_msgfree(res);
+
+        // id taken due to LDAP
+        if (count)
+            continue;
+
+        return i;
+    }
+
+    return -1;
+}
+
+int ceo_add_club(char *uid, char *cn) {
+    int id = ceo_new_uid(club_min_id, club_max_id);
+
+    if (ceo_add_user(uid, users_base, "club", cn, club_home, club_shell, id, NULL))
+        return -1;
+
+    if (ceo_add_group(uid, groups_base, id))
+        return -1;
+
+    return 0;
+}
+
+int ceo_user_exists(char *uid) {
+    char *attrs[] = { LDAP_NO_ATTRS, NULL };
+    LDAPMessage *msg = NULL;
+    char filter[128];
+    int count;
+
+    if (!uid)
+        fatal("null uid");
+
+    snprintf(filter, sizeof(filter), "uid=%s", uid);
+
+    if (ldap_search_s(ld, users_base, LDAP_SCOPE_SUBTREE, filter, attrs, 0, &msg) != LDAP_SUCCESS) {
+        ldap_err("user_exists");
+        return -1;
+    }
+
+    count = ldap_count_entries(ld, msg);
+    ldap_msgfree(msg);
+
+    return count > 0;
+}
+
+static int ldap_sasl_interact(LDAP *ld, unsigned flags, void *defaults, void *in) {
+    sasl_interact_t *interact = in;
+
+    while (interact->id != SASL_CB_LIST_END) {
+        switch (interact->id) {
+
+            // GSSAPI doesn't require any callbacks
+
+            default:
+                interact->result = "";
+                interact->len = 0;
+        }
+
+        interact++;
+    }
+
+    return LDAP_SUCCESS;
+}
+
+void ceo_ldap_init() {
+    int proto = LDAP_DEFAULT_PROTOCOL;
+
+    if (ldap_initialize(&ld, server_url) != LDAP_SUCCESS)
+        ldap_fatal("ldap_initialize");
+
+    if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &proto) != LDAP_OPT_SUCCESS)
+        ldap_fatal("ldap_set_option");
+
+    ceo_krb5_auth(admin_bind_userid, admin_bind_keytab);
+
+    if (ldap_sasl_interactive_bind_s(ld, NULL, sasl_mech, NULL, NULL,
+                LDAP_SASL_QUIET, &ldap_sasl_interact, NULL) != LDAP_SUCCESS)
+        ldap_fatal("Bind failed");
+
+    ceo_krb5_deauth();
+}
+
+void ceo_ldap_cleanup() {
+    ldap_unbind(ld);
+}
diff --git a/src/ldap.h b/src/ldap.h
new file mode 100644 (file)
index 0000000..46b192c
--- /dev/null
@@ -0,0 +1,10 @@
+#define LDAP_DEFAULT_PROTOCOL LDAP_VERSION3
+
+int ceo_add_user(char *, char *, char *, char *, char *, char *, int, ...);
+int ceo_add_group(char *, char *, int);
+int ceo_new_uid(int, int);
+
+void ceo_ldap_init();
+void ceo_ldap_cleanup();
+
+int ceo_user_exists(char *);
diff --git a/src/parser.c b/src/parser.c
new file mode 100644 (file)
index 0000000..a29262d
--- /dev/null
@@ -0,0 +1,214 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+
+#include "parser.h"
+#include "util.h"
+#include "config.h"
+
+#define VAR_MAX 256
+
+void config_var(const char *, const char *);
+
+struct config_file {
+    FILE *p;
+    char *name;
+    int line;
+    struct config_file *parent;
+    int comment;
+};
+
+static void parse_config_file(char *, struct config_file *);
+static void parse_error(struct config_file *file, char *msg) {
+    fatal("parse error on line %d of %s: %s", file->line, file->name, msg);
+}
+
+static int parse_char(struct config_file *file) {
+    int c = getc(file->p);
+    if (c == '\n')
+        (file->line)++;
+    return c;
+}
+
+static void unparse_char(struct config_file *file, int c) {
+    if (c == EOF)
+        return;
+    ungetc(c, file->p);
+    if (c == '\n')
+        (file->line)--;
+}
+
+static void parse_name(struct config_file *file, char *name, size_t maxlen) {
+    int len = 0;
+    int c;
+
+    for (;;) {
+        c = parse_char(file);
+
+        if (c == EOF || c == '\n') {
+            unparse_char(file, c);
+            break;
+        }
+
+        if (!isalpha(c) && c != '_' && c != '-') {
+            unparse_char(file, c);
+            break;
+        }
+
+        if (len == maxlen - 1)
+            parse_error(file, "max name length exceeded");
+
+        name[len++] = c;
+    }
+
+    if (len == 0)
+        parse_error(file, "expected name");
+
+    name[len++] = '\0';
+}
+
+static void parse_value(struct config_file *file, char *value, size_t maxlen) {
+    int len = 0;
+    int quote = 0;
+    int comment = 0;
+    int space = 0;
+    int c;
+
+    for (;;) {
+        c = parse_char(file);
+
+        if (c == EOF || c == '\n')
+            break;
+
+        if (c == '#')
+            comment = 1;
+
+        if ((isspace(c) && !quote) || comment) {
+            space = 1;
+            continue;
+        }
+
+        if (c == '"') {
+            quote = ! quote;
+            continue;
+        }
+
+        if (len == maxlen - space - 1)
+            parse_error(file, "max value length exceeded");
+
+        if (space && len) {
+            value[len++] = ' ';
+        }
+
+        space = 0;
+        value[len++] = c;
+    }
+
+    if (quote)
+        parse_error(file, "unbalanced quotes");
+
+    value[len++] = '\0';
+}
+
+static void parse_include(struct config_file *file) {
+    char path[PATH_MAX];
+    struct config_file *parent = file->parent;
+
+    parse_value(file, path, sizeof(path));
+
+    while (parent != NULL) {
+        if (!strcmp(file->name, parent->name))
+            return;
+        parent = parent->parent;
+    }
+
+    parse_config_file(path, file);
+}
+
+static void parse_config(struct config_file *file) {
+    int c;
+    int comment = 0;
+
+    char var[VAR_MAX];
+    char value[VAR_MAX];
+
+    for (;;) {
+        c = parse_char(file);
+
+        if (c == '\n') {
+            comment = 0;
+            continue;
+        }
+
+        if (c == EOF)
+            return;
+
+        if (isspace(c) | comment)
+            continue;
+
+        if (c == '#') {
+            comment = 1;
+            continue;
+        }
+
+        unparse_char(file, c);
+        parse_name(file, var, sizeof(var));
+
+        if (!strcmp(var, "include")) {
+            parse_include(file);
+            continue;
+        }
+
+        for (;;) {
+            c = parse_char(file);
+            if (c == EOF || c == '\n')
+                parse_error(file, "expected '=' before line end");
+            if (c == '=')
+                break;
+            if (!isspace(c))
+                parse_error(file, "expected '='");
+        }
+
+        parse_value(file, value, sizeof(value));
+        config_var(var, value);
+    }
+}
+
+static void parse_config_file(char *name, struct config_file *parent) {
+    struct config_file file;
+
+    file.p = fopen(name, "r");
+    file.name = name;
+    file.line = 1;
+    file.parent = parent;
+
+    if (!file.p) {
+        if (parent)
+            parse_error(parent, strerror(errno));
+        else
+            fatal("failed to open configuration file '%s': %s", name, strerror(errno));
+    }
+
+    parse_config(&file);
+
+    fclose(file.p);
+}
+
+long config_long(char *var, char *val) {
+    char *endptr;
+    long longval;
+
+    longval = strtol(val, &endptr, 0);
+
+    if (*val == '\0' || *endptr != '\0')
+        fatal("expected integer value for %s", var);
+
+    return longval;
+}
+
+void config_parse(char *filename) {
+    parse_config_file(filename, NULL);
+}
diff --git a/src/parser.h b/src/parser.h
new file mode 100644 (file)
index 0000000..27f8bb9
--- /dev/null
@@ -0,0 +1,2 @@
+void config_parse(char *);
+long config_long(char *var, char *val);
diff --git a/src/util.c b/src/util.c
new file mode 100644 (file)
index 0000000..917b864
--- /dev/null
@@ -0,0 +1,136 @@
+#include <unistd.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <syslog.h>
+#include <errno.h>
+
+#include "util.h"
+
+static char message[4096];
+
+static void errmsg(int prio, const char *prefix, const char *fmt, va_list args) {
+    char *msgp = message;
+
+    msgp +=  snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s: ", prefix);
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args);
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    *msgp++ = '\n';
+    *msgp++ = '\0';
+
+    syslog(prio, "%s", message);
+    fputs(message, stderr);
+}
+
+static void errmsgpe(int prio, const char *prefix, const char *fmt, va_list args) {
+    char *msgp = message;
+
+    msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), "%s: ", prefix);
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    msgp += vsnprintf(msgp, sizeof(message) - 2 - (msgp - message), fmt, args);
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    msgp += snprintf(msgp, sizeof(message) - 2 - (msgp - message), ": %s", strerror(errno));
+    if (msgp - message > sizeof(message) - 2)
+        fatal("error message overflowed");
+
+    *msgp++ = '\n';
+    *msgp++ = '\0';
+
+    syslog(prio, "%s", message);
+    fputs(message, stderr);
+}
+
+NORETURN static void die(int prio, const char *prefix, const char *msg, va_list args) {
+    errmsg(prio, prefix, msg, args);
+    exit(1);
+}
+
+NORETURN static void diepe(int prio, const char *prefix, const char *msg, va_list args) {
+    errmsgpe(prio, prefix, msg, args);
+    exit(1);
+}
+
+NORETURN void fatal(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    die(LOG_CRIT, "fatal", msg, args);
+    va_end(args);
+}
+
+void error(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    errmsg(LOG_ERR, "error", msg, args);
+    va_end(args);
+}
+
+void warn(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    errmsg(LOG_WARNING, "warning", msg, args);
+    va_end(args);
+}
+
+void logmsg(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    vsyslog(LOG_ERR, msg, args);
+    va_end(args);
+}
+
+NORETURN void deny(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    die(LOG_ERR, "denied", msg, args);
+    va_end(args);
+}
+
+NORETURN void badconf(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    die(LOG_CRIT, "configuration error", msg, args);
+    va_end(args);
+}
+
+NORETURN void fatalpe(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    diepe(LOG_CRIT, "fatal", msg, args);
+    va_end(args);
+}
+
+void errorpe(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    errmsgpe(LOG_ERR, "error", msg, args);
+    va_end(args);
+}
+
+void warnpe(const char *msg, ...) {
+    va_list args;
+    va_start(args, msg);
+    errmsgpe(LOG_WARNING, "warning", msg, args);
+    va_end(args);
+}
+
+int spawnv(const char *path, char *argv[]) {
+    int pid, status;
+    pid = fork();
+    if (pid == -1)
+        fatalpe("fork");
+    else if (pid)
+        waitpid(pid, &status, 0);
+    else
+        exit(execv(path, argv));
+    return status;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644 (file)
index 0000000..f8ad49c
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef CEO_UTIL_H
+#define CEO_UTIL_H
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#ifdef __GNUC__
+#define NORETURN __attribute__((__noreturn__))
+#else
+#define NORETURN
+#endif
+
+int spawnv(const char *, char *[]);
+
+NORETURN void fatal(const char *, ...);
+NORETURN void fatalpe(const char *, ...);
+NORETURN void badconf(const char *, ...);
+NORETURN void deny(const char *, ...);
+void error(const char *, ...);
+void warn(const char *, ...);
+void logmsg(const char *, ...);
+void errorpe(const char *, ...);
+void warnpe(const char *, ...);
+
+static inline void *xmalloc(size_t size) {
+    void *alloc = malloc(size);
+
+    if (alloc == NULL)
+        fatal("out of memory");
+
+    return alloc;
+}
+
+static inline char *xstrdup(const char *s) {
+    char *dup = strdup(s);
+
+    if (dup == NULL)
+        fatal("out of memory");
+
+    return dup;
+}
+
+#endif