Blob Blame History Raw
/*  authd, a lightweight IPv6/IPv4 inetd RFC 1413 ident protocol daemon
 *  Copyright 2004 by Red Hat, Inc.
 *
 *  THIS PROGRAM IS RELEASED UNDER THE GPL WITH THE ADDITIONAL EXEMPTION
 *  THAT COMPILING, LINKING, AND/OR USING OPENSSL IS ALLOWED.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *  02110-1301, USA.
 *
 *  Initial Author: Adrian Havill <havill@redhat.com>
 */

static const char RCSID[] = "$Revision: 1.18 $ $Date: 2004/07/28 16:04:05 $";

#include "config.h"

#define _GNU_SOURCE

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/utsname.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <getopt.h>
#include <langinfo.h>
#include <libintl.h>
#include <netdb.h>
#include <pwd.h>
#include <syslog.h>
#include <unistd.h>

#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/rand.h>

#if !defined NDEBUG
    #include <mcheck.h>
#endif

#define _(ascii_msgid) gettext(ascii_msgid)

static struct {
    int log_mask;
    int abrupt, help, hybrid, resolve, verbose, xerror; // longopt bool is pint
    bool debug, error, log, number, other, Version;
    char *codeset, *ident, *lang, *mapped, *os, *passwd, *username;
    char *Encrypt, *Noident;
    unsigned timeout, fn; unsigned long long multiquery;
} opt;

static void log_printf(int level, const char *s, ...) {
    if (opt.debug || level != LOG_DEBUG) {
        va_list ap;

        if (s == NULL || *s == '\0') {
            s = strerror(errno);
            assert(strchr(s, '%') == NULL);
        }
        if (opt.log) {
            va_start(ap, s);
            vsyslog(level, s, ap);
            va_end(ap);
        }
        va_start(ap, s);
        if (vfprintf(level == LOG_INFO ? stdout : stderr, s, ap) < 0) {
            perror(program_invocation_name);
            level = INT_MIN;
        }
        va_end(ap);
    }
    if (level <= LOG_ERR) exit(EXIT_FAILURE);
}
#define debug(...)        log_printf(LOG_DEBUG, __VA_ARGS__)
#define show(...)         log_printf(LOG_INFO, __VA_ARGS__)
#define log_notice(...)   log_printf(LOG_NOTICE, __VA_ARGS__)
#define log_warning(...)  log_printf(LOG_WARNING, __VA_ARGS__)
#define handle_error(...) log_printf(LOG_ERR, __VA_ARGS__)
 
#if !defined NDEBUG
static const void *const LAST_VA_ARG = "FIXME"; // debugging sentinel
#endif

static char *vstrdup(const char *s, ...) {
    va_list ap; char *dup;

    assert(s != LAST_VA_ARG);
    va_start(ap, s);
    while (s == NULL || *s == '\0') {
        s = va_arg(ap, const char *);
        assert(s != LAST_VA_ARG);
    }
    va_end(ap);
    if ((dup = strdup(s)) == NULL) handle_error(NULL);
    return dup;
}
#if !defined NDEBUG
    #define vstrdup(s, ...) vstrdup((s) ,##__VA_ARGS__ ,LAST_VA_ARG)
#endif

static void show_help(void) {
    show(_("%s [options]... [request]...\n"), program_invocation_short_name);
    show(_(" -d\t\t\tOutput debug messages\n"));
    show(_(" -E[cipher]\t\tEncrypt username using [%s]\n"), DFL_CIPHER);
    show(_(" -e\t\t\tSend UNKNOWN-ERROR for common error messages\n"));
    show(_(" -l[mask]\t\tLog to syslog, [mask] for priority levels\n"));
    show(_(" -m[reps]\t\tAllow at most [unlimited] requests/connection\n"));
    show(_(" -N[basename]\t\tHide users with ~/[%s] file\n"), DFL_NO_IDENT);
    show(_(" -n\t\t\tSend uid number instead of username\n"));
    show(_(" -o\t\t\tSend OTHER instead of the operating system\n"));
    show(_(" -t[secs]\t\tTimeout request after [%u] seconds\n"), DFL_T_O);
    show(_(" -V\t\t\tPrint program and version information\n"));
    show(_(" --abrupt\t\tDisconnect without displaying error msgs\n"));
    show(_(" --codeset=rfc1340\tSend [charset] if reply is not ASCII\n"));
    show(_(" --fn[=fields]\t\tUse [all] gecos fields instead of username\n"));
    show(_(" --hybrid\t\tUse hybrid notation (ex. ::127.0.0.1) for IPv6\n"));
    show(_(" --ident[=basename]\tHide users with no ~/[%s]\n"), DFL_IDENT);
    show(_(" --lang=lc\t\tLocalize messages, charsets, & time to [locale]\n"));
    show(_(" --mapped=ipv6\t\tMap addresses with same top 96 bits to IPv4\n"));
    show(_(" --os[=rfc1340]\t\tUse [uname], not %s, as OS name\n"), DFL_OS);
    show(_(" --passwd=pathname\tUse line 1 of [%s] for crypto\n"), DFL_PASSWD);
    show(_(" --resolve\t\tUse host and service names if available\n"));
    show(_(" --username[=login]\tUse [%s] as username\n"), DFL_USERNAME);
    show(_(" --verbose\t\tAdd real uid, addresses/ports, & timestamps\n"));
    show(_(" --xerror\t\tSend more helpful error messages for rare errors\n"));
}

static void show_version(void) {
    show("%s-%s:\n\t%s\n\t%s\n", PACKAGE, VERSION, RCSID, CONTACT);
}

static bool is_print_ascii(const char *s) {
    assert(s != NULL);
    while (*s != '\0')
        if (!isascii(*s) || iscntrl(*s)) return false;
        else s++;
    return true;
}

#define is_in_range(lo,x,hi) ((lo) <= (x) && (x) <= (hi))

static bool is_rfc1413_token(const char *s) {
    assert(s != NULL);
    if (!is_print_ascii(s) || strchr(s, ':') != NULL) return false;
    return is_in_range((size_t) 1, strlen(s), (size_t) 64);
}

static bool is_bad_strto(const char *s, const char *endptr) {
    if (errno == ERANGE || errno == EINVAL) return true;
    return endptr == s || (*endptr != '\0' && !isspace(*endptr));
}

static const int HEX_DIG = 2;           // two digits in 8-bit base 16 octet
static const size_t HEX_LEN_MAX = 32;   // strlen() of /proc/net/tcp6 address

static char *created_addr_hex(const unsigned char *addr, size_t addr_size) {
    char *addr_hex;
    size_t addr_hex_size = addr_size * HEX_DIG + sizeof '\0';

    if ((addr_hex = calloc(addr_hex_size, sizeof(char))) != NULL) {
        size_t z = addr_size; char *p = addr_hex;

        while (z != 0) {
            int n = snprintf(p, addr_hex_size, "%.*hhX", HEX_DIG, addr[--z]);

            assert(n >= 0 && (size_t) n < addr_hex_size);
            addr_hex_size -= (size_t) n; p += (size_t) n;
        }
    }
    return addr_hex;
}

static char *created_pton_hex(const char *prefix) {
    struct in6_addr addr; const size_t SIZE = sizeof(addr.s6_addr);

    assert(prefix != NULL);
    if (inet_pton(AF_INET6, prefix, &addr) <= 0) return NULL;
    return created_addr_hex((const void *) addr.s6_addr, SIZE);
}

static void create_opt(int argc, char *argv[]) {
    enum { PRE_FIRST_LONGOPT = UCHAR_MAX,       // no short opt value overlap
        CODESET_LONGOPT, IDENT_LONGOPT, FN_LONGOPT, LANG_LONGOPT,
        MAPPED_LONGOPT, OS_LONGOPT, PASSWD_LONGOPT, USERNAME_LONGOPT
    };
    int c, i;
    const char *const IPV4_HEX_0 = "00000000";
    const char *const S_FMT = _("%s: invalid argument to --%s: %s\n");
    const char *const C_FMT = _("%s: invalid argument to -%c: %s\n");
    const char *const SHORT_OPTS = "dE::ehl::m::N::not::V"; // pidentd compat
    const struct option LONG_OPTS[] = {
        { "abrupt",   no_argument,       &opt.abrupt,  true             },
        { "codeset",  required_argument, NULL,         CODESET_LONGOPT  },
        { "fn",       optional_argument, NULL,         FN_LONGOPT       },
        { "help",     no_argument,       NULL,         'h'              },
        { "hybrid",   no_argument,       &opt.hybrid,  true             },
        { "ident",    optional_argument, NULL,         IDENT_LONGOPT    },
        { "lang",     required_argument, NULL,         LANG_LONGOPT     },
        { "mapped",   required_argument, NULL,         MAPPED_LONGOPT   },
        { "os",       optional_argument, NULL,         OS_LONGOPT       },
        { "passwd",   required_argument, NULL,         PASSWD_LONGOPT   },
        { "resolve",  no_argument,       &opt.resolve, true             },
        { "usage",    no_argument,       NULL,         'h'              },
        { "username", optional_argument, NULL,         USERNAME_LONGOPT },
        { "verbose",  no_argument,       &opt.verbose, true             },
        { "xerror",   no_argument,       &opt.xerror,  true             },
        { 0,          0,                 0,            0                }
    };

    assert(argc > 0 && argv != NULL);
    memset(&opt, 0, sizeof(opt));
    opt.passwd = vstrdup(DFL_PASSWD);
    if ((opt.mapped = calloc(HEX_LEN_MAX + sizeof '\0', sizeof(char))) == NULL)
        handle_error(NULL);
    memset(opt.mapped, '0', HEX_LEN_MAX);
    opt.multiquery = 1;
    opt.timeout = UINT_MAX;
    while ((c = getopt_long(argc, argv, SHORT_OPTS, LONG_OPTS, &i)) != -1) {
        switch (c) {
            struct utsname os;
            unsigned long lu; char *endptr, *lc;

            case 'd': opt.debug = true; break;
            case 'E':
                free(opt.Encrypt); opt.Encrypt = vstrdup(optarg, DFL_CIPHER);
                break;
            case 'e': opt.error = true; break;
            case 'h': opt.help = true; break;
            case 'l':
                if (optarg != NULL) {
                    lu = strtoul(optarg, &endptr, 0);
                    if (lu > UINT_MAX || is_bad_strto(optarg, endptr))
                        handle_error(C_FMT, *argv, c, optarg);
                    else opt.log_mask = (int) lu;
                    setlogmask(opt.log_mask);
                }
                opt.log = true;
                break;
            case 'm':
                if (optarg != NULL) {
                    opt.multiquery = strtoull(optarg, &endptr, 10);
                    if (is_bad_strto(optarg, endptr))
                        handle_error(C_FMT, *argv, c, optarg);
                }
                else opt.multiquery = ULLONG_MAX;
                break;
            case 'n': opt.number = true; break;
            case 'N':
                free(opt.Noident); opt.Noident = vstrdup(optarg, DFL_NO_IDENT);
                break;
            case 'o': opt.other = true; break;
            case 't':
                lu = optarg == NULL ? DFL_T_O : strtoul(optarg, &endptr, 10);
                if (lu > UINT_MAX || is_bad_strto(optarg, endptr))
                    handle_error(C_FMT, *argv, c, optarg);
                else if (lu < 30) {
                    log_notice(_("Timeout's too low; Raising to 30.\n"));
                    lu = 30;
                }
                else if (lu != 0 && !is_in_range(60, lu, 180)) 
                    log_notice(_("Timeout should be from 60..180 seconds.\n"));
                opt.timeout = (unsigned) lu;
                break;
            case 'V': opt.Version = true; break;
            case CODESET_LONGOPT:
                if (!is_rfc1413_token(optarg))
                    handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                free(opt.codeset); opt.codeset = vstrdup(optarg);
                break;
            case FN_LONGOPT:
                if (optarg != NULL) {
                    lu = strtoul(optarg, &endptr, 10);
                    if (lu > UINT_MAX || is_bad_strto(optarg, endptr))
                        handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                    else opt.fn = (unsigned) lu;
                }
                else opt.fn = UINT_MAX;
                break;
            case IDENT_LONGOPT:
                free(opt.ident); opt.ident = vstrdup(optarg, DFL_IDENT);
                break;
            case LANG_LONGOPT:
                lc = strlen(optarg) == 0 ? NULL : setlocale(LC_ALL, optarg);
                if (lc == NULL)
                    handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                else {
                    free(opt.lang); opt.lang = vstrdup(lc);
                    debug("LC_ALL = %s\n", opt.lang);
                }
                break;
            case MAPPED_LONGOPT:
                free(opt.mapped);
                if ((opt.mapped = created_pton_hex(optarg)) == NULL)
                    handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                if (strncmp(opt.mapped, IPV4_HEX_0, strlen(IPV4_HEX_0)) != 0)
                    log_notice(_("Mapped lower 32 bits not 0; ignoring.\n"));
                debug("prefix map    =%s/%s\n", IPV4_HEX_0, opt.mapped + 8);
                break;
            case OS_LONGOPT:
                if (optarg == NULL && uname(&os) != 0) handle_error(NULL);
                free(opt.os); opt.os = vstrdup(optarg, os.sysname);
                if (!is_rfc1413_token(opt.os))
                    handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                break;
            case PASSWD_LONGOPT:
                free(opt.passwd); opt.passwd = vstrdup(optarg, DFL_PASSWD);
                if (strlen(opt.passwd) == 0)
                    handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg);
                break;
            case USERNAME_LONGOPT:
                free(opt.username);
                opt.username = vstrdup(optarg, DFL_USERNAME);
                break;
            default:
                assert(0 == c); // GNU extensions ('\1' and ':') should be off
                break;
            case '?': exit(EXIT_FAILURE);
        }
    }
    if (optind < argc)
        opt.timeout = 0;        // don't get client input if request(s) in cmd
}

static const char *const DELIM = ",: \t\r\n\v\f";

static unsigned long long get_tok_ullong(char *s, unsigned base) {
    unsigned long long ull = ULLONG_MAX;

    assert(base <= 36);
    if ((s = strtok(s, DELIM)) != NULL) {
        char *endptr;

        ull = strtoull(s, &endptr, (int) base);
        if ((errno == ERANGE && ull == ULLONG_MAX) || is_bad_strto(s, endptr))
            errno = EINVAL;
    }
    else errno = EINVAL;
    return ull;
}


static unsigned long get_tok_uint(char *s, unsigned base) {
    unsigned long ul = ULONG_MAX;

    assert(base <= 36);
    if ((s = strtok(s, DELIM)) != NULL) {
        char *endptr;

        ul = strtoul(s, &endptr, (int) base);
        if (ul > UINT_MAX || is_bad_strto(s, endptr))
            errno = EINVAL;
    }
    else errno = EINVAL;
    return ul;
}

static long get_tok_int(char *s, unsigned base) {

    long l = LONG_MAX;

    assert(base <= 36);
    if ((s = strtok(s, DELIM)) != NULL) {
        char *endptr;

        l = strtol(s, &endptr, (int) base);
        if (l > INT_MAX || is_bad_strto(s, endptr))
            errno = EINVAL;
    }
    else errno = EINVAL;
    return l;
}


static void destroy_opt(void) {
    free(opt.codeset); free(opt.Encrypt); free(opt.ident); free(opt.lang);
    free(opt.Noident); free(opt.os); free(opt.passwd); free(opt.mapped);
    free(opt.username);
    memset(&opt, 0, sizeof(opt));
}

typedef struct {
    unsigned long lport, rport;
    char *laddr, *raddr;
} request_t;

typedef int (*getsockaddr_t)(int, struct sockaddr *, socklen_t *);

static char *created_connected_addr(int sockfd, getsockaddr_t getXXXXname) {
    struct sockaddr_storage name; socklen_t namelen = sizeof(name);
    char *addr_hex = NULL;

    if (getXXXXname(sockfd, (struct sockaddr *) &name, &namelen) == 0) {
        size_t addr_size = 0;
        const unsigned char *addr = NULL; 

        switch (name.ss_family) {
            struct sockaddr_in *sin; struct sockaddr_in6 *sin6;

        case AF_INET:
            sin = (struct sockaddr_in *) &name;
            addr_size = sizeof(sin->sin_addr.s_addr);
            addr = (unsigned char *) &sin->sin_addr.s_addr;
            break;
        case AF_INET6:
            sin6 = (struct sockaddr_in6 *) &name;
            addr_size = sizeof(sin6->sin6_addr.s6_addr);
            addr = (unsigned char *) sin6->sin6_addr.s6_addr;
            break;
        default:
            errno = EAFNOSUPPORT;
        }
        addr_hex = created_addr_hex(addr, addr_size);
    }
    else if (errno == ENOTSOCK)
        errno = 0;      // no error if invoked from cmdline
    return addr_hex;
}

static request_t created_request(int argc, char *argv[]) {
    request_t in;
    char *line = NULL; size_t n = 0;

    assert(argv != NULL && argc >= 1);
    if (optind >= argc) {
        if (fflush_unlocked(stdout) == EOF) handle_error(NULL);
        in.laddr = created_connected_addr(STDIN_FILENO, getsockname);
        in.raddr = created_connected_addr(STDIN_FILENO, getpeername);
        if (opt.timeout != UINT_MAX)
            alarm(opt.timeout);
        if (getline(&line, &n, stdin) == (ssize_t) -1) {
            if (feof_unlocked(stdin)) {
                free(line);
                exit(EXIT_SUCCESS);
            }
            assert(ferror(stdin));
            handle_error(NULL);
        }
        alarm(0);
    }
    else {
        line = vstrdup(argv[(size_t) optind++]);
        in.laddr = in.raddr = NULL;
    }
    in.lport = get_tok_uint(line, 10); in.rport = get_tok_uint(NULL, 10);
    debug("local_address =%s:%04lX\n", in.laddr, in.lport);
    debug("rem_address   =%s:%04lX\n", in.raddr, in.rport);
    free(line);
    return in;
}

static char *created_hostname(const char *node) {
    char *s;
    struct addrinfo *res, hints = { ai_flags: AI_CANONNAME };

    assert(node != NULL && *node != '\0');
    if (opt.resolve && getaddrinfo(node, NULL, &hints, &res) == 0) {
        s = vstrdup(res->ai_canonname, node); freeaddrinfo(res);
    }
    else s = vstrdup(node);
    return s;
}

static const unsigned long PORT_MIN = 1, PORT_MAX = 65535;

static char *created_servicename(unsigned long port) {
    char *s;
    const char *const PROTO = "tcp", *const PORT_FMT = "%lu";
    const int NS_PORT = (int) htons((uint16_t) port);
    struct servent *res = opt.resolve ? getservbyport(NS_PORT, PROTO) : NULL;

    assert(is_in_range(PORT_MIN, port, PORT_MAX));
    if (res == NULL || res->s_name == NULL || *res->s_name == '\0') {
        if (asprintf(&s, PORT_FMT, port) < 0) handle_error(NULL);
    }
    else s = vstrdup(res->s_name);
    return s;
}

static char *get_created_tok_addr(const char *peer_addr_hex) {
    const char *const IPV6_0 = ":0000", *const IPV6_0_RUN = "::";
    const size_t IPV6_SIZE_MAX = INET6_ADDRSTRLEN + sizeof '\0';
    const size_t IPV6_0_RUN_LEN = strlen(IPV6_0_RUN);
    char *addr = calloc(sizeof ':' + IPV6_SIZE_MAX, sizeof(char));
    char *addr_hex, *p = addr, *q, *zero = NULL; size_t zero_n = 0;

    if (addr != NULL) {
        if ((addr_hex = strtok(NULL, DELIM)) != NULL) {
            size_t z = strlen(addr_hex); const bool IS_IPV6 = z > 8;
            bool is_16_bits = true;

            if (peer_addr_hex != NULL) {
                char peer_128[HEX_LEN_MAX + sizeof '\0'];
                char host_128[HEX_LEN_MAX + sizeof '\0'];

                assert(opt.mapped != NULL);
                strcpy(peer_128, opt.mapped); strcpy(host_128, opt.mapped);

                /*
                   If mapping IPV4 to IPV6 space is enabled,
                   take only last 4 numbers of IPV6
                */
                if(opt.mapped[0]) {
                  strncpy(host_128, addr_hex+z-8, 8);
                  strncpy(peer_128, peer_addr_hex, 8);
                } else {
                  strncpy(host_128, addr_hex, z);
                  strncpy(peer_128, peer_addr_hex, strlen(peer_addr_hex));
                }

                if (strcmp(peer_128, host_128) != 0) return addr;
            }
            // hex addr must have even number of digits
            if ((int) z & 1) {
                free(addr);
                errno = EINVAL;
                return NULL;
            }
            while (z > 1) {
                unsigned long ul; char *endptr;
                const bool IS_IPV4 = (opt.hybrid && z < 9) || !IS_IPV6;

                addr_hex[z] = '\0'; z -= HEX_DIG;
                ul = strtoul(addr_hex + z, &endptr, 16);
                if (is_bad_strto(addr_hex + z, endptr)) {
                    free(addr); 
                    errno = EINVAL;
                    return NULL;
                }
	        if ((!IS_IPV4 || 6 == z) && is_16_bits)
                    *p++ = ':';
                if (IS_IPV4)
                    p += sprintf(p, "%lu", ul);
                else p += sprintf(p, "%.*lX", HEX_DIG, ul);
                if (IS_IPV4 && z != 0)
                    *p++ = '.';
                is_16_bits = !is_16_bits;
            }
        }
        // compress 16-bit runs of zeros
        for (p = strstr(addr, IPV6_0); p != NULL; p = strstr(q, IPV6_0)) {
            size_t count = 0; const size_t IPV6_0_LEN = strlen(IPV6_0);

            for (q = p; strncmp(q, IPV6_0, IPV6_0_LEN) == 0; q += IPV6_0_LEN)
                count += IPV6_0_LEN;
            if (count > zero_n) {
                zero_n = count; zero = p;
            }
        }
        if (zero_n >= IPV6_0_RUN_LEN) {
            memcpy(zero, IPV6_0_RUN, IPV6_0_RUN_LEN);
            zero += IPV6_0_RUN_LEN; zero_n -= IPV6_0_RUN_LEN;
            if (zero[zero_n] == ':')
                zero_n++;
            memmove(zero, zero + zero_n, strlen(zero + zero_n) + 1);
        }
        // compress four digit hex sequences by removing leading zeros
        while ((zero = strstr(addr, ":0")) != NULL)
            if (strchr(++zero, ':') == NULL) break;
            else memmove(zero, zero + 1, strlen(zero));
        if (addr[1] != ':')
            memmove(addr, addr + 1, IPV6_SIZE_MAX);  // rm ':' prefix
    }
    return addr;
}

static bool is_regular_file(const char *dir, const char *name) {
    bool is_existing = false;

    assert(dir != NULL);
    if (name != NULL) {
        struct stat st;

        if (stat(dir, &st) == 0 && S_ISDIR(st.st_mode)) {
            char *s;
            const char *const PATHNAME_FMT = "%s/%s";

            if (*name != '\0') {
                if (asprintf(&s, PATHNAME_FMT, dir, name) < 0) return false;
                is_existing = stat(s, &st) == 0 && S_ISREG(st.st_mode);
                free(s);
            }
            else errno = EISDIR;
        }
        else errno = ENOTDIR;
        if (errno == ENOENT)
            errno = 0;  // don't report X-ERRNO-%d
    }
    return is_existing;
}

typedef struct {
    bool error;
    unsigned lport, rport; char *os, *enc, *s;
} reply_t;

static const size_t USERID_MAX_LEN = 512; // as per RFC1413 <octet-string>
static const char *const TEXT_READ_MODE = "r"; // passphrase, /proc plaintext

static bool get_info(reply_t *out, request_t in, const char *tcpname) {
    //unsigned long lport, rport, uid, status; FILE *stream;
    unsigned long lport, rport, status; FILE *stream;
    long uid;
    const unsigned long ESTABLISHED = 0x01;
    unsigned lineno = 0;
    char *laddr = NULL, *raddr = NULL;
    bool is_port_pair_found = false;

    char *created_verbose(const char *name, unsigned long id) {
        size_t n; time_t tod;
        char *s, when[USERID_MAX_LEN], *host1, *port1, *host2, *port2;
        const char *const UTC_FMT = "%FT%TZ", *const TZ_FMT = "(%a %EX %z/%Z)";
        const char *const VERBOSE_FMT = "%s:%lu,%s,%s|%s,%s|%s";

        if (time(&tod) == (time_t) -1) handle_error(NULL);
        n = strftime(when, sizeof(when), UTC_FMT, gmtime(&tod));
        strftime(when + n, sizeof(when) - n, TZ_FMT, localtime(&tod));
        host1 = created_hostname(laddr); port1 = created_servicename(lport);
        host2 = created_hostname(raddr); port2 = created_servicename(rport);
        asprintf(&s, VERBOSE_FMT, name, id, when, host1, port1, host2, port2);
        free(host1); free(port1); free(host2); free(port2);
        return s;
    }

    assert(out != NULL);
    if (tcpname == NULL) return false;
    if ((stream = fopen(tcpname, TEXT_READ_MODE)) == NULL) {
        // some systems won't have tcp6 (and rarely, plain IPv4 tcp) files in
        // /proc/net. The absence of either file isn't an error; although
        // the lack of both prevents authd from looking up anything
        if (errno == ENOENT) {
            if (opt.log) log_notice("%s: %s\n", tcpname, strerror(errno));
            errno = 0;
        }
        else out->error = true;
    }
    else for (;;) {
        unsigned long sl, inode;
        uid_t euid;
        char line[PROC_MAX_LEN], tmp[PROC_MAX_LEN];

        // FIXME: setting the buffer to be larger than normal is done to
        // lessen (but not eliminate) the chance of the proc file changing
        // while in the middle of reading it
        if (0 == lineno) {
            const size_t SIZE = (size_t) (BUFSIZ * PROC_SIZE);

            if (setvbuf(stream, NULL, _IOFBF, SIZE) != 0) {
                out->error = true; break;
            }
        }
        if (fgets_unlocked(line, (int) PROC_MAX_LEN, stream) == NULL) {
            if (ferror_unlocked(stream))
                out->error = true;
            assert(feof(stream));
            break;
        }
        else lineno++;
        // first line of /proc/net/tcp* is a header that looks similar to:
        //    sl local rem st tx rx tr tm->when retrnsmt   uid  timeout inode
        if (1 == lineno) continue;
        // sample line from tcp (tcp6 just has a bigger ip address):
        //    0: 00000000:800B 00000000:0000 0A 0000:0000 00:0000 0000 0000 ...
        sl = get_tok_uint(strcpy(tmp, line), 10); // sl (base 10 uint)
        laddr = get_created_tok_addr(in.laddr);   // local (little endian hex)
        lport = get_tok_uint(NULL, 16);           // ... local port
        raddr = get_created_tok_addr(in.raddr);   // remote (little endian hex)
        rport = get_tok_uint(NULL, 16);           // ... remote port
        status = get_tok_uint(NULL, 16);          // status (01 = ESTABLISHED)
        (void) get_tok_uint(NULL, 16);            // tx_queue
        (void) get_tok_uint(NULL, 16);            // rx_queue
        (void) get_tok_uint(NULL, 16);            // tr (boolean)
        (void) get_tok_ullong(NULL, 16);            // tm->when (unit: jiffies)
        strtok(NULL, DELIM);                      // retrnsmt
        //uid = get_tok_uint(NULL, 10);             // uid (base 10 uint)
        uid = get_tok_int(NULL, 10);             // uid (base 10 int)
        strtok(NULL, DELIM);                      // timeout
        inode = get_tok_uint(NULL, 10);           // inode (base 10 uint)
        if (errno == EINVAL) {
            // format of /proc/net/tcp* has changed or it's a bogus file
            debug("%s:%u: %s", tcpname, lineno, line);  // XXX: NL terminated?
            out->error = true; errno = 0;
            out->s = opt.xerror ? vstrdup("X-PROC") : NULL; break;
        }
        else if (errno != 0) {
            // most likely an out-of-memory error
            out->error = true; break;
        }
        else if (*laddr == '\0' || *raddr == '\0') {
            // host address doesn't match peer address, so skip it
            free(laddr); free(raddr); laddr = raddr = NULL; continue;
        }
        else euid = (uid_t) uid;
        if (lport == in.lport && rport == in.rport && status == ESTABLISHED) {
            struct passwd *pwd = getpwuid(euid);

            is_port_pair_found = true;
            debug("%-14s=sl:%lu uid:%lu inode:%lu\n", tcpname, sl, uid, inode);
            if (opt.username != NULL) {
                if ((pwd = getpwnam(opt.username)) == NULL) {
                    out->error = true;
                    out->s = opt.xerror ? vstrdup("X-NAME") : NULL; break;
                }
                else euid = pwd->pw_uid;
            }
            if (pwd == NULL) { // passwd db doesn't have uid in /proc/net/tcp*
                out->error = true;
                out->s = opt.xerror ? vstrdup("X-UID") : NULL; break;
            }
            else {
                bool hidden = opt.ident != NULL;

                if (is_regular_file(pwd->pw_dir, opt.ident))
                    hidden = false;
                if (is_regular_file(pwd->pw_dir, opt.Noident))
                    hidden = true;
                if (errno) {
                    out->error = true;
                    out->s = opt.xerror ? vstrdup("X-FILE") : NULL;
                    break;
                }
                else if (hidden) {
                    out->error = true; out->s = vstrdup("HIDDEN-USER"); break;
                }
                out->s = vstrdup(pwd->pw_name);
                if (opt.number) {
                    const char *const UID_FMT = "%lu";

                    free(out->s);
                    if (asprintf(&out->s, UID_FMT, (unsigned long) euid) < 0) {
                        out->error = true; break;
                    }
                }
                if (opt.fn != 0 && *pwd->pw_gecos != '\0') {
                    char *s = vstrdup(pwd->pw_gecos);
                    size_t n = 0;

                    // gecos field usually just has 1 field, the full name--
                    // but sometimes the user's office, office phone and home
                    // phone will be in csv format
                    for (unsigned u = 0; u < opt.fn && s[n] != '\0'; u++, n++)
                        n += strcspn(s + n, ",");
                    s[--n] = '\0';
                    if (opt.number) {
                        n = strcspn(s, ",");
                        memmove(s, s + n, strlen(s + n) + 1);
                        n = strlen(out->s) + strlen(s);
                        if ((out->s = realloc(out->s, ++n)) == NULL) {
                            out->error = true; break;
                        }
                        strcat(out->s, s); free(s);
                    }
                    else { free(out->s); out->s = s; }
                }
            }
            if (opt.verbose && !out->error) {
                char *brief = out->s;

                if ((out->s = created_verbose(brief, uid)) == NULL)
                    out->error = true;
                else free(brief);
            }
            break;
        }
        else { free(laddr); free(raddr); laddr = raddr = NULL; }
    }
    free(laddr); free(raddr);
    if (stream != NULL && fclose(stream) == EOF) {
        out->error = true; out->s = NULL;
    }
    return is_port_pair_found;
}

typedef struct {
    const EVP_CIPHER *cipher; BIO *writer; BUF_MEM *buffer;
    unsigned char *key, *iv, salt[PKCS5_SALT_LEN];
} crypto_t;

static bool initialize_crypto(crypto_t *x, const char *filename) {
    struct stat file;
    bool is_initialized = false;

    assert(filename != NULL && x != NULL);
    if (stat(filename, &file) == 0) {
        FILE *stream; ssize_t len;
        const EVP_MD *const HASH = EVP_md5();   // openssl compat: enc -pass
        const size_t KEY_SIZE = EVP_CIPHER_key_length(x->cipher);
        const size_t IV_SIZE = EVP_CIPHER_iv_length(x->cipher);
        unsigned char *pass = NULL; size_t z = 0;

        if (!S_ISREG(file.st_mode)) return false;       // no dirs, devs, etc.
        if  (file.st_mode & (S_IROTH | S_IWOTH)) return false;  // no ------rw-
        if ((x->key = malloc(KEY_SIZE)) == NULL) return false;
        if ((x->iv = malloc(IV_SIZE)) == NULL) return false;
        if ((stream = fopen(filename, TEXT_READ_MODE)) == NULL) return false;
        if ((len = getline(&pass, &z, stream)) == (ssize_t) -1) return false;
        if (fclose(stream) == EOF) return false;
        if (len > 0 && pass[(size_t) (len - 1)] == '\n')
            pass[(size_t) --len] = '\0';
        if (RAND_pseudo_bytes(x->salt, sizeof(x->salt)) <= 0) return false;
        EVP_BytesToKey(x->cipher, HASH, x->salt, pass, len, 1, x->key, x->iv);
        memset(pass, 0, len);                           // XXX: crypto erase
        free(pass);
        is_initialized = true;
        if (opt.debug) {
            char line[80] = { '\0' }, *p = line;
            int n; size_t size = sizeof(line);

            n = snprintf(p, size, "salt[%2zu bytes]=", sizeof(x->salt));
            assert(n >= 0);
            for (z = 0; z < sizeof(x->salt) && n < (int) size; z++) {
                p += n; size -= n;
                n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->salt[z]);
                assert(n >= 0);
            }
            debug("%s\n", line); p = line; size = sizeof(line);
            n = snprintf(p, size, "key[%4d bits]=", (int) KEY_SIZE * CHAR_BIT);
            assert(n >= 0);
            for (z = 0; z < KEY_SIZE && n < (int) size; z++) {
                p += n; size -= n;
                n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->key[z]);
                assert(n >= 0);
            }
            debug("%s\n", line); p = line; size = sizeof(line);
            n = snprintf(p, size, "iv[%5d bits]=", (int) IV_SIZE * CHAR_BIT);
            assert(n >= 0);
            for (z = 0; z < IV_SIZE && n < (int) size; z++) {
                p += n; size -= n;
                n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->iv[z]);
                assert(n >= 0);
            }
            debug("%s\n", line);
            memset(line, 0, sizeof(line));              // XXX: crypto erase
        }
    }
    return is_initialized;
}

static char *created_ciphertext_b64(const char *s) {
    BIO *encoder; char *b64;
    const char *const MAGIC = "Salted__";       // openssl compat: enc -salt
    const char *const NL = "\n";                // is strcat()ed to plaintext
    crypto_t x = { cipher: NULL };

    assert(s != NULL);
    OpenSSL_add_all_ciphers();
    if ((x.cipher = EVP_get_cipherbyname(opt.Encrypt)) == NULL) return NULL;
    debug("cipher name   =%s\n", EVP_CIPHER_name(x.cipher));
    debug("cipher block  =%d bytes\n", EVP_CIPHER_block_size(x.cipher));
    switch (EVP_CIPHER_mode(x.cipher)) {
    case EVP_CIPH_ECB_MODE: debug("cipher mode   =%s\n", "ECB"); break;
    case EVP_CIPH_CBC_MODE: debug("cipher mode   =%s\n", "CBC"); break;
    case EVP_CIPH_CFB_MODE: debug("cipher mode   =%s\n", "CFB"); break;
    case EVP_CIPH_OFB_MODE: debug("cipher mode   =%s\n", "OFB"); break;
    case EVP_CIPH_STREAM_CIPHER: debug("cipher mode   =%s\n", "stream");
    }
    if (!initialize_crypto(&x, opt.passwd)) return NULL;
    if ((encoder = BIO_new(BIO_s_mem())) == NULL) return NULL;
    else x.writer = encoder;
    if ((encoder = BIO_new(BIO_f_base64())) == NULL) return NULL;
    else {
        BIO_set_flags(encoder, BIO_FLAGS_BASE64_NO_NL);
        x.writer = BIO_push(encoder, x.writer);
        if (BIO_write(x.writer, MAGIC, strlen(MAGIC)) <= 0) return NULL;
        if (BIO_write(x.writer, x.salt, sizeof(x.salt)) <= 0) return NULL;
    }
    if ((encoder = BIO_new(BIO_f_cipher())) == NULL) return NULL;
    else {
        BIO_set_cipher(encoder, x.cipher, x.key, x.iv, true);
        x.writer = BIO_push(encoder, x.writer);
    }
    if (BIO_write(x.writer, s, strlen(s)) <= 0) return NULL;
    if (BIO_write(x.writer, NL, strlen(NL)) <= 0) return NULL;
    if (BIO_flush(x.writer) <= 0) return NULL;
    BIO_get_mem_ptr(x.writer, &x.buffer);
    if ((b64 = calloc(x.buffer->length + 3, sizeof(char))) != NULL) {
        b64[0] = '[';                   // pidentd compat: base64 in brackets
        memcpy(b64 + 1, x.buffer->data, x.buffer->length);
        b64[x.buffer->length + 1] = ']';
    }
    BIO_free_all(x.writer); EVP_cleanup();
    free(x.key); free(x.iv);
    memset(&x, 0, sizeof(x));                           // XXX: crypto erase
    return b64;
}

static reply_t created_reply(request_t in) {
    reply_t out = { lport: 0, rport: 0 };
    bool is_invalid_port = false;
    unsigned attempts = 0;

    out.os = vstrdup(opt.other ? "OTHER" : opt.os, DFL_OS);
    out.enc = vstrdup(opt.codeset, nl_langinfo(CODESET), "X-UNKNOWN");
    if (!is_in_range(PORT_MIN, in.lport, PORT_MAX)) {
        in.lport = 0; is_invalid_port = true; errno = 0;
    }
    if (!is_in_range(PORT_MIN, in.rport, PORT_MAX)) {
        in.rport = 0; is_invalid_port = true; errno = 0;
    }
    out.lport = (unsigned) in.lport; out.rport = (unsigned) in.rport;
    if (is_invalid_port) {
        out.s = vstrdup("INVALID-PORT"); out.error = true;
    }
    // FIXME: because the /proc/net/tcp*'s updated asyncronously, it's possible
    // to miss reading a pair when reading syncronously (even with a locked
    // read & a large enough buffer). To lessen the chance of a false negative,
    // inefficiently & unreliably re-read proc more than once if not found.
    else for (;;) {
        // TODO: port pair more likely to be IPv4 than IPv6,
        // so scan IPv4 first for better performance; should be configurable
        if (get_info(&out, in, PROC_V4) || get_info(&out, in, PROC_V6)) break;
        else if (attempts++ < PROC_RETRY) {
            if (usleep(PROC_SLEEP_US) == -1) handle_error(NULL);
            continue;
        }
        out.error = true; errno = 0; out.s = vstrdup("NO-USER"); break;
    }
    if (!out.error && opt.Encrypt != NULL) {
        char *encrypted = created_ciphertext_b64(out.s);

        memset(out.s, 0, strlen(out.s));                // XXX: crypto erase
        free(out.s);
        if (encrypted == NULL) {
            int n;
            char msg[USERID_MAX_LEN], *p = msg;
            unsigned long code;
            size_t size = USERID_MAX_LEN;

            out.error = true;
            if (opt.xerror) {
                n = snprintf(p, size, "%s", "X-CRYPTO");
                assert(n >= 0);
                do if ((code = ERR_get_error()) != 0) {
                    p += n; size = (size_t) n > size ? 0 : size - n;
                    n = snprintf(p, size, "-%08lX", code);
                    assert(n >= 0);
                } while (code != 0 && (size_t) n < size);
                out.s = vstrdup(msg);
            }
            else out.s = NULL;
        }
        else out.s = encrypted;
    }
    // RFC1413: when the info field isn't usable as-is, set OS to OTHER
    if (opt.fn != 0 || opt.verbose || opt.number || opt.Encrypt != NULL) {
        free(out.os); out.os = vstrdup("OTHER");
    }
    return out;
}

static void send_reply(reply_t out) {
    char *s; int n; const char *fmt;
    // RFC 1413 says servers shouldn't use unnecessary space, but mimic
    // existing servers' behavior so inflexible clients work
    const char *const GOOD_FMT = "%u , %u : USERID : %s%.0s :%s\r\n";
    const char *const GOOD_NONASCII_FMT = "%u , %u : USERID : %s , %s :%s\r\n";
    const char *const BAD_FMT = "%u , %u : ERROR :%.0s%.0s%s%.0d\r\n";

    if (out.s == NULL || (opt.error && *out.s != 'X')) {
        out.s = "UNKNOWN-ERROR"; out.error = true;
    }
    if (strlen(out.s) > USERID_MAX_LEN || strpbrk(out.s, "\r\n") != NULL) {
        out.error = true; out.s = opt.xerror ? "X-RFC1413" : NULL;
    }
    if (out.error) {
        fmt = BAD_FMT;
        if (opt.log && errno != 0) log_warning(NULL);
        if (opt.abrupt) goto abrupt;
        if (opt.xerror && errno != 0 && *out.s != 'X')
            out.s = "X-ERRNO-";
    }
    else fmt = is_print_ascii(out.s) ? GOOD_FMT : GOOD_NONASCII_FMT;
    n = asprintf(&s, fmt, out.lport, out.rport, out.os, out.enc, out.s, errno);
    if (n < 0) handle_error(NULL);
    if (fputs_unlocked(s, stdout) == EOF) handle_error(NULL);
    else free(s);
abrupt:
    if (out.error) exit(EXIT_FAILURE); // must exit to clean alloc res
}

static void destroy_reply(reply_t *out) {
    assert(out != NULL && !out->error);
    free(out->os); free(out->enc); free(out->s); memset(out, 0, sizeof(*out));
}

static void destroy_request(request_t *in) {
    free(in->laddr); free(in->raddr); memset(in, 0, sizeof(*in));
}

static void catch_signal(int which) {
    _Exit(which == SIGALRM ? EXIT_SUCCESS : EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
#   if !defined NDEBUG
    mtrace();
#   endif
    if (setlocale(LC_ALL, "") == NULL) abort();
    errno = 0;			/* not an error for some locale files to
				   be missing */
    if (signal(SIGALRM, catch_signal) == SIG_ERR) abort();
    if (textdomain(PACKAGE) == NULL) handle_error(NULL);
    if (bindtextdomain(PACKAGE, NULL) == NULL) handle_error(NULL);
    openlog(PACKAGE, LOG_PID, LOG_DAEMON);
    create_opt(argc, argv);
    atexit(destroy_opt);
    if (opt.help) show_help();
    else if (opt.Version) show_version();
    else while (opt.multiquery-- != 0 && (opt.timeout != 0 || optind < argc)) {
        request_t in = created_request(argc, argv);
        reply_t out = created_reply(in);

        destroy_request(&in);
        send_reply(out);
        destroy_reply(&out);
    } 
    return EXIT_SUCCESS;
}