#!/usr/bin/env python

"""

A Python program to analyze the dependencies of a DNS domain. It
retrieves the nameservers of the domain, then the parent domains of
the nameservers, then the nameservers of these parents and so on.

In the end, it creates a Graphviz '.dot' file
(http://www.graphviz.org/) with all the dependencies.

It is recommended to use dotty (in the Graphviz) package to browse the
file: for most domains, the graph is too complicated for printing.

Author:

Stephane Bortzmeyer <bortzmeyer@nic.fr>

Licence:

What you want

Acknowledgements:

Slawomir Gruca <slawomir.gruca@nask.pl> for the idea.

References:

* A good paper on this issue of dependencies:
  http://www.cs.cornell.edu/People/egs/beehive/dnssurvey.html

* Slawomir Gruca's script:
  http://www.dns.pl/cgi-bin/dnsexplorer.pl

"""

__version__ = "$Id: ns2dot.py,v 1.2 2005/07/19 12:10:59 bortzmeyer Exp $"

# Default values
debug_level = 0
verbose = False
exclude_root = False
go_to_parent = True
printing = False

# Settings:
nameserver_to_parent_domain_color = "cyan"
domain_to_parent_domain_color = "gold2"
domain_to_nameserver_color = "red"

import sys
import string
import time
import getopt

# DNS Python http://www.dnspython.org/
import dns.resolver

domains_nameservers = {}
domains_domains = {}
nameservers_domains= {}
recursion_depth = 0

def usage():
    sys.stderr.write("Usage: %s [options] domain\n" % sys.argv[0])
    sys.stderr.write("""
    Possible options:
      --debug
      --printing: put in the '.dot' file options which are suitable
                  for printing.
      --exclude-root: omit the DNS root (by default, the root is
                      always analyzed since every domain ultimately
                      depend on it).
      --no-go-to-parent: do not climb up from a domain to its parent
      
      """)
    
def error(message):
    sys.stderr.write("ERROR: %s\n" % message)
    sys.exit(1)

def debug(message, max_level=1):
    if debug_level >= max_level:
        indent = " " * (2*recursion_depth)
        sys.stdout.write("%sDEBUG: %s\n" % (indent, message))

def canonicalize(old_name):
    name = string.lower(old_name)
    if name[len(name)-1] != '.':
        name = name + '.'
    debug("Canonical form of %s is %s" % (old_name, name), max_level=3)
    return name

def ns_of_nameserver(server):
    global recursion_depth
    server = canonicalize(server)
    if nameservers_domains.has_key(server):
        return
    recursion_depth = recursion_depth + 1
    result = []
    (label, parent) = string.split(server, ".", 1)
    if parent == "":
        if not exclude_root:
            parent = "." # The root
        else:
            recursion_depth = recursion_depth - 1
            return
    if label != "": # We are not yet at the top (the root)
        nameservers_domains[server] = parent
        ns_of_domain(parent)
    recursion_depth = recursion_depth - 1
    return

def ns_of_domain(domain):
    global recursion_depth
    domain = canonicalize(domain)
    if domains_nameservers.has_key(domain):
        return
    else:
        domains_nameservers[domain] = []
    recursion_depth = recursion_depth + 1
    result = []
    try:
        answers = dns.resolver.query(domain, 'NS')
        debug("%i responses for %s" % (len(answers), domain), 2)
        for rdata in answers:
            server = canonicalize(str(rdata))
            ns_of_nameserver(server)
            domains_nameservers[domain].append(server)
    except dns.resolver.NoAnswer:
        debug("No answer for %s" % domain)    
        answers = None # Try the parent, anyway, a domain can exist without NS records
    except dns.resolver.NXDOMAIN:
        debug("No such domain %s" % domain)
        answers = None # Try the parent, anyway, a domain can exist "virtually"
    (label, parent) = string.split(domain, ".", 1)
    if parent == "":
        if not exclude_root:
            parent = "." # The root
        else:
            domains_domains[domain] = None
            recursion_depth = recursion_depth - 1
            return 
    if label != "": # We are not yet at the top (the root)
        domains_domains[domain] = parent
        if go_to_parent:
            ns_of_domain(parent)
    recursion_depth = recursion_depth - 1
    return 

# Main program starts here
try:
    optlist, args = getopt.getopt (sys.argv[1:], "hvd:epr",
                                   ["help", "verbose", "version", "exclude-root",
                                    "no-go-to-parent", "printing"])
    for option, value in optlist:
        if option == "--help" or option == "-h":
            usage()
            sys.exit(0)
        elif option == "--verbose" or option == "-v": debug_level = 1
        elif option == "--debug" or option == "-d": debug_level = int(value)
        elif option == "--printing" or option == "-r":
            printing = True
        elif option == "--exclude-root" or option == "-e":
            exclude_root = True
        elif option == "--no-go-to-parent" or option == "-p":
            go_to_parent = False
        elif option == "--version":
            print "%s %s" % (sys.argv[0], __version__)
            sys.exit(0)
        else: error ("Unknown option " + option)
except getopt.error, reason:
    error ("Usage: " + sys.argv[0] + ": " + reason)
if debug_level > 0:
    verbose = True
if len(args) != 1:
    usage()
    error("You must indicate a domain to analyze")
domain = canonicalize(args[0])
output = open("%sdot" % domain, "w")
ns_of_domain(domain)
if printing:
    presentation = """
         // Size in inches
         page="8.5,11.6";
         ratio = fill;
    """
else:
    presentation = """
         // Size in inches
         size="8.5,11.6";
         ratio = auto;
    """
output.write("""digraph DNS {
         label = "Nameserver dependencies of \\"%s\\" on %s. %i nameservers. %i domains.";
         fontsize = 24;
         overlap = scale;
         %s
         // Domains
         node [shape=ellipse, fontsize=14, width=1.2];
""" % (domain,
       time.strftime("%A %d %B %Y %H:%M UT", time.gmtime(time.time())),
       len(nameservers_domains),
       len(domains_domains),
       presentation))
for ordinary_domain in domains_domains.keys():
    if ordinary_domain != domain:
        output.write ("\"%s\";\n" % ordinary_domain)
output.write("""// The analyzed domain
         node [shape=circle, fontsize=22];
         "%s";\n""" % domain)
output.write("""// Nameservers
    node [shape=octagon, fontsize=10];
    """)
for domain in domains_nameservers.keys():
    for ns in domains_nameservers[domain]:
        output.write("\"%s\";\n" % ns)
output.write("\n")
for domain in domains_domains.keys():
    if len(string.split(domain, ".")) == 2:
        output.write("{rank=min; \"%s\"; }\n" % domain)
    elif len(string.split(domain, ".")) > 3:
        output.write("{rank=max; \"%s\"; }\n" % domain)
output.write ("\n")
for domain in domains_domains.keys():
    if not exclude_root or domains_domains[domain] is not None:
        # Relationship between a domain and its parent
        if go_to_parent or domains_domains.has_key(domains_domains[domain]):
            output.write("\"%s\" -> \"%s\" [color=%s, weigth=20];\n" %
                         (domain, domains_domains[domain],
                          domain_to_parent_domain_color))
for domain in domains_nameservers.keys():
    for ns in domains_nameservers[domain]:
        # Relationship between a domain and its nameservers
        output.write("\"%s\" -> \"%s\" [color=%s, weigth=10];\n" %
                     (domain, ns, domain_to_nameserver_color))
for ns in nameservers_domains.keys():
    # Relationship between a nameserver and its parent domain
    output.write("\"%s\" -> \"%s\" [color=%s, weigth=10];\n" %
                 (ns, nameservers_domains[ns], nameserver_to_parent_domain_color))
output.write("}\n")
output.close()

	      

