/*
 *      dnsutl - utilities to make DNS easier to configure
 *      Copyright (C) 1999-2001, 2003, 2006, 2007 Peter Miller
 *
 *      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 3 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, see
 *      <http://www.gnu.org/licenses/>.
 */

#include <ac/ctype.h>
#include <ac/string.h>

#include <check.h>
#include <error.h>
#include <gram.h>
#include <lex.h>
#include <srrf/reader.h>
#include <srrf/origin.h>
#include <symtab.h>

static symtab_ty *in_a_stp;
static symtab_ty *in_cname_stp;
static symtab_ty *rev_ns_stp;
static symtab_ty *in_ptr_stp;
static string_ty *dir;
static int       num_cross_errors;


static void
check_domain_name(string_ty *s)
{
    const char      *cp;
    int             num_empty = !s->str_length;
    int             num_upper = 0;

    if (s->str_length == 1 && s->str_text[0] == '.')
        return;
    if (s->str_length >= 1 && s->str_text[s->str_length - 1] == '.')
    {
        gram_error("domain must not end in dot");
        return;
    }
    cp = s->str_text;
    for (;;)
    {
        while (*cp == '.')
        {
            ++cp;
            ++num_empty;
        }
        if (*cp == 0)
            break;

        for (;;)
        {
            int c = (unsigned char)*cp;
            if (c == 0 || c == '.')
                break;
            if (isupper(c))
                ++num_upper;
            ++cp;
        }
        if (*cp == 0)
            break;
        ++cp;
    }
    if (num_empty)
    {
        gram_error
        (
            "domain \"%s\" contains %d empty component%s",
            s->str_text,
            num_empty,
            (num_empty == 1 ? "" : "s")
        );
    }
    if (num_upper)
    {
        gram_error
        (
      "domain \"%s\" contains %d upper case charcater%s, please use lower case",
            s->str_text,
            num_upper,
            (num_upper == 1 ? "" : "s")
        );
    }
}


static void
check_in_a_records(symtab_ty *stp, string_ty *key, void *data, void *aux)
{
    srrf_t          *rp = data;
    int             a1, a2, a3, a4;
    string_ty       *s;

    (void)stp;
    (void)key;
    (void)aux;
    sscanf(rp->arg.string[0]->str_text, "%d.%d.%d.%d", &a1, &a2, &a3, &a4);
    s = str_format("%d.%d.%d.%d.in-addr.arpa.", a4, a3, a2, a1);
    if (!symtab_query_downcase(in_ptr_stp, s))
    {
        /* Unless it's an external name server. */
        if (!symtab_query_downcase(rev_ns_stp, rp->name))
        {
            error
            (
                "%s: %d: no matching IN A record for \"%s IN PTR %s\"",
                rp->file_name->str_text,
                rp->line_number,
                rp->name->str_text,
                rp->arg.string[0]->str_text
            );
            ++num_cross_errors;
        }
    }
    str_free(s);
}


static void
check_in_ptr_records(symtab_ty *stp, string_ty *key, void *data, void *aux)
{
    srrf_t          *rp = data;
    int             a1, a2, a3, a4;

    (void)stp;
    (void)key;
    (void)aux;
    if (sscanf(rp->name->str_text, "%d.%d.%d.%d.in-addr.arpa.",
        &a1, &a2, &a3, &a4) != 4)
    {
        error("%s: %d: malformed \"%s IN PTR\" name",
            rp->file_name->str_text, rp->line_number,
            rp->name->str_text);
        ++num_cross_errors;
    }
    else if (!symtab_query_downcase(in_a_stp, rp->arg.string[0]))
    {
        error("%s: %d: no matching IN A record for \"%s IN PTR %s\"",
            rp->file_name->str_text, rp->line_number,
            rp->name->str_text, rp->arg.string[0]->str_text);
        ++num_cross_errors;
    }
}


static void
check_rev_ns_records(symtab_ty *stp, string_ty *key, void *data, void *aux)
{
    srrf_t          *rp = data;

    (void)stp;
    (void)key;
    (void)aux;
    if
    (
        !symtab_query_downcase(in_a_stp, key)
    &&
        !symtab_query_downcase(in_cname_stp, key)
    )
    {
        error
        (
            "%s: %d: no address for \"%s\" name server",
            rp->file_name->str_text,
            rp->line_number,
            rp->arg.string[0]->str_text
        );
        ++num_cross_errors;
    }
}


void
check(const char *filename)
{
    in_a_stp = symtab_alloc(5);
    in_cname_stp = symtab_alloc(5);
    in_ptr_stp = symtab_alloc(5);
    rev_ns_stp = symtab_alloc(5);
    grammar(filename);

    /*
     * Make sure the IN A records and IN PTR records all match.
     */
    num_cross_errors = 0;
    symtab_walk(in_a_stp, check_in_a_records, 0);
    symtab_walk(in_ptr_stp, check_in_ptr_records, 0);
    symtab_walk(rev_ns_stp, check_rev_ns_records, 0);
    if (num_cross_errors)
    {
        fatal
        (
            "found %d cross validation error%s",
            num_cross_errors,
            (num_cross_errors == 1 ? "" : "s")
        );
    }
}


void
check_cache(string_ty *domain, string_ty *filename)
{
    static string_ty *dot;
    string_ty        *fn1;
    string_ty        *fn2;
    srrf_list_ty     *data;
    size_t           j;
    srrf_class_ty    *in;
    srrf_type_ty     *in_a;
    srrf_type_ty     *in_ns;
    int              number_of_errors = 0;

    /*
     * Make sure we have seen a directory directive before this.
     */
    if (!dir)
    {
        gram_error("Missing ``directory'' line above this one.");
        return;
    }

    /*
     * make sure the domain is dot
     */
    if (!dot)
        dot = str_from_c(".");
    if (!str_equal(dot, domain))
        gram_error("domain must be ``.''");
    srrf_origin_set(domain);

    /*
     * read the cache file
     */
    fn1 = str_format("%s/%s", dir->str_text, filename->str_text);
    fn2 = srrf_find(fn1);
    data = srrf_reader(fn2->str_text, 0, 0);
    str_free(fn2);
    str_free(fn1);

    /*
     * the only entries should be ``in ns'' or ``in a''
     */
    in = srrf_class_by_name("in");
    in_a = srrf_type_by_name(in, "a");
    in_ns = srrf_type_by_name(in, "ns");
    for (j = 0; j < data->nrecords; ++j)
    {
        srrf_t          *rp;

        rp = data->record[j];
        if (rp->type == in_ns)
        {
            if (!str_equal(rp->name, dot))
            {
                /*
                 * The name server records must all be
                 * for root (".").
                 */
                error
                (
                    "%s: %d: the NS records should only be for ``.''",
                    rp->file_name->str_text,
                    rp->line_number
                );
                ++number_of_errors;
            }
            symtab_assign_downcase(rev_ns_stp, rp->arg.string[0], rp);
        }
        else if (rp->type == in_a)
        {
            symtab_assign_downcase(in_a_stp, rp->name, rp);
        }
        else
        {
            /*
             * Actually, you can cache anything, but once
             * you have the root server addresses, you can
             * find out the rest.
             */
            error
            (
          "%s: %d: cache file must only contain ``in a'' and ``in ns'' records",
                    rp->file_name->str_text,
                    rp->line_number
            );
            ++number_of_errors;
        }
        if (rp->ttl < 30*24*60*60)
        {
            error
            (
                "%s: %d: time-to-live needs to be very long",
                rp->file_name->str_text,
                rp->line_number
            );
            ++number_of_errors;
        }
    }

    /*
     * For each NS record, there must be an A record.
     * For each A record, there must be an NS record.
     */
    for (j = 0; j < data->nrecords; ++j)
    {
        srrf_t          *rp;

        rp = data->record[j];
        if (rp->type == in_a)
        {
            if (!symtab_query_downcase(rev_ns_stp, rp->name))
            {
                error
                (
                    "%s: %d: no name server for %s",
                    rp->file_name->str_text,
                    rp->line_number,
                    rp->name->str_text
                );
                ++number_of_errors;
            }
        }
    }

    /*
     * Mention the problem if anything bad was found,
     * but don't stop yet, there's probably heaps more.
     */
    if (number_of_errors)
    {
        gram_error
        (
            "the cache file \"%s\" contains %d fatal error%s",
            filename->str_text,
            number_of_errors,
            (number_of_errors == 1 ? "" : "s")
        );
    }
}


void
check_directory(string_ty *name)
{
    char            *cp;

    /*
     * Make sure they specified an absolute path name.
     *
     * This a bit of a pest when we want to test it on something
     * other than live data, or when we want to test data *before*
     * making it live.
     *
     * This is where the include search paths come into it.  If *any*
     * include path options (-I) appeared on the command line,
     * we are going to treat them as relative paths.
     */
    if (name->str_text[0] != '/')
        gram_error("directory path must be absolute");
    for (cp = name->str_text; *cp == '/'; ++cp)
        ;
    if (*cp == 0)
        gram_error("directory path must not be root");

    /*
     * Stash it away.
     */
    if (dir)
        str_free(dir);
    if (srrf_include_path_specified())
        dir = str_from_c(cp);
    else
        dir = str_copy(name);
}


void
check_domain(string_ty *name)
{
    (void)name;
}


void
check_forwarder(string_ty *address)
{
    (void)address;
}


static string_ty *
append_dot_if_needed(string_ty *s)
{
    if (s->str_length < 1 || s->str_text[s->str_length - 1] != '.')
        return str_format("%s.", s->str_text);
    return str_copy(s);
}


void
check_primary(string_ty *domain, string_ty *filename)
{
    string_ty       *fn1;
    string_ty       *fn2;
    srrf_list_ty    *data;
    int             number_of_errors = 0;
    size_t          j;
    srrf_class_ty   *in;
    srrf_type_ty    *in_a;
    srrf_type_ty    *in_cname;
    srrf_type_ty    *in_ns;
    srrf_type_ty    *in_ptr;

    /*
     * Make sure we have seen a directory directive before this.
     */
    if (!dir)
    {
        gram_error("Missing ``directory'' line above this one.");
        return;
    }

    /*
     * Set the origin before we read the file.
     */
    check_domain_name(domain);
    fn2 = append_dot_if_needed(domain);
    srrf_origin_set(fn2);
    str_free(fn2);

    /*
     * read the file
     */
    fn1 = str_format("%s/%s", dir->str_text, filename->str_text);
    fn2 = srrf_find(fn1);
    data = srrf_reader(fn2->str_text, 0, 0);
    str_free(fn2);
    str_free(fn1);

    /*
     * Stash the records so we can cross check once everything has
     * been read in.
     */
    in = srrf_class_by_name("in");
    in_a = srrf_type_by_name(in, "a");
    in_cname = srrf_type_by_name(in, "cname");
    in_ns = srrf_type_by_name(in, "ns");
    in_ptr = srrf_type_by_name(in, "ptr");
    for (j = 0; j < data->nrecords; ++j)
    {
        srrf_t          *rp;

        rp = data->record[j];
        if (rp->type == in_ns)
            symtab_assign_downcase(rev_ns_stp, rp->arg.string[0], rp);
        if (rp->type == in_a)
        {
            srrf_t *other = symtab_query_downcase(in_a_stp, rp->name);
            if (other)
            {
                error
                (
                    "%s: %d: duplicate %s IN A record",
                    rp->file_name->str_text,
                    rp->line_number,
                    rp->name->str_text
                );
                error
                (
                    "%s: %d: ...here is the first one",
                    other->file_name->str_text,
                    other->line_number
                );
                ++number_of_errors;
            }
            else
                symtab_assign_downcase(in_a_stp, rp->name, rp);
        }
        if (rp->type == in_cname)
        {
            srrf_t *other = symtab_query_downcase(in_cname_stp, rp->name);
            if (other)
            {
                error
                (
                    "%s: %d: duplicate %s IN CNAME record",
                    rp->file_name->str_text,
                    rp->line_number,
                    rp->name->str_text
                );
                error
                (
                    "%s: %d: ...here is the first one",
                    other->file_name->str_text,
                    other->line_number
                );
                ++number_of_errors;
            }
            else
                symtab_assign_downcase(in_cname_stp, rp->name, rp);
        }
        if (rp->type == in_ptr)
        {
            srrf_t *other = symtab_query_downcase(in_ptr_stp, rp->name);
            if (other)
            {
                error
                (
                    "%s: %d: duplicate %s IN PTR record",
                    rp->file_name->str_text,
                    rp->line_number,
                    rp->name->str_text
                );
                error
                (
                    "%s: %d: ...here is the first one",
                    other->file_name->str_text,
                    other->line_number
                );
                ++number_of_errors;
            }
            else
                symtab_assign_downcase(in_ptr_stp, rp->name, rp);
        }
    }

    /*
     * Mention the problem if anything bad was found,
     * but don't stop yet, there's probably heaps more.
     */
    if (number_of_errors)
    {
        gram_error
        (
            "the cache file \"%s\" contains %d fatal error%s",
            filename->str_text,
            number_of_errors,
            (number_of_errors == 1 ? "" : "s")
        );
    }
}


void
check_secondary(string_ty *domain, string_ty *filename)
{
    (void)filename;

    /*
     * Make sure we have seen a directory directive before this.
     */
    if (!dir)
    {
        gram_error("Missing ``directory'' line above this one.");
        return;
    }

    /*
     * Set the origin before we read the file.
     */
    check_domain_name(domain);
}


void
check_option(string_ty *name)
{
    static const char *table[] =
    {
        "query-log",
        "no-recursion",
        "forward-only",
        "fake-iquery",
        "no-fetch-glue",
    };
    const char **cpp;

    for (cpp = table; cpp < ENDOF(table); ++cpp)
        if (0 == strcmp(*cpp, name->str_text))
            return;
    gram_error("unknown ``%s'' option", name->str_text);
}


syntax highlighted by Code2HTML, v. 0.9.1