#--
#Copyright 2007 Nominet UK
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.
#++

#= NAME
#
#axfr - Perform a DNS zone transfer
#
#= SYNOPSIS
#
#axfr [ -fqs ] [ -D directory ] [ @nameserver ] zone
#
#= DESCRIPTION
#
#axfr performs a DNS zone transfer, prints each record to the standard
#output, and stores the zone to a file.  If the zone has already been
#stored in a file, axfr will read the file instead of performing a
#zone transfer.
#
#Zones will be stored in a directory hierarchy.  For example, the
#zone transfer for foo.bar.com will be stored in the file
#  HOME/.dns-zones/com/bar/foo/axfr.  The directory can be changed
#  with the B<-D> option.
#  
#  This programs requires that the Storable module be installed.
#    
#= OPTIONS
#    
#    * -f     Force a zone transfer, even if the zone has already been stored
#    in a file.
#    
#    * -q    Be quiet -- don't print the records from the zone.
#    
#    * -s    Perform a zone transfer if the SOA serial number on the nameserver
#    is different than the serial number in the zone file.
#    
#    * -D directory   Store zone files under I<directory> instead of the default directory (see "FILES")
#    
#    * nameserver    Query nameserver instead of the default nameserver.
#    
#= FILES
#    
#  * ${HOME}/.dns-zones   Default directory for storing zone files.
#      
#= AUTHOR
#      
#      Michael Fuhr <mike@fuhr.org>
#      

require 'getoptLong'
require 'dnsruby'

#------------------------------------------------------------------------------
# Read any command-line options and check syntax.
#------------------------------------------------------------------------------

#getopts("fqsD:");
opts = GetoptLong.new(["-f", GetoptLong::NO_ARGUMENT],
  ["-q", GetoptLong::NO_ARGUMENT],
  ["-D", GetoptLong::REQUIRED_ARGUMENT],
  ["-s", GetoptLong::NO_ARGUMENT])

opt_q = false
opt_f = false 
opt_s = false 
opt_d = nil
opts.each do |opt, arg|
  case opt
  when '-q'
    opt_q=true
  when '-f'
    opt_f = true
  when '-s'
    opt_s = true
  when '-D'
    opt_d = arg
  end
end

if (ARGV.length < 1) || (ARGV.length > 2)
  print "Usage: #{$0} [ -fqs ] [ -D directory ] [ @nameserver ] zone\n" 
else
  #------------------------------------------------------------------------------
  # Get the nameserver (if specified) and set up the zone transfer directory
  # hierarchy.
  #------------------------------------------------------------------------------
  
  nameserver = (ARGV[0] =~ /^@/) ? ARGV.shift : ""
  nameserver = nameserver.sub(/^@/, "")
  res = nil
  if nameserver
    res = Dnsruby::Resolver.new(nameserver)
  else
    res = Dnsruby::Resolver.new
  end
  
  zone = ARGV.shift 
  basedir = opt_d!=nil ? opt_d : (ENV["HOME"]!=nil ? ENV["HOME"] : "") + "/.dns-zones"
  zonedir = zone.split(/\./).reverse.join("/")
  zonefile = basedir + "/" + zonedir + "/axfr"
  
  # Don't worry about the 0777 permissions here - the current umask setting
  # will be applied.
  if !(FileTest.directory?basedir) 
    Dir.mkdir(basedir, 0777) or raise RuntimeError, "can't mkdir #{basedir}: #{$!}\n"
  end
  
  dir = basedir
  zonedir.split("/").each do |subdir|
    dir += "/" + subdir
    if (!FileTest.directory?dir)
      Dir.mkdir(dir, 0777) or raise RuntimeError, "can't mkdir #{dir}: #{$!}\n"
    end
  end
  
  #------------------------------------------------------------------------------
  # Get the zone.
  #------------------------------------------------------------------------------
  
  zonearray = nil
  
  if (FileTest.exist?(zonefile) && !opt_f)
    zoneref = Marshal.load(File.open(zonefile))
    if (zoneref==nil)
      raise RuntimeError, "couldn't retrieve zone from #{zonefile}: #{$!}\n"
    end
    
    #----------------------------------------------------------------------
    # Check the SOA serial number if desired.
    #----------------------------------------------------------------------
    
    if (opt_s)
      serial_file, serial_zone = nil
      
      zoneref.each do |rr|
        if (rr.type == "SOA")
          serial_file = rr.serial
          break
        end
      end
      if serial_file==nil
        raise RuntimeError,  "no SOA in #{zonefile}\n" 
      end
      
      soa = res.query(zone, "SOA")
      if soa==nil
        raise RuntimeError, "couldn't get SOA for #{zone}: " + res.errorstring + "\n" 
      end
      
      soa.answer.each do |rr|
        if (rr.type == "SOA")
          serial_zone = rr.serial
          break
        end
      end
      
      if (serial_zone != serial_file)
        opt_f = true
      end
    end 
  else
    opt_f = true
  end
  
  if (opt_f)
    print "nameserver = #{nameserver}, zone=#{zone}"
    zt = Dnsruby::ZoneTransfer.new
    zt.server=(nameserver) if nameserver!=""
    
    zoneref = zt.transfer(zone)
    if zoneref==nil
      raise RuntimeError,  "couldn't transfer zone\n"
    end
    Marshal.dump(zoneref, File.open(zonefile, File::CREAT|File::RDWR))
  end
  
  #------------------------------------------------------------------------------
  # Print the records in the zone.
  #------------------------------------------------------------------------------
  
  if (!opt_q) 
    zoneref.each do |z|
      print z.to_s + "\n"
    end
  end  
end  
