# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0.  If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.

import argparse
import os
import sys
from subprocess import Popen, PIPE

from isc.utils import prefix, version

prog = "dnssec-checkds"


############################################################################
# SECRR class:
# Class for DS resource record
############################################################################
class SECRR:
    hashalgs = {1: "SHA-1", 2: "SHA-256", 3: "GOST", 4: "SHA-384"}
    rrname = ""
    rrclass = "IN"
    keyid = None
    keyalg = None
    hashalg = None
    digest = ""
    ttl = 0

    def __init__(self, rrtext):
        if not rrtext:
            raise Exception

        # 'str' does not have decode method in python3
        if type(rrtext) is not str:
            fields = rrtext.decode("ascii").split()
        else:
            fields = rrtext.split()
        if len(fields) < 7:
            raise Exception

        self.rrtype = "DS"
        self.rrname = fields[0].lower()

        fields = fields[1:]
        if fields[0].upper() in ["IN", "CH", "HS"]:
            self.rrclass = fields[0].upper()
            fields = fields[1:]
        else:
            self.ttl = int(fields[0])
            self.rrclass = fields[1].upper()
            fields = fields[2:]

        if fields[0].upper() != self.rrtype:
            raise Exception("%s does not match %s" % (fields[0].upper(), self.rrtype))

        self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4])
        self.digest = "".join(fields[4:]).upper()

    def __repr__(self):
        return "%s %s %s %d %d %d %s" % (
            self.rrname,
            self.rrclass,
            self.rrtype,
            self.keyid,
            self.keyalg,
            self.hashalg,
            self.digest,
        )

    def __eq__(self, other):
        return self.__repr__() == other.__repr__()


############################################################################
# check:
# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DS records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def check(zone, args):
    rrlist = []
    if args.dssetfile:
        fp = open(args.dssetfile).read()
    else:
        cmd = [args.dig, "+noall", "+answer", "-t", "ds", "-q", zone]
        fp, _ = Popen(cmd, stdout=PIPE).communicate()

    for line in fp.splitlines():
        if type(line) is not str:
            line = line.decode("ascii")
        rrlist.append(SECRR(line))
    rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))

    klist = []

    cmd = [args.dsfromkey]
    for algo in args.algo:
        cmd += ["-a", algo]

    if args.masterfile:
        cmd += ["-f", args.masterfile, zone]
        fp, _ = Popen(cmd, stdout=PIPE).communicate()
    else:
        intods, _ = Popen(
            [args.dig, "+noall", "+answer", "-t", "dnskey", "-q", zone], stdout=PIPE
        ).communicate()
        cmd += ["-f", "-", zone]
        fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)

    for line in fp.splitlines():
        if type(line) is not str:
            line = line.decode("ascii")
        klist.append(SECRR(line))

    if len(klist) < 1:
        print("No DNSKEY records found in zone apex")
        return False

    match = True
    for rr in rrlist:
        if rr not in klist:
            print(
                "KSK for %s %s/%03d/%05d (%s) missing from child"
                % (
                    rr.rrtype,
                    rr.rrname.strip("."),
                    rr.keyalg,
                    rr.keyid,
                    SECRR.hashalgs[rr.hashalg],
                )
            )
            match = False
    for rr in klist:
        if rr not in rrlist:
            print(
                "%s for KSK %s/%03d/%05d (%s) missing from parent"
                % (
                    rr.rrtype,
                    rr.rrname.strip("."),
                    rr.keyalg,
                    rr.keyid,
                    SECRR.hashalgs[rr.hashalg],
                )
            )
            match = False
    for rr in klist:
        if rr in rrlist:
            print(
                "%s for KSK %s/%03d/%05d (%s) found in parent"
                % (
                    rr.rrtype,
                    rr.rrname.strip("."),
                    rr.keyalg,
                    rr.keyid,
                    SECRR.hashalgs[rr.hashalg],
                )
            )

    return match


############################################################################
# parse_args:
# Read command line arguments, set global 'args' structure
############################################################################
def parse_args():
    parser = argparse.ArgumentParser(description=prog + ": checks DS coverage")

    bindir = "bin"
    sbindir = "bin" if os.name == "nt" else "sbin"

    parser.add_argument("zone", type=str, help="zone to check")
    parser.add_argument(
        "-a",
        "--algo",
        dest="algo",
        action="append",
        default=[],
        type=str,
        help="DS digest algorithm",
    )
    parser.add_argument(
        "-d",
        "--dig",
        dest="dig",
        default=os.path.join(prefix(bindir), "dig"),
        type=str,
        help="path to 'dig'",
    )
    parser.add_argument(
        "-D",
        "--dsfromkey",
        dest="dsfromkey",
        default=os.path.join(prefix(sbindir), "dnssec-dsfromkey"),
        type=str,
        help="path to 'dnssec-dsfromkey'",
    )
    parser.add_argument(
        "-f", "--file", dest="masterfile", type=str, help="zone master file"
    )
    parser.add_argument(
        "-s", "--dsset", dest="dssetfile", type=str, help="prepared DSset file"
    )
    parser.add_argument("-v", "--version", action="version", version=version)
    args = parser.parse_args()

    args.zone = args.zone.strip(".")

    return args


############################################################################
# Main
############################################################################
def main():
    args = parse_args()
    match = check(args.zone, args)
    exit(0 if match else 1)
