# 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.

############################################################################
# ans.py: See README.anspy for details.
############################################################################

from __future__ import print_function
import os
import sys
import signal
import socket
import select
from datetime import datetime, timedelta
import functools

import dns, dns.message, dns.query
from dns.rdatatype import *
from dns.rdataclass import *
from dns.rcode import *
from dns.name import *

############################################################################
# set up the RRs to be returned in the next answer
#
# the message contains up to two pipe-separated ('|') fields.
#
# the first field of the message is a comma-separated list
# of actions indicating what to put into the answer set
# (e.g., a dname, a cname, another cname, etc)
#
# supported actions:
# - cname (cname from the current name to a new one in the same domain)
# - dname (dname to a new domain, plus a synthesized cname)
# - xname ("external" cname, to a new name in a new domain)
#
# example: xname, dname, cname represents a CNAME to an external
# domain which is then answered by a DNAME and synthesized
# CNAME pointing to yet another domain, which is then answered
# by a CNAME within the same domain, and finally an answer
# to the query. each RR in the answer set has a corresponding
# RRSIG. these signatures are not valid, but will exercise the
# response parser.
#
# the second field is a comma-separated list of which RRs in the
# answer set to include in the answer, in which order. if prepended
# with 's', the number indicates which signature to include.
#
# examples: for the answer set "cname, cname, cname", an rr set
# '1, s1, 2, s2, 3, s3, 4, s4' indicates that all four RRs should
# be included in the answer, with siagntures, in the original
# order, while 4, s4, 3, s3, 2, s2, 1, s1' indicates the order
# should be reversed, 's3, s3, s3, s3' indicates that the third
# RRSIG should be repeated four times and everything else should
# be omitted, and so on.
#
# if there is no second field (i.e., no pipe symbol appears in
# the line) , the default is to send all answers and signatures.
# if a pipe symbol exists but the second field is empty, then
# nothing is sent at all.
############################################################################
actions = []
rrs = []


def ctl_channel(msg):
    global actions, rrs

    msg = msg.splitlines().pop(0)
    print("received control message: %s" % msg)

    msg = msg.split(b"|")
    if len(msg) == 0:
        return

    actions = [x.strip() for x in msg[0].split(b",")]
    n = functools.reduce(
        lambda n, act: (n + (2 if act == b"dname" else 1)), [0] + actions
    )

    if len(msg) == 1:
        rrs = []
        for i in range(n):
            for b in [False, True]:
                rrs.append((i, b))
        return

    rlist = [x.strip() for x in msg[1].split(b",")]
    rrs = []
    for item in rlist:
        if item[0] == b"s"[0]:
            i = int(item[1:].strip()) - 1
            if i > n:
                print("invalid index %d" + (i + 1))
                continue
            rrs.append((int(item[1:]) - 1, True))
        else:
            i = int(item) - 1
            if i > n:
                print("invalid index %d" % (i + 1))
                continue
            rrs.append((i, False))


############################################################################
# Respond to a DNS query.
############################################################################
def create_response(msg):
    m = dns.message.from_wire(msg)
    qname = m.question[0].name.to_text()
    labels = qname.lower().split(".")
    wantsigs = True if m.ednsflags & dns.flags.DO else False

    # get qtype
    rrtype = m.question[0].rdtype
    typename = dns.rdatatype.to_text(rrtype)

    # for 'www.example.com.'...
    # - name is 'www'
    # - domain is 'example.com.'
    # - sld is 'example'
    # - tld is 'com.'
    name = labels.pop(0)
    domain = ".".join(labels)
    sld = labels.pop(0)
    tld = ".".join(labels)

    print("query: " + qname + "/" + typename)
    print("domain: " + domain)

    # default answers, depending on QTYPE.
    # currently only A, AAAA, TXT and NS are supported.
    ttl = 86400
    additionalA = "10.53.0.4"
    additionalAAAA = "fd92:7065:b8e:ffff::4"
    if typename == "A":
        final = "10.53.0.4"
    elif typename == "AAAA":
        final = "fd92:7065:b8e:ffff::4"
    elif typename == "TXT":
        final = "Some\ text\ here"
    elif typename == "NS":
        domain = qname
        final = "ns1.%s" % domain
    else:
        final = None

    # RRSIG rdata - won't validate but will exercise response parsing
    t = datetime.now()
    delta = timedelta(30)
    t1 = t - delta
    t2 = t + delta
    inception = t1.strftime("%Y%m%d000000")
    expiry = t2.strftime("%Y%m%d000000")
    sigdata = "OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso="

    # construct answer set.
    answers = []
    sigs = []
    curdom = domain
    curname = name
    i = 0

    for action in actions:
        if name != "test":
            continue
        if action == b"xname":
            owner = curname + "." + curdom
            newname = "cname%d" % i
            i += 1
            newdom = "domain%d.%s" % (i, tld)
            i += 1
            target = newname + "." + newdom
            print("add external CNAME %s to %s" % (owner, target))
            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
                ttl,
                expiry,
                inception,
                domain,
                sigdata,
            )
            print("add external RRISG(CNAME) %s to %s" % (owner, target))
            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
            curname = newname
            curdom = newdom
            continue

        if action == b"cname":
            owner = curname + "." + curdom
            newname = "cname%d" % i
            target = newname + "." + curdom
            i += 1
            print("add CNAME %s to %s" % (owner, target))
            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
                ttl,
                expiry,
                inception,
                domain,
                sigdata,
            )
            print("add RRSIG(CNAME) %s to %s" % (owner, target))
            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
            curname = newname
            continue

        if action == b"dname":
            owner = curdom
            newdom = "domain%d.%s" % (i, tld)
            i += 1
            print("add DNAME %s to %s" % (owner, newdom))
            answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom))
            rrsig = "DNAME 5 3 %d %s %s 12345 %s %s" % (
                ttl,
                expiry,
                inception,
                domain,
                sigdata,
            )
            print("add RRSIG(DNAME) %s to %s" % (owner, newdom))
            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
            owner = curname + "." + curdom
            target = curname + "." + newdom
            print("add synthesized CNAME %s to %s" % (owner, target))
            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
                ttl,
                expiry,
                inception,
                domain,
                sigdata,
            )
            print("add synthesized RRSIG(CNAME) %s to %s" % (owner, target))
            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
            curdom = newdom
            continue

    # now add the final answer
    owner = curname + "." + curdom
    answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final))
    rrsig = "%s 5 3 %d %s %s 12345 %s %s" % (
        typename,
        ttl,
        expiry,
        inception,
        domain,
        sigdata,
    )
    sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))

    # prepare the response and convert to wire format
    r = dns.message.make_response(m)

    if name != "test":
        r.answer.append(answers[-1])
        if wantsigs:
            r.answer.append(sigs[-1])
    else:
        for i, sig in rrs:
            if sig and not wantsigs:
                continue
            elif sig:
                r.answer.append(sigs[i])
            else:
                r.answer.append(answers[i])

    if typename != "NS":
        r.authority.append(
            dns.rrset.from_text(domain, ttl, IN, "NS", ("ns1.%s" % domain))
        )
    r.additional.append(
        dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, A, additionalA)
    )
    r.additional.append(
        dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, AAAA, additionalAAAA)
    )

    r.flags |= dns.flags.AA
    r.use_edns()
    return r.to_wire()


def sigterm(signum, frame):
    print("Shutting down now...")
    os.remove("ans.pid")
    running = False
    sys.exit(0)


############################################################################
# Main
#
# Set up responder and control channel, open the pid file, and start
# the main loop, listening for queries on the query channel or commands
# on the control channel and acting on them.
############################################################################
ip4 = "10.53.0.4"
ip6 = "fd92:7065:b8e:ffff::4"

try:
    port = int(os.environ["PORT"])
except:
    port = 5300

try:
    ctrlport = int(os.environ["EXTRAPORT1"])
except:
    ctrlport = 5300

query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
query4_socket.bind((ip4, port))

havev6 = True
try:
    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    try:
        query6_socket.bind((ip6, port))
    except:
        query6_socket.close()
        havev6 = False
except:
    havev6 = False

ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ctrl_socket.bind((ip4, ctrlport))
ctrl_socket.listen(5)

signal.signal(signal.SIGTERM, sigterm)

f = open("ans.pid", "w")
pid = os.getpid()
print(pid, file=f)
f.close()

running = True

print("Listening on %s port %d" % (ip4, port))
if havev6:
    print("Listening on %s port %d" % (ip6, port))
print("Control channel on %s port %d" % (ip4, ctrlport))
print("Ctrl-c to quit")

if havev6:
    input = [query4_socket, query6_socket, ctrl_socket]
else:
    input = [query4_socket, ctrl_socket]

while running:
    try:
        inputready, outputready, exceptready = select.select(input, [], [])
    except select.error as e:
        break
    except socket.error as e:
        break
    except KeyboardInterrupt:
        break

    for s in inputready:
        if s == ctrl_socket:
            # Handle control channel input
            conn, addr = s.accept()
            print("Control channel connected")
            while True:
                msg = conn.recv(65535)
                if not msg:
                    break
                ctl_channel(msg)
            conn.close()
        if s == query4_socket or s == query6_socket:
            print("Query received on %s" % (ip4 if s == query4_socket else ip6))
            # Handle incoming queries
            msg = s.recvfrom(65535)
            rsp = create_response(msg[0])
            if rsp:
                s.sendto(rsp, msg[1])
    if not running:
        break
