root/usr.sbin/identd/identd.c
/*      $OpenBSD: identd.c,v 1.40 2019/07/03 03:24:03 deraadt Exp $ */

/*
 * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
 *
 * 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 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/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/queue.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/uio.h>

#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netinet/tcp_timer.h>
#include <netinet/tcp_var.h>

#include <netdb.h>

#include <err.h>
#include <ctype.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>

#define IDENTD_USER "_identd"

#define DOTNOIDENT ".noident"

#define TIMEOUT_MIN 4
#define TIMEOUT_MAX 240
#define TIMEOUT_DEFAULT 120
#define INPUT_MAX 256

enum ident_client_state {
        S_BEGINNING = 0,
        S_SERVER_PORT,
        S_PRE_COMMA,
        S_POST_COMMA,
        S_CLIENT_PORT,
        S_PRE_EOL,
        S_EOL,

        S_DEAD,
        S_QUEUED
};

#define E_NONE          0
#define E_NOUSER        1
#define E_UNKNOWN       2
#define E_HIDDEN        3

struct ident_client {
        struct {
                /* from the socket */
                struct sockaddr_storage ss;
                socklen_t len;

                /* from the request */
                u_int port;
        } client, server;
        SIMPLEQ_ENTRY(ident_client) entry;
        enum ident_client_state state;
        struct event ev;
        struct event tmo;
        size_t rxbytes;

        char *buf;
        size_t buflen;
        size_t bufoff;
        uid_t uid;
};

struct ident_resolver {
        SIMPLEQ_ENTRY(ident_resolver) entry;
        char *buf;
        size_t buflen;
        u_int error;
};

struct identd_listener {
        struct event ev, pause;
};

void    parent_rd(int, short, void *);
void    parent_wr(int, short, void *);
int     parent_username(struct ident_resolver *, struct passwd *);
int     parent_uid(struct ident_resolver *, struct passwd *);
int     parent_token(struct ident_resolver *, struct passwd *);
void    parent_noident(struct ident_resolver *, struct passwd *);

void    child_rd(int, short, void *);
void    child_wr(int, short, void *);

void    identd_listen(const char *, const char *, int);
void    identd_paused(int, short, void *);
void    identd_accept(int, short, void *);
int     identd_error(struct ident_client *, const char *);
void    identd_close(struct ident_client *);
void    identd_timeout(int, short, void *);
void    identd_request(int, short, void *);
enum ident_client_state
        identd_parse(struct ident_client *, int);
void    identd_resolving(int, short, void *);
void    identd_response(int, short, void *);
int     fetchuid(struct ident_client *);

const char *gethost(struct sockaddr_storage *);
const char *gentoken(void);

struct loggers {
        __dead void (*err)(int, const char *, ...)
            __attribute__((__format__ (printf, 2, 3)));
        __dead void (*errx)(int, const char *, ...)
            __attribute__((__format__ (printf, 2, 3)));
        void (*warn)(const char *, ...)
            __attribute__((__format__ (printf, 1, 2)));
        void (*warnx)(const char *, ...)
            __attribute__((__format__ (printf, 1, 2)));
        void (*notice)(const char *, ...)
            __attribute__((__format__ (printf, 1, 2)));
        void (*debug)(const char *, ...)
            __attribute__((__format__ (printf, 1, 2)));
};

const struct loggers conslogger = {
        err,
        errx,
        warn,
        warnx,
        warnx, /* notice */
        warnx /* debug */
};

__dead void     syslog_err(int, const char *, ...)
                    __attribute__((__format__ (printf, 2, 3)));
__dead void     syslog_errx(int, const char *, ...)
                    __attribute__((__format__ (printf, 2, 3)));
void            syslog_warn(const char *, ...)
                    __attribute__((__format__ (printf, 1, 2)));
void            syslog_warnx(const char *, ...)
                    __attribute__((__format__ (printf, 1, 2)));
void            syslog_notice(const char *, ...)
                    __attribute__((__format__ (printf, 1, 2)));
void            syslog_debug(const char *, ...)
                    __attribute__((__format__ (printf, 1, 2)));
void            syslog_vstrerror(int, int, const char *, va_list)
                    __attribute__((__format__ (printf, 3, 0)));

const struct loggers syslogger = {
        syslog_err,
        syslog_errx,
        syslog_warn,
        syslog_warnx,
        syslog_notice,
        syslog_debug
};

const struct loggers *logger = &conslogger;

#define lerr(_e, _f...) logger->err((_e), _f)
#define lerrx(_e, _f...) logger->errx((_e), _f)
#define lwarn(_f...) logger->warn(_f)
#define lwarnx(_f...) logger->warnx(_f)
#define lnotice(_f...) logger->notice(_f)
#define ldebug(_f...) logger->debug(_f)

#define sa(_ss) ((struct sockaddr *)(_ss))

static __dead void
usage(void)
{
        extern char *__progname;
        fprintf(stderr, "usage: %s [-46deHhNn] [-l address] [-t timeout]\n",
            __progname);
        exit(1);
}

struct timeval timeout = { TIMEOUT_DEFAULT, 0 };
int debug = 0;
int noident = 0;
int unknown_err = 0;
int hideall = 0;

int (*parent_uprintf)(struct ident_resolver *, struct passwd *) =
    parent_username;

struct event proc_rd, proc_wr;
union {
        struct {
                SIMPLEQ_HEAD(, ident_resolver) replies;
        } parent;
        struct {
                SIMPLEQ_HEAD(, ident_client) pushing, popping;
        } child;
} sc;

int
main(int argc, char *argv[])
{
        extern char *__progname;
        const char *errstr = NULL;

        int              c;
        struct passwd   *pw;

        char *addr = NULL;
        int family = AF_UNSPEC;

        int pair[2];
        pid_t parent;
        int sibling;

        while ((c = getopt(argc, argv, "46deHhl:Nnt:")) != -1) {
                switch (c) {
                case '4':
                        family = AF_INET;
                        break;
                case '6':
                        family = AF_INET6;
                        break;
                case 'd':
                        debug = 1;
                        break;
                case 'e':
                        unknown_err = 1;
                        break;
                case 'H':
                        hideall = 1;
                        /* FALLTHROUGH */
                case 'h':
                        parent_uprintf = parent_token;
                        break;
                case 'l':
                        addr = optarg;
                        break;
                case 'N':
                        noident = 1;
                        break;
                case 'n':
                        parent_uprintf = parent_uid;
                        break;
                case 't':
                        timeout.tv_sec = strtonum(optarg,
                            TIMEOUT_MIN, TIMEOUT_MAX, &errstr);
                        if (errstr != NULL)
                                errx(1, "timeout %s is %s", optarg, errstr);
                        break;
                default:
                        usage();
                        /* NOTREACHED */
                }
        }

        argc -= optind;
        argv += optind;

        if (argc != 0)
                usage();

        if (geteuid() != 0)
                errx(1, "need root privileges");

        if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK,
            PF_UNSPEC, pair) == -1)
                err(1, "socketpair");

        pw = getpwnam(IDENTD_USER);
        if (pw == NULL)
                errx(1, "no %s user", IDENTD_USER);

        if (!debug && daemon(1, 0) == -1)
                err(1, "daemon");

        parent = fork();
        switch (parent) {
        case -1:
                lerr(1, "fork");

        case 0:
                /* child */
                setproctitle("listener");
                close(pair[1]);
                sibling = pair[0];
                break;

        default:
                /* parent */
                setproctitle("resolver");
                close(pair[0]);
                sibling = pair[1];
                break;
        }

        if (!debug) {
                openlog(__progname, LOG_PID|LOG_NDELAY, LOG_DAEMON);
                tzset();
                logger = &syslogger;
        }

        event_init();

        if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
                lerr(1, "signal(SIGPIPE)");

        if (parent) {
                if (pledge("stdio getpw rpath id", NULL) == -1)
                        err(1, "pledge");

                SIMPLEQ_INIT(&sc.parent.replies);

                event_set(&proc_rd, sibling, EV_READ | EV_PERSIST,
                    parent_rd, NULL);
                event_set(&proc_wr, sibling, EV_WRITE,
                    parent_wr, NULL);
        } else {
                SIMPLEQ_INIT(&sc.child.pushing);
                SIMPLEQ_INIT(&sc.child.popping);

                identd_listen(addr, "auth", family);

                if (chroot(pw->pw_dir) == -1)
                        lerr(1, "chroot(%s)", pw->pw_dir);

                if (chdir("/") == -1)
                        lerr(1, "chdir(%s)", pw->pw_dir);

                event_set(&proc_rd, sibling, EV_READ | EV_PERSIST,
                    child_rd, NULL);
                event_set(&proc_wr, sibling, EV_WRITE,
                    child_wr, NULL);
        }

        if (setgroups(1, &pw->pw_gid) ||
            setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
            setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
                lerr(1, "unable to revoke privs");

        if (parent) {
                if (noident) {
                        if (pledge("stdio getpw rpath", NULL) == -1)
                                err(1, "pledge");
                } else {
                        if (pledge("stdio getpw", NULL) == -1)
                                err(1, "pledge");
                }
        }

        event_add(&proc_rd, NULL);
        event_dispatch();
        return (0);
}

void
parent_rd(int fd, short events, void *arg)
{
        struct ident_resolver *r;
        struct passwd *pw;
        ssize_t n;
        uid_t uid;

        n = read(fd, &uid, sizeof(uid));
        switch (n) {
        case -1:
                switch (errno) {
                case EAGAIN:
                case EINTR:
                        return;
                default:
                        lerr(1, "parent read");
                }
                break;
        case 0:
                lerrx(1, "child has gone");
        case sizeof(uid):
                break;
        default:
                lerrx(1, "unexpected %zd data from child", n);
        }

        r = calloc(1, sizeof(*r));
        if (r == NULL)
                lerr(1, "resolver alloc");

        pw = getpwuid(uid);
        if (pw == NULL && !hideall) {
                r->error = E_NOUSER;
                goto done;
        }

        if (noident && !hideall) {
                parent_noident(r, pw);
                if (r->error != E_NONE)
                        goto done;
        }

        n = (*parent_uprintf)(r, pw);
        if (n == -1) {
                r->error = E_UNKNOWN;
                goto done;
        }

        r->buflen = n + 1;

done:
        SIMPLEQ_INSERT_TAIL(&sc.parent.replies, r, entry);
        event_add(&proc_wr, NULL);
}

int
parent_username(struct ident_resolver *r, struct passwd *pw)
{
        return (asprintf(&r->buf, "%s", pw->pw_name));
}

int
parent_uid(struct ident_resolver *r, struct passwd *pw)
{
        return (asprintf(&r->buf, "%u", (u_int)pw->pw_uid));
}

int
parent_token(struct ident_resolver *r, struct passwd *pw)
{
        const char *token;
        int rv;

        token = gentoken();
        rv = asprintf(&r->buf, "%s", token);
        if (rv != -1) {
                if (pw)
                        lnotice("token %s == uid %u (%s)", token,
                            (u_int)pw->pw_uid, pw->pw_name);
                else
                        lnotice("token %s == NO USER", token);
        }

        return (rv);
}

void
parent_noident(struct ident_resolver *r, struct passwd *pw)
{
        char path[PATH_MAX];
        struct stat st;
        int rv;

        rv = snprintf(path, sizeof(path), "%s/%s", pw->pw_dir, DOTNOIDENT);
        if (rv < 0 || rv >= sizeof(path)) {
                r->error = E_UNKNOWN;
                return;
        }

        if (stat(path, &st) == -1)
                return;

        r->error = E_HIDDEN;
}

void
parent_wr(int fd, short events, void *arg)
{
        struct ident_resolver *r = SIMPLEQ_FIRST(&sc.parent.replies);
        struct iovec iov[2];
        int iovcnt = 0;
        ssize_t n;

        iov[iovcnt].iov_base = &r->error;
        iov[iovcnt].iov_len = sizeof(r->error);
        iovcnt++;

        if (r->buflen > 0) {
                iov[iovcnt].iov_base = r->buf;
                iov[iovcnt].iov_len = r->buflen;
                iovcnt++;
        }

        n = writev(fd, iov, iovcnt);
        if (n == -1) {
                switch (errno) {
                case EINTR:
                case EAGAIN:
                        event_add(&proc_wr, NULL);
                        return;
                default:
                        lerr(1, "parent write");
                }
        }

        if (n != sizeof(r->error) + r->buflen)
                lerrx(1, "unexpected parent write length %zd", n);

        SIMPLEQ_REMOVE_HEAD(&sc.parent.replies, entry);

        if (r->buflen > 0)
                free(r->buf);

        free(r);

        if (!SIMPLEQ_EMPTY(&sc.parent.replies))
                event_add(&proc_wr, NULL);
}

void
child_rd(int fd, short events, void *arg)
{
        struct ident_client *c;
        struct {
                u_int error;
                char buf[512];
        } reply;
        ssize_t n;

        n = read(fd, &reply, sizeof(reply));
        switch (n) {
        case -1:
                switch (errno) {
                case EAGAIN:
                case EINTR:
                        return;
                default:
                        lerr(1, "child read");
                }
                break;
        case 0:
                lerrx(1, "parent has gone");
        default:
                break;
        }

        c = SIMPLEQ_FIRST(&sc.child.popping);
        if (c == NULL)
                lerrx(1, "unsolicited data from parent");

        SIMPLEQ_REMOVE_HEAD(&sc.child.popping, entry);

        if (n < sizeof(reply.error))
                lerrx(1, "short data from parent");

        /* check if something went wrong while the parent was working */
        if (c->state == S_DEAD) {
                free(c);
                return;
        }
        c->state = S_DEAD;

        switch (reply.error) {
        case E_NONE:
                n = asprintf(&c->buf, "%u , %u : USERID : UNIX : %s\r\n",
                    c->server.port, c->client.port, reply.buf);
                break;
        case E_NOUSER:
                n = asprintf(&c->buf, "%u , %u : ERROR : %s\r\n",
                    c->server.port, c->client.port,
                    unknown_err ? "UNKNOWN-ERROR" : "NO-USER");
                break;
        case E_UNKNOWN:
                n = asprintf(&c->buf, "%u , %u : ERROR : UNKNOWN-ERROR\r\n",
                    c->server.port, c->client.port);
                break;
        case E_HIDDEN:
                n = asprintf(&c->buf, "%u , %u : ERROR : HIDDEN-USER\r\n",
                    c->server.port, c->client.port);
                break;
        default:
                lerrx(1, "unexpected error from parent %u", reply.error);
        }
        if (n == -1)
                goto fail;

        c->buflen = n;

        fd = EVENT_FD(&c->ev);
        event_del(&c->ev);
        event_set(&c->ev, fd, EV_READ | EV_WRITE | EV_PERSIST,
            identd_response, c);
        event_add(&c->ev, NULL);
        return;

fail:
        identd_close(c);
}

void
child_wr(int fd, short events, void *arg)
{
        struct ident_client *c = SIMPLEQ_FIRST(&sc.child.pushing);
        const char *errstr = NULL;
        ssize_t n;

        n = write(fd, &c->uid, sizeof(c->uid));
        switch (n) {
        case -1:
                switch (errno) {
                case EINTR:
                case EAGAIN:
                        event_add(&proc_wr, NULL);
                        return;
                case ENOBUFS: /* parent has a backlog of requests */
                        errstr = "UNKNOWN-ERROR";
                        break;
                default:
                        lerr(1, "child write");
                }
                break;
        case sizeof(c->uid):
                break;
        default:
                lerrx(1, "unexpected child write length %zd", n);
        }

        SIMPLEQ_REMOVE_HEAD(&sc.child.pushing, entry);
        if (errstr == NULL)
                SIMPLEQ_INSERT_TAIL(&sc.child.popping, c, entry);
        else if (identd_error(c, errstr) == -1)
                identd_close(c);

        if (!SIMPLEQ_EMPTY(&sc.child.pushing))
                event_add(&proc_wr, NULL);
}

void
identd_listen(const char *addr, const char *port, int family)
{
        struct identd_listener *l = NULL;

        struct addrinfo hints, *res, *res0;
        int error, s;
        const char *cause = NULL;
        int on = 1;

        memset(&hints, 0, sizeof(hints));
        hints.ai_family = family;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_flags = AI_PASSIVE;

        error = getaddrinfo(addr, port, &hints, &res0);
        if (error)
                lerrx(1, "%s/%s: %s", addr, port, gai_strerror(error));

        for (res = res0; res != NULL; res = res->ai_next) {
                s = socket(res->ai_family, res->ai_socktype | SOCK_NONBLOCK,
                    res->ai_protocol);
                if (s == -1) {
                        cause = "socket";
                        continue;
                }

                if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                    &on, sizeof(on)) == -1)
                        err(1, "listener setsockopt(SO_REUSEADDR)");

                if (bind(s, res->ai_addr, res->ai_addrlen) == -1) {
                        int serrno = errno;

                        cause = "bind";
                        close(s);
                        errno = serrno;
                        continue;
                }

                if (listen(s, 5) == -1)
                        err(1, "listen");

                l = calloc(1, sizeof(*l));
                if (l == NULL)
                        err(1, "listener ev alloc");

                event_set(&l->ev, s, EV_READ | EV_PERSIST, identd_accept, l);
                event_add(&l->ev, NULL);
                evtimer_set(&l->pause, identd_paused, l);
        }
        if (l == NULL)
                err(1, "%s", cause);

        freeaddrinfo(res0);
}

void
identd_paused(int fd, short events, void *arg)
{
        struct identd_listener *l = arg;
        event_add(&l->ev, NULL);
}

void
identd_accept(int fd, short events, void *arg)
{
        struct identd_listener *l = arg;
        struct sockaddr_storage ss;
        struct timeval pause = { 1, 0 };
        struct ident_client *c = NULL;
        socklen_t len;
        int s;

        len = sizeof(ss);
        s = accept4(fd, sa(&ss), &len, SOCK_NONBLOCK);
        if (s == -1) {
                switch (errno) {
                case EINTR:
                case EWOULDBLOCK:
                case ECONNABORTED:
                        return;
                case EMFILE:
                case ENFILE:
                        event_del(&l->ev);
                        evtimer_add(&l->pause, &pause);
                        return;
                default:
                        lerr(1, "accept");
                }
        }

        c = calloc(1, sizeof(*c));
        if (c == NULL) {
                lwarn("client alloc");
                close(fd);
                return;
        }

        memcpy(&c->client.ss, &ss, len);
        c->client.len = len;
        ldebug("client: %s", gethost(&ss));

        /* lookup the local ip it connected to */
        c->server.len = sizeof(c->server.ss);
        if (getsockname(s, sa(&c->server.ss), &c->server.len) == -1)
                lerr(1, "getsockname");

        event_set(&c->ev, s, EV_READ | EV_PERSIST, identd_request, c);
        event_add(&c->ev, NULL);

        evtimer_set(&c->tmo, identd_timeout, c);
        evtimer_add(&c->tmo, &timeout);
}

void
identd_timeout(int fd, short events, void *arg)
{
        struct ident_client *c = arg;

        event_del(&c->ev);
        close(fd);
        free(c->buf);

        if (c->state == S_QUEUED) /* it is queued for resolving */
                c->state = S_DEAD;
        else
                free(c);
}

void
identd_request(int fd, short events, void *arg)
{
        struct ident_client *c = arg;
        unsigned char buf[64];
        ssize_t n, i;
        char *errstr = unknown_err ? "UNKNOWN-ERROR" : "INVALID-PORT";

        n = read(fd, buf, sizeof(buf));
        switch (n) {
        case -1:
                switch (errno) {
                case EINTR:
                case EAGAIN:
                        return;
                default:
                        lwarn("%s read", gethost(&c->client.ss));
                        goto fail;
                }
                break;

        case 0:
                ldebug("%s closed connection", gethost(&c->client.ss));
                goto fail;
        default:
                break;
        }

        c->rxbytes += n;
        if (c->rxbytes >= INPUT_MAX)
                goto fail;

        for (i = 0; c->state < S_EOL && i < n; i++)
                c->state = identd_parse(c, buf[i]);

        if (c->state == S_DEAD)
                goto error;
        if (c->state != S_EOL)
                return;

        if (c->server.port < 1 || c->client.port < 1)
                goto error;

        if (fetchuid(c) == -1) {
                errstr = unknown_err ? "UNKNOWN-ERROR" : "NO-USER";
                goto error;
        }

        SIMPLEQ_INSERT_TAIL(&sc.child.pushing, c, entry);
        c->state = S_QUEUED;

        event_del(&c->ev);
        event_set(&c->ev, fd, EV_READ | EV_PERSIST, identd_resolving, c);
        event_add(&c->ev, NULL);

        event_add(&proc_wr, NULL);
        return;

error:
        if (identd_error(c, errstr) == -1)
                goto fail;

        return;

fail:
        identd_close(c);
}

int
identd_error(struct ident_client *c, const char *errstr)
{
        int fd = EVENT_FD(&c->ev);
        ssize_t n;

        n = asprintf(&c->buf, "%u , %u : ERROR : %s\r\n",
            c->server.port, c->client.port, errstr);
        if (n == -1)
                return (-1);

        c->buflen = n;

        event_del(&c->ev);
        event_set(&c->ev, fd, EV_READ | EV_WRITE | EV_PERSIST,
            identd_response, c);
        event_add(&c->ev, NULL);

        return (0);
}

void
identd_close(struct ident_client *c)
{
        int fd = EVENT_FD(&c->ev);

        evtimer_del(&c->tmo);
        event_del(&c->ev);
        close(fd);
        free(c->buf);
        free(c);
}

void
identd_resolving(int fd, short events, void *arg)
{
        struct ident_client *c = arg;
        char buf[64];
        ssize_t n;

        /*
         * something happened while we're waiting for the parent to lookup
         * the user.
         */

        n = read(fd, buf, sizeof(buf));
        switch (n) {
        case -1:
                switch (errno) {
                case EINTR:
                case EAGAIN:
                        return;
                default:
                        lwarn("resolving read");
                        break;
                }
                break;
        case 0:
                ldebug("%s closed connection during resolving",
                    gethost(&c->client.ss));
                break;
        default:
                c->rxbytes += n;
                if (c->rxbytes >= INPUT_MAX)
                        break;

                /* ignore extra input */
                return;
        }

        evtimer_del(&c->tmo);
        event_del(&c->ev);
        close(fd);
        c->state = S_DEAD; /* on the resolving queue */
}

enum ident_client_state
identd_parse(struct ident_client *c, int ch)
{
        enum ident_client_state s = c->state;

        switch (s) {
        case S_BEGINNING:
                /* ignore leading space */
                if (ch == '\t' || ch == ' ')
                        return (s);

                if (ch == '0' || !isdigit(ch))
                        return (S_DEAD);

                c->server.port = ch - '0';
                return (S_SERVER_PORT);

        case S_SERVER_PORT:
                if (ch == '\t' || ch == ' ')
                        return (S_PRE_COMMA);
                if (ch == ',')
                        return (S_POST_COMMA);

                if (!isdigit(ch))
                        return (S_DEAD);

                c->server.port *= 10;
                c->server.port += ch - '0';
                if (c->server.port > 65535)
                        return (S_DEAD);

                return (s);

        case S_PRE_COMMA:
                if (ch == '\t' || ch == ' ')
                        return (s);
                if (ch == ',')
                        return (S_POST_COMMA);

                return (S_DEAD);

        case S_POST_COMMA:
                if (ch == '\t' || ch == ' ')
                        return (s);

                if (ch == '0' || !isdigit(ch))
                        return (S_DEAD);

                c->client.port = ch - '0';
                return (S_CLIENT_PORT);

        case S_CLIENT_PORT:
                if (ch == '\t' || ch == ' ')
                        return (S_PRE_EOL);
                if (ch == '\r' || ch == '\n')
                        return (S_EOL);

                if (!isdigit(ch))
                        return (S_DEAD);

                c->client.port *= 10;
                c->client.port += ch - '0';
                if (c->client.port > 65535)
                        return (S_DEAD);

                return (s);

        case S_PRE_EOL:
                if (ch == '\t' || ch == ' ')
                        return (s);
                if (ch == '\r' || ch == '\n')
                        return (S_EOL);

                return (S_DEAD);

        case S_EOL:
                /* ignore trailing garbage */
                return (s);

        default:
                return (S_DEAD);
        }
}

void
identd_response(int fd, short events, void *arg)
{
        struct ident_client *c = arg;
        char buf[64];
        ssize_t n;

        if (events & EV_READ) {
                n = read(fd, buf, sizeof(buf));
                switch (n) {
                case -1:
                        switch (errno) {
                        case EINTR:
                        case EAGAIN:
                                /* meh, try a write */
                                break;
                        default:
                                lwarn("response read");
                                goto done;
                        }
                        break;
                case 0:
                        ldebug("%s closed connection during response",
                            gethost(&c->client.ss));
                        goto done;
                default:
                        c->rxbytes += n;
                        if (c->rxbytes >= INPUT_MAX)
                                goto done;

                        /* ignore extra input */
                        break;
                }
        }

        if (!(events & EV_WRITE))
                return; /* try again later */

        n = write(fd, c->buf + c->bufoff, c->buflen - c->bufoff);
        if (n == -1) {
                switch (errno) {
                case EINTR:
                case EAGAIN:
                        return; /* try again later */
                case EPIPE:
                        goto done;
                default:
                        lwarn("response write");
                        goto done;
                }
        }

        c->bufoff += n;
        if (c->bufoff != c->buflen)
                return; /* try again later */

done:
        identd_close(c);
}

void
syslog_vstrerror(int e, int priority, const char *fmt, va_list ap)
{
        char *s;

        if (vasprintf(&s, fmt, ap) == -1) {
                syslog(LOG_EMERG, "unable to alloc in syslog_vstrerror");
                exit(1);
        }
        syslog(priority, "%s: %s", s, strerror(e));
        free(s);
}

void
syslog_err(int ecode, const char *fmt, ...)
{
        va_list ap;

        va_start(ap, fmt);
        syslog_vstrerror(errno, LOG_CRIT, fmt, ap);
        va_end(ap);
        exit(ecode);
}

void
syslog_errx(int ecode, const char *fmt, ...)
{
        va_list ap;

        va_start(ap, fmt);
        vsyslog(LOG_CRIT, fmt, ap);
        va_end(ap);
        exit(ecode);
}

void
syslog_warn(const char *fmt, ...)
{
        va_list ap;

        va_start(ap, fmt);
        syslog_vstrerror(errno, LOG_ERR, fmt, ap);
        va_end(ap);
}

void
syslog_warnx(const char *fmt, ...)
{
        va_list ap;

        va_start(ap, fmt);
        vsyslog(LOG_ERR, fmt, ap);
        va_end(ap);
}

void
syslog_notice(const char *fmt, ...)
{
        va_list ap;

        va_start(ap, fmt);
        vsyslog(LOG_NOTICE, fmt, ap);
        va_end(ap);
}

void
syslog_debug(const char *fmt, ...)
{
        va_list ap;

        if (!debug)
                return;

        va_start(ap, fmt);
        vsyslog(LOG_DEBUG, fmt, ap);
        va_end(ap);
}

const char *
gethost(struct sockaddr_storage *ss)
{
        struct sockaddr *sa = (struct sockaddr *)ss;
        static char buf[NI_MAXHOST];

        if (getnameinfo(sa, sa->sa_len, buf, sizeof(buf),
            NULL, 0, NI_NUMERICHOST) != 0)
                return ("(unknown)");

        return (buf);
}

const char *
gentoken(void)
{
        static char buf[21];
        u_int32_t r;
        int i;

        buf[0] = 'a' + arc4random_uniform(26);
        for (i = 1; i < sizeof(buf) - 1; i++) {
                r = arc4random_uniform(36);
                buf[i] = (r < 26 ? 'a' : '0' - 26) + r;
        }
        buf[i] = '\0';

        return (buf);
}

int
fetchuid(struct ident_client *c)
{
        struct tcp_ident_mapping tir;
        int mib[] = { CTL_NET, PF_INET, IPPROTO_TCP, TCPCTL_IDENT };
        struct sockaddr_in *s4;
        struct sockaddr_in6 *s6;
        int err = 0;
        size_t len;

        memset(&tir, 0, sizeof(tir));
        memcpy(&tir.faddr, &c->client.ss, sizeof(tir.faddr));
        memcpy(&tir.laddr, &c->server.ss, sizeof(tir.laddr));

        switch (c->server.ss.ss_family) {
        case AF_INET:
                s4 = (struct sockaddr_in *)&tir.faddr;
                s4->sin_port = htons(c->client.port);

                s4 = (struct sockaddr_in *)&tir.laddr;
                s4->sin_port = htons(c->server.port);
                break;
        case AF_INET6:
                s6 = (struct sockaddr_in6 *)&tir.faddr;
                s6->sin6_port = htons(c->client.port);

                s6 = (struct sockaddr_in6 *)&tir.laddr;
                s6->sin6_port = htons(c->server.port);
                break;
        default:
                lerrx(1, "unexpected family %d", c->server.ss.ss_family);
        }

        len = sizeof(tir);
        err = sysctl(mib, sizeof(mib) / sizeof(mib[0]), &tir, &len, NULL, 0);
        if (err == -1)
                lerr(1, "sysctl");

        if (tir.ruid == -1)
                return (-1);

        c->uid = tir.ruid;
        return (0);
}