/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krb5/os/dnsglue.c */
/*
 * Copyright 2004, 2009 by the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#include "autoconf.h"
#ifdef KRB5_DNS_LOOKUP

#include "dnsglue.h"
#ifdef __APPLE__
#include <dns.h>
#endif

/*
 * Only use res_ninit() if there's also a res_ndestroy(), to avoid
 * memory leaks (Linux & Solaris) and outright corruption (AIX 4.x,
 * 5.x).  While we're at it, make sure res_nsearch() is there too.
 *
 * In any case, it is probable that platforms having broken
 * res_ninit() will have thread safety hacks for res_init() and _res.
 */

/*
 * Opaque handle
 */
struct krb5int_dns_state {
    int nclass;
    int ntype;
    void *ansp;
    int anslen;
    int ansmax;
#if HAVE_NS_INITPARSE
    int cur_ans;
    ns_msg msg;
#else
    unsigned char *ptr;
    unsigned short nanswers;
#endif
};

#if !HAVE_NS_INITPARSE
static int initparse(struct krb5int_dns_state *);
#endif

/*
 * Define macros to use the best available DNS search functions.  INIT_HANDLE()
 * returns true if handle initialization is successful, false if it is not.
 * SEARCH() returns the length of the response or -1 on error.
 * DECLARE_HANDLE() must be used last in the declaration list since it may
 * evaluate to nothing.
 */

#if defined(__APPLE__)

/* Use the OS X interfaces dns_open, dns_search, and dns_free. */
#define DECLARE_HANDLE(h) dns_handle_t h
#define INIT_HANDLE(h) ((h = dns_open(NULL)) != NULL)
#define SEARCH(h, n, c, t, a, l) dns_search(h, n, c, t, a, l, NULL, NULL)
#define DESTROY_HANDLE(h) dns_free(h)

#elif HAVE_RES_NINIT && HAVE_RES_NSEARCH

/* Use res_ninit, res_nsearch, and res_ndestroy or res_nclose. */
#define DECLARE_HANDLE(h) struct __res_state h
#define INIT_HANDLE(h) (memset(&h, 0, sizeof(h)), res_ninit(&h) == 0)
#define SEARCH(h, n, c, t, a, l) res_nsearch(&h, n, c, t, a, l)
#if HAVE_RES_NDESTROY
#define DESTROY_HANDLE(h) res_ndestroy(&h)
#else
#define DESTROY_HANDLE(h) res_nclose(&h)
#endif

#else

/* Use res_init and res_search. */
#define DECLARE_HANDLE(h)
#define INIT_HANDLE(h) (res_init() == 0)
#define SEARCH(h, n, c, t, a, l) res_search(n, c, t, a, l)
#define DESTROY_HANDLE(h)

#endif

/*
 * krb5int_dns_init()
 *
 * Initialize an opaque handle.  Do name lookup and initial parsing of
 * reply, skipping question section.  Prepare to iterate over answer
 * section.  Returns -1 on error, 0 on success.
 */
int
krb5int_dns_init(struct krb5int_dns_state **dsp,
                 char *host, int nclass, int ntype)
{
    struct krb5int_dns_state *ds;
    int len, ret;
    size_t nextincr, maxincr;
    unsigned char *p;
    DECLARE_HANDLE(h);

    *dsp = ds = malloc(sizeof(*ds));
    if (ds == NULL)
        return -1;

    ret = -1;
    ds->nclass = nclass;
    ds->ntype = ntype;
    ds->ansp = NULL;
    ds->anslen = 0;
    ds->ansmax = 0;
    nextincr = 2048;
    maxincr = INT_MAX;

#if HAVE_NS_INITPARSE
    ds->cur_ans = 0;
#endif

    if (!INIT_HANDLE(h))
        return -1;

    do {
        p = (ds->ansp == NULL)
            ? malloc(nextincr) : realloc(ds->ansp, nextincr);

        if (p == NULL) {
            ret = -1;
            goto errout;
        }
        ds->ansp = p;
        ds->ansmax = nextincr;

        len = SEARCH(h, host, ds->nclass, ds->ntype, ds->ansp, ds->ansmax);
        if ((size_t) len > maxincr) {
            ret = -1;
            goto errout;
        }
        while (nextincr < (size_t) len)
            nextincr *= 2;
        if (len < 0 || nextincr > maxincr) {
            ret = -1;
            goto errout;
        }
    } while (len > ds->ansmax);

    ds->anslen = len;
#if HAVE_NS_INITPARSE
    ret = ns_initparse(ds->ansp, ds->anslen, &ds->msg);
#else
    ret = initparse(ds);
#endif
    if (ret < 0)
        goto errout;

    ret = 0;

errout:
    DESTROY_HANDLE(h);
    if (ret < 0) {
        if (ds->ansp != NULL) {
            free(ds->ansp);
            ds->ansp = NULL;
        }
    }

    return ret;
}

#if HAVE_NS_INITPARSE
/*
 * krb5int_dns_nextans - get next matching answer record
 *
 * Sets pp to NULL if no more records.  Returns -1 on error, 0 on
 * success.
 */
int
krb5int_dns_nextans(struct krb5int_dns_state *ds,
                    const unsigned char **pp, int *lenp)
{
    int len;
    ns_rr rr;

    *pp = NULL;
    *lenp = 0;
    while (ds->cur_ans < ns_msg_count(ds->msg, ns_s_an)) {
        len = ns_parserr(&ds->msg, ns_s_an, ds->cur_ans, &rr);
        if (len < 0)
            return -1;
        ds->cur_ans++;
        if (ds->nclass == (int)ns_rr_class(rr)
            && ds->ntype == (int)ns_rr_type(rr)) {
            *pp = ns_rr_rdata(rr);
            *lenp = ns_rr_rdlen(rr);
            return 0;
        }
    }
    return 0;
}
#endif

/*
 * krb5int_dns_expand - wrapper for dn_expand()
 */
int
krb5int_dns_expand(struct krb5int_dns_state *ds, const unsigned char *p,
                   char *buf, int len)
{

#if HAVE_NS_NAME_UNCOMPRESS
    return ns_name_uncompress(ds->ansp,
                              (unsigned char *)ds->ansp + ds->anslen,
                              p, buf, (size_t)len);
#else
    return dn_expand(ds->ansp,
                     (unsigned char *)ds->ansp + ds->anslen,
                     p, buf, len);
#endif
}

/*
 * Free stuff.
 */
void
krb5int_dns_fini(struct krb5int_dns_state *ds)
{
    if (ds == NULL)
        return;
    if (ds->ansp != NULL)
        free(ds->ansp);
    free(ds);
}

/*
 * Compat routines for BIND 4
 */
#if !HAVE_NS_INITPARSE

/*
 * initparse
 *
 * Skip header and question section of reply.  Set a pointer to the
 * beginning of the answer section, and prepare to iterate over
 * answer records.
 */
static int
initparse(struct krb5int_dns_state *ds)
{
    HEADER *hdr;
    unsigned char *p;
    unsigned short nqueries, nanswers;
    int len;
#if !HAVE_DN_SKIPNAME
    char host[MAXDNAME];
#endif

    if ((size_t) ds->anslen < sizeof(HEADER))
        return -1;

    hdr = (HEADER *)ds->ansp;
    p = ds->ansp;
    nqueries = ntohs((unsigned short)hdr->qdcount);
    nanswers = ntohs((unsigned short)hdr->ancount);
    p += sizeof(HEADER);

    /*
     * Skip query records.
     */
    while (nqueries--) {
#if HAVE_DN_SKIPNAME
        len = dn_skipname(p, (unsigned char *)ds->ansp + ds->anslen);
#else
        len = dn_expand(ds->ansp, (unsigned char *)ds->ansp + ds->anslen,
                        p, host, sizeof(host));
#endif
        if (len < 0 || !INCR_OK(ds->ansp, ds->anslen, p, len + 4))
            return -1;
        p += len + 4;
    }
    ds->ptr = p;
    ds->nanswers = nanswers;
    return 0;
}

/*
 * krb5int_dns_nextans() - get next answer record
 *
 * Sets pp to NULL if no more records.
 */
int
krb5int_dns_nextans(struct krb5int_dns_state *ds,
                    const unsigned char **pp, int *lenp)
{
    int len;
    unsigned char *p;
    unsigned short ntype, nclass, rdlen;
#if !HAVE_DN_SKIPNAME
    char host[MAXDNAME];
#endif

    *pp = NULL;
    *lenp = 0;
    p = ds->ptr;

    while (ds->nanswers--) {
#if HAVE_DN_SKIPNAME
        len = dn_skipname(p, (unsigned char *)ds->ansp + ds->anslen);
#else
        len = dn_expand(ds->ansp, (unsigned char *)ds->ansp + ds->anslen,
                        p, host, sizeof(host));
#endif
        if (len < 0 || !INCR_OK(ds->ansp, ds->anslen, p, len))
            return -1;
        p += len;
        SAFE_GETUINT16(ds->ansp, ds->anslen, p, 2, ntype, out);
        /* Also skip 4 bytes of TTL */
        SAFE_GETUINT16(ds->ansp, ds->anslen, p, 6, nclass, out);
        SAFE_GETUINT16(ds->ansp, ds->anslen, p, 2, rdlen, out);

        if (!INCR_OK(ds->ansp, ds->anslen, p, rdlen))
            return -1;
        if (rdlen > INT_MAX)
            return -1;
        if (nclass == ds->nclass && ntype == ds->ntype) {
            *pp = p;
            *lenp = rdlen;
            ds->ptr = p + rdlen;
            return 0;
        }
        p += rdlen;
    }
    return 0;
out:
    return -1;
}

#endif

/*
 * Try to look up a TXT record pointing to a Kerberos realm
 */

krb5_error_code
k5_try_realm_txt_rr(krb5_context context, const char *prefix, const char *name,
                    char **realm)
{
    krb5_error_code retval = KRB5_ERR_HOST_REALM_UNKNOWN;
    const unsigned char *p, *base;
    char host[MAXDNAME];
    int ret, rdlen, len;
    struct krb5int_dns_state *ds = NULL;
    struct k5buf buf;

    /*
     * Form our query, and send it via DNS
     */

    k5_buf_init_fixed(&buf, host, sizeof(host));
    if (name == NULL || name[0] == '\0') {
        k5_buf_add(&buf, prefix);
    } else {
        k5_buf_add_fmt(&buf, "%s.%s", prefix, name);

        /* Realm names don't (normally) end with ".", but if the query
           doesn't end with "." and doesn't get an answer as is, the
           resolv code will try appending the local domain.  Since the
           realm names are absolutes, let's stop that.

           But only if a name has been specified.  If we are performing
           a search on the prefix alone then the intention is to allow
           the local domain or domain search lists to be expanded.
        */

        len = k5_buf_len(&buf);
        if (len > 0 && host[len - 1] != '.')
            k5_buf_add(&buf, ".");
    }
    if (k5_buf_data(&buf) == NULL)
        return KRB5_ERR_HOST_REALM_UNKNOWN;
    ret = krb5int_dns_init(&ds, host, C_IN, T_TXT);
    if (ret < 0) {
        TRACE_TXT_LOOKUP_NOTFOUND(context, host);
        goto errout;
    }

    ret = krb5int_dns_nextans(ds, &base, &rdlen);
    if (ret < 0 || base == NULL)
        goto errout;

    p = base;
    if (!INCR_OK(base, rdlen, p, 1))
        goto errout;
    len = *p++;
    *realm = malloc((size_t)len + 1);
    if (*realm == NULL) {
        retval = ENOMEM;
        goto errout;
    }
    strncpy(*realm, (const char *)p, (size_t)len);
    (*realm)[len] = '\0';
    /* Avoid a common error. */
    if ( (*realm)[len-1] == '.' )
        (*realm)[len-1] = '\0';
    retval = 0;
    TRACE_TXT_LOOKUP_SUCCESS(context, host, *realm);

errout:
    if (ds != NULL) {
        krb5int_dns_fini(ds);
        ds = NULL;
    }
    return retval;
}

#endif /* KRB5_DNS_LOOKUP */
