#!/usr/bin/perl
#
# $Id: dns2hosts,v 2.0 90/09/11 11:07:27 hakanson Rel $
#
# Convert DNS master (RFC-1035) files to /etc/hosts format.
#   Marion Hakanson (hakanson@cse.ogi.edu)
#   Oregon Graduate Institute of Science and Technology
#
# Copyright (c) 1990, Marion Hakanson.
#
# You may distribute under the terms of the GNU General Public License
# as specified in the README file that comes with the dnsparse kit.
#
# Other than "-d defdom" (to specify a default domain different from that
# found in /etc/resolv.conf), command line args are DNS master file names,
# as described for the dns_init() subroutine in dnsparse.pl.  The output
# is printed to the standard output stream.
#
# This Perl program looks at the A and CNAME records encountered in the
# DNS master files, and produces an /etc/hosts file which includes the
# same host-to-address, address-to-host, and alias-to-host mappings.
# Note that some such mappings are necessarily dependent on the order in
# which they are encountered in the DNS master files.  The DNS system
# is not sensitive to the order, but /etc/hosts files are, thus one
# should take care that the command line arguments are presented in
# the same order from one invocation to the next, if one expects the
# two resulting output files to be comparable.
#
# A large part of the complication of this program is due to the notion
# of the "default domain" that the BIND implementation of a DNS resolver
# provides.  Namely, this allows a user to type a short alias and have
# it match to a fully qualified domain name (FQDN) somewhere in the
# machine's default domain.  E.g. given "admin" and a default domain
# of "cse.ogi.edu", the resolver will try "admin.cse.ogi.edu" -- if no
# match is found, it will try "admin.ogi.edu" (but it will not try
# just the first-level domain, "admin.edu").
#
# Thus, the aliases and canonical names are set up in the output file
# to mimic the behavior of a system using the BIND resolver.  Among
# other things, this means that host numbers will always be resolved
# to the FQDN of the host.  Instead of using PTR records to provide
# the number-to-name mappings, this program assumes that the PTR records
# would be automatically generated from the A records.  It further assumes
# that a host is considered to have a single, unique FQDN, and that all
# of its addresses (interfaces) will resolve back to that FQDN.  To my
# knowledge, this is the only area where the output hosts file may not
# quite mimic the behavior of the BIND resolver.  I would be willing to
# consider a design change if demand warrants it.

do 'getopts.pl'; die "$@, aborted" if $@;
do 'dnsparse.pl'; die "$@, aborted" if $@;

do Getopts('d:');

# Get default domain
if ( defined($opt_d) ) {
  $defdom = $opt_d;
} elsif ( open(F, '</etc/resolv.conf') ) {
  while ( <F> ) {
    $defdom = $1 if ( /\s*domain\s+([^\s]+)/ );
  }
  close(F);
}

# Set up domains the resolver would try
if ( defined($defdom) ) {
  $defdom =~ s/^\.//;	# strip leading dot
  $defdom = do dns_makefqdn($defdom,'');

  @domparts = split(/\./,$defdom);	# escaped dots?
  while ( $#domparts > 0 ) {
    push(@defdoms, join('.',@domparts));
    shift(@domparts);
  }
}

# Parse the dns db file, collecting names & addrs.
do dns_init(@ARGV);

open(OFILE, ">&STDOUT") || die "Cannot dupe 'STDOUT', aborted";

# Treat loopback address specially, since the DNS is usually set
# up to resolve loopback to "localhost".
$hostsbyaddr{'127.0.0.1'} = 'localhost';
push(@addrs,'127.0.0.1');

rr: while ( (@rr = do dns_getrr()) && @rr ) {
  ($domain, $ttl, $class, $type, @data) = @rr;

  next rr if ( $class ne 'IN' );

  case: {
    if ( $type eq 'A' ) {
      $list = '';
      if ( defined($hostsbyaddr{$data[0]}) ) {
        $list = $hostsbyaddr{$data[0]} . $dns'delim;
      } else {
        push(@addrs,$data[0]);
      }
      $list .= $domain;
      $hostsbyaddr{$data[0]} = $list;
      $fqdnseen{$domain}++;
      last case;
    } elsif ( $type eq 'CNAME' ) {
      $list = '';
      if ( defined($cnamesbyhost{$data[0]}) ) {
        $list = $cnamesbyhost{$data[0]} . $dns'delim;
      }
      $list .= $domain;
      $cnamesbyhost{$data[0]} = $list;
      $fqdnseen{$domain}++;
      last case;
    }
  }
}

# Go through the names encountered, building abbreviations,
# and attaching them to their fully-qualified forms.
while ( ($fqdn,$cnt) = each %fqdnseen ) {
  #print STDERR "$fqdn seen $cnt times\n";
  foreach $truncdom (@defdoms) {
    $abbrev = $fqdn;
    # don't generate abbrev's which wouldn't resolve
    if ( $abbrev =~ s/\.$truncdom// ) {
      # now we mimic what the resolver would do
      foreach $dom (@defdoms) {
        if ( defined($fqdnseen{"$abbrev.$dom"}) ) {
          $abbrevsbyhost{"$abbrev.$dom"} .= $abbrev . $dns'delim;
          #print STDERR "$abbrev -> $abbrev.$dom\n";
          # stop with the first one
          last;
        }
      }
    }
  }
}

# for debugging
#while ( ($key,$val) = each(%abbrevsbyhost) ) {
#  print STDERR "|$key";
#  @vals = split(/$dns'delim/,$val);
#  foreach $val (@vals) {
#    print STDERR "|$val";
#  }
#  print STDERR "|\n";
#}

# Write out the hosts file.
# First write out some commentary about the input source, etc.
$datefmt = '%02d/%02d/%02d %02d:%02d:%02d';
($sec,$min,$hour,$mday,$mon,$year,@rest) = localtime();
$mon++;	# 0 == January
$date = sprintf($datefmt, $year, $mon, $mday, $hour, $min, $sec);

chop($host = `hostname`);
print OFILE "#\n";
print OFILE "# Host table (/etc/hosts)\n";
print OFILE "#\n";
print OFILE "# Created on host '$host' at $date\n";
print OFILE "#\n";
print OFILE "# Abbreviations set up for default domain '$defdom'\n";
print OFILE "#\n";
print OFILE "#   THIS TABLE WAS GENERATED AUTOMATICALLY.\n";
print OFILE "#   ANY CHANGES OR ADDITIONS TO IT WILL BE\n";
print OFILE "#   LOST WHEN THE NEXT UPDATE TAKES PLACE.\n";
print OFILE "#\n";
print OFILE "# Built from the following DNS master files:\n";
print OFILE "#\n";
print OFILE "# ModTime\t\tFile\n";
print OFILE "#\tOrigin\n";

while ( $#ARGV >= $[ ) {
    ($file,$origin) = do dns_commasplit(shift(@ARGV));
    $origin = '.' unless ( $origin );	# root
    if ( $file eq '' || $file eq '-' ) {
        $file eq 'Standard Input';
        @ARGV = ();	# STDIN is last
        $date = 'See Above';
    } else {
        if ( -r $file ) {
            ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
                $atime,$mtime,$ctime,$blksize,$blocks) = stat(_);
            ($sec,$min,$hour,$mday,$mon,$year,@rest) = localtime($mtime);
            $mon++;	# 0 == January
            $date = sprintf($datefmt, $year, $mon, $mday, $hour, $min, $sec);
        } else {
            $date = "Not Found";
            $origin = $!;	# the error message
        }
    }
    print OFILE "# $date\t$file\n";
    print OFILE "#\t$origin\n";
}
print OFILE "#\n";
print OFILE "#\n";

# Build up an output record and then write it out using a
# format which may break a long record into multiple lines,
# all starting with the same address and canonical name.
format OFILE =
@<<<<<<<<<<<<<<  ~^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$addr,              $names
.

$: = ' ';	# OK to break $names on a blank

foreach $addr ( @addrs ) {
  @hosts = split(/$dns'delim/, $hostsbyaddr{$addr});
  %nameseen = ();
  $names = '';
  $canon = $hosts[0];		# save the first one

  foreach $host ( @hosts ) {
    unless ( defined($nameseen{$host}) ) {
      $nameseen{$host} = 1;
      $names .= " $host";
      if ( defined($abbrevsbyhost{$host}) ) {
        @abbrevs = split(/$dns'delim/, $abbrevsbyhost{$host});
        foreach $abbrev ( @abbrevs ) {
          unless ( defined($nameseen{$abbrev}) ) {
            $nameseen{$abbrev} = 1;
            $names .= " $abbrev";
          }
        }
      }
    }

    if ( defined($cnamesbyhost{$host}) ) {
      @aliases = split(/$dns'delim/, $cnamesbyhost{$host});
      foreach $alias ( @aliases ) {
        unless ( defined($nameseen{$alias}) ) {
          $nameseen{$alias} = 1;
          $names .= " $alias";
          if ( defined($abbrevsbyhost{$alias}) ) {
            @abbrevs = split(/$dns'delim/, $abbrevsbyhost{$alias});
            foreach $abbrev ( @abbrevs ) {
              unless ( defined($nameseen{$abbrev}) ) {
                $nameseen{$abbrev} = 1;
                $names .= " $abbrev";
              }
            }
          }
        }
      }
    }
  }
  $names =~ s/^ $canon *//;	# we'll put it back below
  do {
    #print STDERR "|$names|\n";
    $names = "$canon $names";
    write(OFILE);	# has side effect of shortening $names
  } while ( $names );
}

exit(0);
