root/lib/libtacplus/taclib.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 1998, 2001, 2002, Juniper Networks, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

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

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <md5.h>
#include <netdb.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <security/pam_appl.h>
#include <security/openpam.h>

#include "taclib_private.h"

static int               add_str_8(struct tac_handle *, u_int8_t *,
                            struct tac_str *);
static int               add_str_16(struct tac_handle *, u_int16_t *,
                            struct tac_str *);
static int               protocol_version(int, int, int);
static void              close_connection(struct tac_handle *);
static int               conn_server(struct tac_handle *);
static void              crypt_msg(struct tac_handle *, struct tac_msg *);
static void             *dup_str(struct tac_handle *, const struct tac_str *,
                            size_t *);
static int               establish_connection(struct tac_handle *);
static void              free_str(struct tac_str *);
static void              generr(struct tac_handle *, const char *, ...)
                            __printflike(2, 3);
static void              gen_session_id(struct tac_msg *);
static int               get_srvr_end(struct tac_handle *);
static int               get_str(struct tac_handle *, const char *,
                                      struct tac_str *, size_t);
static void              init_str(struct tac_str *);
static int               read_timed(struct tac_handle *, void *, size_t,
                            const struct timeval *);
static int               recv_msg(struct tac_handle *);
static int               save_str(struct tac_handle *, struct tac_str *,
                            const void *, size_t);
static int               send_msg(struct tac_handle *);
static int               split(char *, char *[], int, char *, size_t);
static void             *xmalloc(struct tac_handle *, size_t);
static char             *xstrdup(struct tac_handle *, const char *);
static void              clear_srvr_avs(struct tac_handle *);
static void              create_msg(struct tac_handle *, int, int, int);

/*
 * Append some optional data to the current request, and store its
 * length into the 8-bit field referenced by "fld".  Returns 0 on
 * success, or -1 on failure.
 *
 * This function also frees the "cs" string data and initializes it
 * for the next time.
 */
static int
add_str_8(struct tac_handle *h, u_int8_t *fld, struct tac_str *cs)
{
        u_int16_t len;

        if (add_str_16(h, &len, cs) == -1)
                return -1;
        len = ntohs(len);
        if (len > 0xff) {
                generr(h, "Field too long");
                return -1;
        }
        *fld = len;
        return 0;
}

/*
 * Append some optional data to the current request, and store its
 * length into the 16-bit field (network byte order) referenced by
 * "fld".  Returns 0 on success, or -1 on failure.
 *
 * This function also frees the "cs" string data and initializes it
 * for the next time.
 */
static int
add_str_16(struct tac_handle *h, u_int16_t *fld, struct tac_str *cs)
{
        size_t len;

        len = cs->len;
        if (cs->data == NULL)
                len = 0;
        if (len != 0) {
                int offset;

                if (len > 0xffff) {
                        generr(h, "Field too long");
                        return -1;
                }
                offset = ntohl(h->request.length);
                if (offset + len > BODYSIZE) {
                        generr(h, "Message too long");
                        return -1;
                }
                memcpy(h->request.u.body + offset, cs->data, len);
                h->request.length = htonl(offset + len);
        }
        *fld = htons(len);
        free_str(cs);
        return 0;
}

static int
protocol_version(int msg_type, int var, int type)
{
    int minor;

    switch (msg_type) {
        case TAC_AUTHEN:
            /* 'var' represents the 'action' */
            switch (var) {
                case TAC_AUTHEN_LOGIN:
                    switch (type) {

                        case TAC_AUTHEN_TYPE_PAP:
                        case TAC_AUTHEN_TYPE_CHAP:
                        case TAC_AUTHEN_TYPE_MSCHAP:
                        case TAC_AUTHEN_TYPE_ARAP:
                            minor = 1;
                        break;

                        default:
                            minor = 0;
                        break;
                     }
                break;

                case TAC_AUTHEN_SENDAUTH:
                    minor = 1;
                break;

                default:
                    minor = 0;
                break;
            };
        break;

        case TAC_AUTHOR:
            /* 'var' represents the 'method' */
            switch (var) {
                /*
                 * When new authentication methods are added, include 'method'
                 * in determining the value of 'minor'.  At this point, all
                 * methods defined in this implementation (see "Authorization 
                 * authentication methods" in taclib.h) are minor version 0
                 * Not all types, however, indicate minor version 0.
                 */
                case TAC_AUTHEN_METH_NOT_SET:
                case TAC_AUTHEN_METH_NONE:
                case TAC_AUTHEN_METH_KRB5:
                case TAC_AUTHEN_METH_LINE:
                case TAC_AUTHEN_METH_ENABLE:
                case TAC_AUTHEN_METH_LOCAL:
                case TAC_AUTHEN_METH_TACACSPLUS:
                case TAC_AUTHEN_METH_RCMD:
                    switch (type) {
                        case TAC_AUTHEN_TYPE_PAP:
                        case TAC_AUTHEN_TYPE_CHAP:
                        case TAC_AUTHEN_TYPE_MSCHAP:
                        case TAC_AUTHEN_TYPE_ARAP:
                            minor = 1;
                        break;

                        default:
                            minor = 0;
                        break;
                     }
                break;
                default:
                    minor = 0;
                break;
            }
        break;

        case TAC_ACCT:

        default:
            minor = 0;
        break;
    }

    return TAC_VER_MAJOR << 4 | minor;
}


static void
close_connection(struct tac_handle *h)
{
        if (h->fd != -1) {
                close(h->fd);
                h->fd = -1;
        }
}

static int
conn_server(struct tac_handle *h)
{
        struct tac_server *srvp = &h->servers[h->cur_server];
        int flags;

        if ((h->fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
                generr(h, "Cannot create socket: %s", strerror(errno));
                return -1;
        }
        if ((flags = fcntl(h->fd, F_GETFL, 0)) == -1 ||
            fcntl(h->fd, F_SETFL, flags | O_NONBLOCK) == -1) {
                generr(h, "Cannot set non-blocking mode on socket: %s",
                    strerror(errno));
                close(h->fd);
                h->fd = -1;
                return -1;
        }
        if (connect(h->fd, (struct sockaddr *)&srvp->addr,
            sizeof srvp->addr) == 0)
                return 0;

        if (errno == EINPROGRESS) {
                fd_set wfds;
                struct timeval tv;
                int nfds;
                struct sockaddr peer;
                socklen_t errlen, peerlen;
                int err;

                /* Wait for the connection to complete. */
                FD_ZERO(&wfds);
                FD_SET(h->fd, &wfds);
                tv.tv_sec = srvp->timeout;
                tv.tv_usec = 0;
                nfds = select(h->fd + 1, NULL, &wfds, NULL, &tv);
                if (nfds == -1) {
                        generr(h, "select: %s", strerror(errno));
                        close(h->fd);
                        h->fd = -1;
                        return -1;
                }
                if (nfds == 0) {
                        generr(h, "connect: timed out");
                        close(h->fd);
                        h->fd = -1;
                        return -1;
                }

                /* See whether we are connected now. */
                peerlen = sizeof peer;
                if (getpeername(h->fd, &peer, &peerlen) == 0)
                        return 0;

                if (errno != ENOTCONN) {
                        generr(h, "getpeername: %s", strerror(errno));
                        close(h->fd);
                        h->fd = -1;
                        return -1;
                }

                /* Find out why the connect failed. */
                errlen = sizeof err;
                getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
                errno = err;
        }
        generr(h, "connect: %s", strerror(errno));
        close(h->fd);
        h->fd = -1;
        return -1;
}

/*
 * Encrypt or decrypt a message.  The operations are symmetrical.
 */
static void
crypt_msg(struct tac_handle *h, struct tac_msg *msg)
{
        const char *secret;
        MD5_CTX base_ctx;
        MD5_CTX ctx;
        unsigned char md5[16];
        int chunk;
        int msg_len;

        secret = h->servers[h->cur_server].secret;
        if (secret[0] == '\0')
                msg->flags |= TAC_UNENCRYPTED;
        if (msg->flags & TAC_UNENCRYPTED)
                return;

        msg_len = ntohl(msg->length);

        MD5Init(&base_ctx);
        MD5Update(&base_ctx, msg->session_id, sizeof msg->session_id);
        MD5Update(&base_ctx, secret, strlen(secret));
        MD5Update(&base_ctx, &msg->version, sizeof msg->version);
        MD5Update(&base_ctx, &msg->seq_no, sizeof msg->seq_no);

        ctx = base_ctx;
        for (chunk = 0;  chunk < msg_len;  chunk += sizeof md5) {
                int chunk_len;
                int i;

                MD5Final(md5, &ctx);

                if ((chunk_len = msg_len - chunk) > sizeof md5)
                        chunk_len = sizeof md5;
                for (i = 0;  i < chunk_len;  i++)
                        msg->u.body[chunk + i] ^= md5[i];

                ctx = base_ctx;
                MD5Update(&ctx, md5, sizeof md5);
        }
}

/*
 * Return a dynamically allocated copy of the given server string.
 * The copy is null-terminated.  If "len" is non-NULL, the length of
 * the string (excluding the terminating null byte) is stored via it.
 * Returns NULL on failure.  Empty strings are still allocated even
 * though they have no content.
 */
static void *
dup_str(struct tac_handle *h, const struct tac_str *ss, size_t *len)
{
        unsigned char *p;

        if ((p = (unsigned char *)xmalloc(h, ss->len + 1)) == NULL)
                return NULL;
        if (ss->data != NULL && ss->len != 0)
                memcpy(p, ss->data, ss->len);
        p[ss->len] = '\0';
        if (len != NULL)
                *len = ss->len;
        return p;
}

static int
establish_connection(struct tac_handle *h)
{
        int i;

        if (h->fd >= 0)         /* Already connected. */
                return 0;
        if (h->num_servers == 0) {
                generr(h, "No TACACS+ servers specified");
                return -1;
        }
        /*
         * Try the servers round-robin.  We begin with the one that
         * worked for us the last time.  That way, once we find a good
         * server, we won't waste any more time trying the bad ones.
         */
        for (i = 0;  i < h->num_servers;  i++) {
                if (conn_server(h) == 0) {
                        h->single_connect = (h->servers[h->cur_server].flags &
                            TAC_SRVR_SINGLE_CONNECT) != 0;
                        return 0;
                }
                if (++h->cur_server >= h->num_servers)  /* Wrap around */
                        h->cur_server = 0;
        }
        /* Just return whatever error was last reported by conn_server(). */
        return -1;
}

/*
 * Free a client string, obliterating its contents first for security.
 */
static void
free_str(struct tac_str *cs)
{
        if (cs->data != NULL) {
                memset_s(cs->data, cs->len, 0, cs->len);
                free(cs->data);
                cs->data = NULL;
                cs->len = 0;
        }
}

static void
generr(struct tac_handle *h, const char *format, ...)
{
        va_list          ap;

        va_start(ap, format);
        vsnprintf(h->errmsg, ERRSIZE, format, ap);
        va_end(ap);
}

static void
gen_session_id(struct tac_msg *msg)
{
        int r;

        r = arc4random();
        msg->session_id[0] = r >> 8;
        msg->session_id[1] = r;
        r = arc4random();
        msg->session_id[2] = r >> 8;
        msg->session_id[3] = r;
}

/*
 * Verify that we are exactly at the end of the response message.
 * Returns 0 on success, -1 on failure.
 */
static int
get_srvr_end(struct tac_handle *h)
{
        int len;

        len = ntohl(h->response.length);

        if (h->srvr_pos != len) {
                generr(h, "Invalid length field in response "
                       "from server: end expected at %u, response length %u",
                       h->srvr_pos, len);
                return -1;
        }
        return 0;
}

static int
get_str(struct tac_handle *h, const char *field,
    struct tac_str *ss, size_t len)
{
        if (h->srvr_pos + len > ntohl(h->response.length)) {
                generr(h, "Invalid length field in %s response from server "
                       "(%lu > %lu)", field, (u_long)(h->srvr_pos + len),
                       (u_long)ntohl(h->response.length));
                return -1;
        }
        ss->data = len != 0 ? h->response.u.body + h->srvr_pos : NULL;
        ss->len = len;
        h->srvr_pos += len;
        return 0;
}

static void
init_str(struct tac_str *cs)
{
        cs->data = NULL;
        cs->len = 0;
}

static int
read_timed(struct tac_handle *h, void *buf, size_t len,
    const struct timeval *deadline)
{
        char *ptr;

        ptr = (char *)buf;
        while (len > 0) {
                int n;

                n = read(h->fd, ptr, len);
                if (n == -1) {
                        struct timeval tv;
                        int nfds;

                        if (errno != EAGAIN) {
                                generr(h, "Network read error: %s",
                                    strerror(errno));
                                return -1;
                        }

                        /* Wait until we can read more data. */
                        gettimeofday(&tv, NULL);
                        timersub(deadline, &tv, &tv);
                        if (tv.tv_sec >= 0) {
                                fd_set rfds;

                                FD_ZERO(&rfds);
                                FD_SET(h->fd, &rfds);
                                nfds =
                                    select(h->fd + 1, &rfds, NULL, NULL, &tv);
                                if (nfds == -1) {
                                        generr(h, "select: %s",
                                            strerror(errno));
                                        return -1;
                                }
                        } else
                                nfds = 0;
                        if (nfds == 0) {
                                generr(h, "Network read timed out");
                                return -1;
                        }
                } else if (n == 0) {
                        generr(h, "unexpected EOF from server");
                        return -1;
                } else {
                        ptr += n;
                        len -= n;
                }
        }
        return 0;
}

/*
 * Receive a response from the server and decrypt it.  Returns 0 on
 * success, or -1 on failure.
 */
static int
recv_msg(struct tac_handle *h)
{
        struct timeval deadline;
        struct tac_msg *msg;
        u_int32_t len;

        msg = &h->response;
        gettimeofday(&deadline, NULL);
        deadline.tv_sec += h->servers[h->cur_server].timeout;

        /* Read the message header and make sure it is reasonable. */
        if (read_timed(h, msg, HDRSIZE, &deadline) == -1)
                return -1;
        if (memcmp(msg->session_id, h->request.session_id,
            sizeof msg->session_id) != 0) {
                generr(h, "Invalid session ID in received message");
                return -1;
        }
        if (msg->type != h->request.type) {
                generr(h, "Invalid type in received message"
                          " (got %u, expected %u)",
                          msg->type, h->request.type);
                return -1;
        }
        len = ntohl(msg->length);
        if (len > BODYSIZE) {
                generr(h, "Received message too large (%u > %u)",
                          len, BODYSIZE);
                return -1;
        }
        if (msg->seq_no != ++h->last_seq_no) {
                generr(h, "Invalid sequence number in received message"
                          " (got %u, expected %u)",
                          msg->seq_no, h->last_seq_no);
                return -1;
        }

        /* Read the message body. */
        if (read_timed(h, msg->u.body, len, &deadline) == -1)
                return -1;

        /* Decrypt it. */
        crypt_msg(h, msg);

        /*
         * Turn off single-connection mode if the server isn't amenable
         * to it.
         */
        if (!(msg->flags & TAC_SINGLE_CONNECT))
                h->single_connect = 0;
        return 0;
}

static int
save_str(struct tac_handle *h, struct tac_str *cs, const void *data,
    size_t len)
{
        free_str(cs);
        if (data != NULL && len != 0) {
                if ((cs->data = xmalloc(h, len)) == NULL)
                        return -1;
                cs->len = len;
                memcpy(cs->data, data, len);
        }
        return 0;
}

/*
 * Send the current request, after encrypting it.  Returns 0 on success,
 * or -1 on failure.
 */
static int
send_msg(struct tac_handle *h)
{
        struct timeval deadline;
        struct tac_msg *msg;
        char *ptr;
        int len;

        if (h->last_seq_no & 1) {
                generr(h, "Attempt to send message out of sequence");
                return -1;
        }

        if (establish_connection(h) == -1)
                return -1;

        msg = &h->request;
        msg->seq_no = ++h->last_seq_no;
        if (msg->seq_no == 1)
                gen_session_id(msg);
        crypt_msg(h, msg);

        if (h->single_connect)
                msg->flags |= TAC_SINGLE_CONNECT;
        else
                msg->flags &= ~TAC_SINGLE_CONNECT;
        gettimeofday(&deadline, NULL);
        deadline.tv_sec += h->servers[h->cur_server].timeout;
        len = HDRSIZE + ntohl(msg->length);
        ptr = (char *)msg;
        while (len > 0) {
                int n;

                n = write(h->fd, ptr, len);
                if (n == -1) {
                        struct timeval tv;
                        int nfds;

                        if (errno != EAGAIN) {
                                generr(h, "Network write error: %s",
                                    strerror(errno));
                                return -1;
                        }

                        /* Wait until we can write more data. */
                        gettimeofday(&tv, NULL);
                        timersub(&deadline, &tv, &tv);
                        if (tv.tv_sec >= 0) {
                                fd_set wfds;

                                FD_ZERO(&wfds);
                                FD_SET(h->fd, &wfds);
                                nfds =
                                    select(h->fd + 1, NULL, &wfds, NULL, &tv);
                                if (nfds == -1) {
                                        generr(h, "select: %s",
                                            strerror(errno));
                                        return -1;
                                }
                        } else
                                nfds = 0;
                        if (nfds == 0) {
                                generr(h, "Network write timed out");
                                return -1;
                        }
                } else {
                        ptr += n;
                        len -= n;
                }
        }
        return 0;
}

static int
tac_add_server_av(struct tac_handle *h, const char *host, int port,
    const char *secret, int timeout, int flags, const char *const *avs)
{
        struct tac_server *srvp;
        const char *p;
        size_t len;
        int i;

        if (h->num_servers >= MAXSERVERS) {
                generr(h, "Too many TACACS+ servers specified");
                return -1;
        }
        srvp = &h->servers[h->num_servers];

        memset(&srvp->addr, 0, sizeof srvp->addr);
        srvp->addr.sin_len = sizeof srvp->addr;
        srvp->addr.sin_family = AF_INET;
        if (!inet_aton(host, &srvp->addr.sin_addr)) {
                struct hostent *hent;

                if ((hent = gethostbyname(host)) == NULL) {
                        generr(h, "%s: host not found", host);
                        return -1;
                }
                memcpy(&srvp->addr.sin_addr, hent->h_addr,
                    sizeof srvp->addr.sin_addr);
        }
        srvp->addr.sin_port = htons(port != 0 ? port : TACPLUS_PORT);
        if ((srvp->secret = xstrdup(h, secret)) == NULL)
                return -1;
        srvp->timeout = timeout;
        srvp->flags = flags;
        srvp->navs = 0;
        for (i = 0; avs[i] != NULL; i++) {
                if (i >= MAXAVPAIRS) {
                        generr(h, "too many AV pairs");
                        goto fail;
                }
                for (p = avs[i], len = 0; is_arg(*p); p++)
                        len++;
                if (p == avs[i] || *p != '=') {
                        generr(h, "invalid AV pair %d", i);
                        goto fail;
                }
                while (*p++ != '\0')
                        len++;
                if ((srvp->avs[i].data = xstrdup(h, avs[i])) == NULL)
                        goto fail;
                srvp->avs[i].len = len;
                srvp->navs++;
        }
        h->num_servers++;
        return 0;
fail:
        memset_s(srvp->secret, strlen(srvp->secret), 0, strlen(srvp->secret));
        free(srvp->secret);
        srvp->secret = NULL;
        for (i = 0; i < srvp->navs; i++) {
                free(srvp->avs[i].data);
                srvp->avs[i].data = NULL;
                srvp->avs[i].len = 0;
        }
        return -1;
}

int
tac_add_server(struct tac_handle *h, const char *host, int port,
    const char *secret, int timeout, int flags)
{
        const char *const *avs = { NULL };

        return tac_add_server_av(h, host, port, secret, timeout, flags, avs);
}

void
tac_close(struct tac_handle *h)
{
        int i, srv;

        if (h->fd != -1)
                close(h->fd);
        for (srv = 0;  srv < h->num_servers;  srv++) {
                memset(h->servers[srv].secret, 0,
                    strlen(h->servers[srv].secret));
                free(h->servers[srv].secret);
        }
        free_str(&h->user);
        free_str(&h->port);
        free_str(&h->rem_addr);
        free_str(&h->data);
        free_str(&h->user_msg);
        for (i=0; i<MAXAVPAIRS; i++)
                free_str(&(h->avs[i]));

        /* Clear everything else before freeing memory */
        memset(h, 0, sizeof(struct tac_handle));
        free(h);
}

static void
freev(char **fields, int nfields)
{
        if (fields != NULL) {
                while (nfields-- > 0)
                        free(fields[nfields]);
                free(fields);
        }
}

int
tac_config(struct tac_handle *h, const char *path)
{
        FILE *fp;
        char **fields;
        int linenum, nfields;
        int retval;

        if (path == NULL)
                path = PATH_TACPLUS_CONF;
        if ((fp = fopen(path, "r")) == NULL) {
                generr(h, "Cannot open \"%s\": %s", path, strerror(errno));
                return -1;
        }
        retval = 0;
        linenum = nfields = 0;
        fields = NULL;
        while ((fields = openpam_readlinev(fp, &linenum, &nfields)) != NULL) {
                char *host, *res;
                char *port_str;
                char *secret;
                char *timeout_str;
                char *end;
                unsigned long timeout;
                int port;
                int options;
                int i;

                if (nfields == 0) {
                        freev(fields, nfields);
                        continue;
                }
                if (nfields < 2) {
                        generr(h, "%s:%d: missing shared secret", path,
                            linenum);
                        retval = -1;
                        break;
                }
                host = fields[0];
                secret = fields[1];

                /* Parse and validate the fields. */
                res = host;
                host = strsep(&res, ":");
                port_str = strsep(&res, ":");
                if (port_str != NULL) {
                        port = strtoul(port_str, &end, 10);
                        if (port_str[0] == '\0' || *end != '\0') {
                                generr(h, "%s:%d: invalid port", path,
                                    linenum);
                                retval = -1;
                                break;
                        }
                } else
                        port = 0;
                i = 2;
                if (nfields > i && strlen(fields[i]) > 0 &&
                    strspn(fields[i], "0123456789") == strlen(fields[i])) {
                        timeout_str = fields[i];
                        timeout = strtoul(timeout_str, &end, 10);
                        if (timeout_str[0] == '\0' || *end != '\0') {
                                generr(h, "%s:%d: invalid timeout", path,
                                    linenum);
                                retval = -1;
                                break;
                        }
                        i++;
                } else
                        timeout = TIMEOUT;
                options = 0;
                if (nfields > i &&
                    strcmp(fields[i], "single-connection") == 0) {
                        options |= TAC_SRVR_SINGLE_CONNECT;
                        i++;
                }
                if (tac_add_server_av(h, host, port, secret, timeout,
                    options, (const char *const *)(fields + i)) == -1) {
                        char msg[ERRSIZE];

                        strcpy(msg, h->errmsg);
                        generr(h, "%s:%d: %s", path, linenum, msg);
                        retval = -1;
                        break;
                }
                memset_s(secret, strlen(secret), 0, strlen(secret));
                freev(fields, nfields);
        }
        freev(fields, nfields);
        fclose(fp);
        return retval;
}

int
tac_create_authen(struct tac_handle *h, int action, int type, int service)
{
        struct tac_authen_start *as;

        create_msg(h, TAC_AUTHEN, action, type);

        as = &h->request.u.authen_start;
        as->action = action;
        as->priv_lvl = TAC_PRIV_LVL_USER;
        as->authen_type = type;
        as->service = service;

        return 0;
}

int
tac_create_author(struct tac_handle *h, int method, int type, int service)
{
        struct tac_author_request *areq;

        create_msg(h, TAC_AUTHOR, method, type);

        areq = &h->request.u.author_request;
        areq->authen_meth = method;
        areq->priv_lvl = TAC_PRIV_LVL_USER;
        areq->authen_type = type;
        areq->service = service;

        return 0;
}

int
tac_create_acct(struct tac_handle *h, int acct, int action, int type, int service)
{
        struct tac_acct_start *as;

        create_msg(h, TAC_ACCT, action, type);

        as = &h->request.u.acct_start;
        as->action = acct;
        as->authen_action = action;
        as->priv_lvl = TAC_PRIV_LVL_USER;
        as->authen_type = type;
        as->authen_service = service;

        return 0;
}

static void
create_msg(struct tac_handle *h, int msg_type, int var, int type)
{
        struct tac_msg *msg;
        int i;

        h->last_seq_no = 0;

        msg = &h->request;
        msg->type = msg_type;
        msg->version = protocol_version(msg_type, var, type);
        msg->flags = 0; /* encrypted packet body */

        free_str(&h->user);
        free_str(&h->port);
        free_str(&h->rem_addr);
        free_str(&h->data);
        free_str(&h->user_msg);

        for (i=0; i<MAXAVPAIRS; i++)
                free_str(&(h->avs[i]));
}

void *
tac_get_data(struct tac_handle *h, size_t *len)
{
        return dup_str(h, &h->srvr_data, len);
}

char *
tac_get_msg(struct tac_handle *h)
{
        return dup_str(h, &h->srvr_msg, NULL);
}

/*
 * Create and initialize a tac_handle structure, and return it to the
 * caller.  Can fail only if the necessary memory cannot be allocated.
 * In that case, it returns NULL.
 */
struct tac_handle *
tac_open(void)
{
        int i;
        struct tac_handle *h;

        h = (struct tac_handle *)malloc(sizeof(struct tac_handle));
        if (h != NULL) {
                h->fd = -1;
                h->num_servers = 0;
                h->cur_server = 0;
                h->errmsg[0] = '\0';
                init_str(&h->user);
                init_str(&h->port);
                init_str(&h->rem_addr);
                init_str(&h->data);
                init_str(&h->user_msg);
                for (i=0; i<MAXAVPAIRS; i++) {
                        init_str(&(h->avs[i]));
                        init_str(&(h->srvr_avs[i]));
                }
                init_str(&h->srvr_msg);
                init_str(&h->srvr_data);
        }
        return h;
}

int
tac_send_authen(struct tac_handle *h)
{
        struct tac_authen_reply *ar;

        if (h->num_servers == 0)
            return -1;

        if (h->last_seq_no == 0) {      /* Authentication START packet */
                struct tac_authen_start *as;

                as = &h->request.u.authen_start;
                h->request.length =
                    htonl(offsetof(struct tac_authen_start, rest[0]));
                if (add_str_8(h, &as->user_len, &h->user) == -1 ||
                    add_str_8(h, &as->port_len, &h->port) == -1 ||
                    add_str_8(h, &as->rem_addr_len, &h->rem_addr) == -1 ||
                    add_str_8(h, &as->data_len, &h->data) == -1)
                        return -1;
        } else {                        /* Authentication CONTINUE packet */
                struct tac_authen_cont *ac;

                ac = &h->request.u.authen_cont;
                ac->flags = 0;
                h->request.length =
                    htonl(offsetof(struct tac_authen_cont, rest[0]));
                if (add_str_16(h, &ac->user_msg_len, &h->user_msg) == -1 ||
                    add_str_16(h, &ac->data_len, &h->data) == -1)
                        return -1;
        }

        /* Send the message and retrieve the reply. */
        if (send_msg(h) == -1 || recv_msg(h) == -1)
                return -1;

        /* Scan the optional fields in the reply. */
        ar = &h->response.u.authen_reply;
        h->srvr_pos = offsetof(struct tac_authen_reply, rest[0]);
        if (get_str(h, "msg", &h->srvr_msg, ntohs(ar->msg_len)) == -1 ||
            get_str(h, "data", &h->srvr_data, ntohs(ar->data_len)) == -1 ||
            get_srvr_end(h) == -1)
                return -1;

        if (!h->single_connect &&
            ar->status != TAC_AUTHEN_STATUS_GETDATA &&
            ar->status != TAC_AUTHEN_STATUS_GETUSER &&
            ar->status != TAC_AUTHEN_STATUS_GETPASS)
                close_connection(h);

        return ar->flags << 8 | ar->status;
}

int
tac_send_author(struct tac_handle *h)
{
        int i, current;
        char dbgstr[64];
        struct tac_author_request *areq = &h->request.u.author_request;
        struct tac_author_response *ares = &h->response.u.author_response;
        struct tac_server *srvp;

        h->request.length =
                htonl(offsetof(struct tac_author_request, rest[0]));

        /* Count each specified AV pair */
        for (areq->av_cnt=0, i=0; i<MAXAVPAIRS; i++)
                if (h->avs[i].len && h->avs[i].data)
                        areq->av_cnt++;

        /*
         * Each AV size is a byte starting right after 'av_cnt'.  Update the
         * offset to include these AV sizes.
         */
        h->request.length = ntohl(htonl(h->request.length) + areq->av_cnt);

        /* Now add the string arguments from 'h' */
        if (add_str_8(h, &areq->user_len, &h->user) == -1 ||
            add_str_8(h, &areq->port_len, &h->port) == -1 ||
            add_str_8(h, &areq->rem_addr_len, &h->rem_addr) == -1)
                return -1;

        /* Add each AV pair, the size of each placed in areq->rest[current] */
        for (current=0, i=0; i<MAXAVPAIRS; i++) {
                if (h->avs[i].len && h->avs[i].data) {
                        if (add_str_8(h, &areq->rest[current++],
                                      &(h->avs[i])) == -1)
                                return -1;
                }
        }

        /* Send the message and retrieve the reply. */
        if (send_msg(h) == -1 || recv_msg(h) == -1)
                return -1;
        srvp = &h->servers[h->cur_server];

        /* Update the offset in the response packet based on av pairs count */
        h->srvr_pos = offsetof(struct tac_author_response, rest[0]) +
                ares->av_cnt;

        /* Scan the optional fields in the response. */
        if (get_str(h, "msg", &h->srvr_msg, ntohs(ares->msg_len)) == -1 ||
            get_str(h, "data", &h->srvr_data, ntohs(ares->data_len)) ==-1)
                return -1;

        /* Get each AV pair (just setting pointers, not malloc'ing) */
        clear_srvr_avs(h);
        for (i=0; i<ares->av_cnt; i++) {
                snprintf(dbgstr, sizeof dbgstr, "av-pair-%d", i);
                if (get_str(h, dbgstr, &(h->srvr_avs[i]),
                                 ares->rest[i]) == -1)
                        return -1;
                h->srvr_navs++;
        }

        /* Should have ended up at the end */
        if (get_srvr_end(h) == -1)
                return -1;

        /* Sanity checks */
        if (!h->single_connect)
                close_connection(h);

        return (h->srvr_navs + srvp->navs) << 8 | ares->status;
}

int
tac_send_acct(struct tac_handle *h)
{
        register int i, current;
        struct tac_acct_start *as = &h->request.u.acct_start;
        struct tac_acct_reply *ar = &h->response.u.acct_reply;

        /* start */
        as = &h->request.u.acct_start;
        h->request.length = htonl(offsetof(struct tac_acct_start, rest[0]));
        for (as->av_cnt = 0, i = 0; i < MAXAVPAIRS; i++)
                if (h->avs[i].len && h->avs[i].data)
                        as->av_cnt++;
        h->request.length = ntohl(htonl(h->request.length) + as->av_cnt);

        if (add_str_8(h, &as->user_len, &h->user) == -1 ||
            add_str_8(h, &as->port_len, &h->port) == -1 ||
            add_str_8(h, &as->rem_addr_len, &h->rem_addr) == -1)
                return -1;

        for (i = current = 0; i < MAXAVPAIRS; i++)
                if (h->avs[i].len && h->avs[i].data)
                        if (add_str_8(h, &as->rest[current++], &(h->avs[i])) == -1)
                                return -1;

        /* send */
        if (send_msg(h) == -1 || recv_msg(h) == -1)
                return -1;

        /* reply */
        h->srvr_pos = offsetof(struct tac_acct_reply, rest[0]);
        if (get_str(h, "msg", &h->srvr_msg, ntohs(ar->msg_len)) == -1 ||
            get_str(h, "data", &h->srvr_data, ntohs(ar->data_len)) == -1 ||
            get_srvr_end(h) == -1)
                return -1;

        /* Sanity checks */
        if (!h->single_connect)
                close_connection(h);

        return ar->status;
}

int
tac_set_rem_addr(struct tac_handle *h, const char *addr)
{
        return save_str(h, &h->rem_addr, addr, addr != NULL ? strlen(addr) : 0);
}

int
tac_set_data(struct tac_handle *h, const void *data, size_t data_len)
{
        return save_str(h, &h->data, data, data_len);
}

int
tac_set_msg(struct tac_handle *h, const char *msg)
{
        return save_str(h, &h->user_msg, msg, msg != NULL ? strlen(msg) : 0);
}

int
tac_set_port(struct tac_handle *h, const char *port)
{
        return save_str(h, &h->port, port, port != NULL ? strlen(port) : 0);
}

int
tac_set_priv(struct tac_handle *h, int priv)
{
        if (!(TAC_PRIV_LVL_MIN <= priv && priv <= TAC_PRIV_LVL_MAX)) {
                generr(h, "Attempt to set invalid privilege level");
                return -1;
        }
        h->request.u.authen_start.priv_lvl = priv;
        return 0;
}

int
tac_set_user(struct tac_handle *h, const char *user)
{
        return save_str(h, &h->user, user, user != NULL ? strlen(user) : 0);
}

int
tac_set_av(struct tac_handle *h, u_int index, const char *av)
{
        if (index >= MAXAVPAIRS)
                return -1;
        return save_str(h, &(h->avs[index]), av, av != NULL ? strlen(av) : 0);
}

char *
tac_get_av(struct tac_handle *h, u_int index)
{
        struct tac_server *srvp;

        if (index < h->srvr_navs)
                return dup_str(h, &h->srvr_avs[index], NULL);
        index -= h->srvr_navs;
        srvp = &h->servers[h->cur_server];
        if (index < srvp->navs)
                return xstrdup(h, srvp->avs[index].data);
        return NULL;
}

char *
tac_get_av_value(struct tac_handle *h, const char *attribute)
{
        int i, attr_len;
        int   found_seperator;
        char *ch, *end;
        struct tac_str *candidate;
        struct tac_str value;
        struct tac_server *srvp = &h->servers[h->cur_server];

        if (attribute == NULL || (attr_len = strlen(attribute)) == 0)
                return NULL;

        for (i = 0; i < h->srvr_navs + srvp->navs; i++) {
                if (i < h->srvr_navs)
                        candidate = &h->srvr_avs[i];
                else
                        candidate = &srvp->avs[i - h->srvr_navs];

                if (attr_len < candidate->len &&
                    strncmp(candidate->data, attribute, attr_len) == 0) {

                        ch = candidate->data + attr_len;
                        end = candidate->data + candidate->len;

                        /*
                         * Sift out the white space between A and V (should not
                         * be any, but don't trust implementation of server...)
                         */
                        found_seperator = 0;
                        while ((*ch == '=' || *ch == '*' || *ch == ' ' ||
                                *ch == '\t') && ch != end) {
                                if (*ch == '=' || *ch == '*')
                                        found_seperator++;
                                ch++;
                        }

                        /*
                         * Note:
                         *     The case of 'attribute' == "foo" and
                         *     h->srvr_avs[0] = "foobie=var1"
                         *     h->srvr_avs[1] = "foo=var2"
                         * is handled.
                         *
                         * Note that for empty string attribute values a
                         * 0-length string is returned in order to distinguish
                         * against unset values.
                         * dup_str() will handle srvr.len == 0 correctly.
                         */
                        if (found_seperator == 1) {
                                value.len = end - ch;
                                value.data = ch;
                                return dup_str(h, &value, NULL);
                        }
                }
        }
        return NULL;
}

void
tac_clear_avs(struct tac_handle *h)
{
        int i;
        for (i=0; i<MAXAVPAIRS; i++)
                save_str(h, &(h->avs[i]), NULL, 0);
}

static void
clear_srvr_avs(struct tac_handle *h)
{
        int i;

        for (i = 0; i < h->srvr_navs; i++)
                init_str(&(h->srvr_avs[i]));
        h->srvr_navs = 0;
}


const char *
tac_strerror(struct tac_handle *h)
{
        return h->errmsg;
}

static void *
xmalloc(struct tac_handle *h, size_t size)
{
        void *r;

        if ((r = malloc(size)) == NULL)
                generr(h, "Out of memory");
        return r;
}

static char *
xstrdup(struct tac_handle *h, const char *s)
{
        char *r;

        if ((r = strdup(s)) == NULL)
                generr(h, "Out of memory");
        return r;
}