#!/usr/bin/env python3

import argparse,os,re,socket,sys

class IPRange(object):

  re_iprange=re.compile(r'(?P<a>\d+)(?:\.(?P<b>\d+)(?:\.(?P<c>\d+)(?:\.(?P<d>\d+))?)?)?(?:/(?P<mask>\d+))?$')

  class Error(Exception):
    def __init__(self,msg):
      self.msg=msg
    def __str__(self):
      return self.msg

  def __init__(self,range_spec,broadcast=True):
    '''Range range_spec may take one of two forms:

    Partial dotted decimal: For example "1.2.3", is translated into
    "1.2.3.0/24" and represents the range from 1.2.3.1 to 1.2.3.254.
    A range_spec value of "1.2.3.0" would have the same result.

    CIDR notation: For example "4.5.0.0/16".'''

    m=self.re_iprange.match(range_spec)
    if not m:
      raise self.Error('Invalid IP address or subnet format: %r'%range_spec)
    # Convert these values from strings to integers, and store them in the
    # local value a, b, c, d, and mask (names taken from re_iprange). This is a
    # really kludgy way of doing this, but Python 3 seems less willing to
    # update locals than 2 was.
    addr=type('',(),{k:eval(str(v)) for k,v in m.groupdict().items()})
    a=addr.a
    b=addr.b
    c=addr.c
    d=addr.d
    mask=addr.mask
    if b==None:
      b=c=d=0
      if mask==None:
        mask=8
    elif c==None:
      c=d=0
      if mask==None:
        mask=16
    elif d==None:
      d=0
      if mask==None:
        mask=24
    if mask==None:
      if d==0:
        if c==0:
          if b==0:
            mask=8
          else:
            mask=16
        else:
          mask=24
      else:
        mask=32
    # Validate these values.
    n=a<<24|b<<16|c<<8|d
    m=(0xffffffff00000000>>mask)&0xffffffff
    if n&m!=n:
      #raise self.Error('Network %d.%d.%d.%d (0x%08x) exceeds %d-bit mask (0x%08x): %r'%(a,b,c,d,n,mask,m))
      raise self.Error(f"Network {a}.{b}.{c}.{d} (0x{n:08x}) exceeds {mask}-bit mask (0x{m:08x})")

    # Set up for iteration (just in case that's what we're being created for).
    self.allow_broadcast=broadcast
    self.network=n
    self.netmask=m
    self.masksize=mask
    self.broadcast=(m^0xffffffff)&0xffffffff

  def __iter__(self):
    self.addr=-1
    return self

  def __next__(self):
    self.addr+=1
    if not self.allow_broadcast and self.addr==0:
      self.addr+=1
    #print ''
    #print 'mask=%08x'%self.netmask
    #print 'addr=%08x'%self.addr
    if self.addr&self.netmask!=0 or (not self.allow_broadcast and self.addr==self.broadcast):
      raise StopIteration
    x=self.network|self.addr
    return '%d.%d.%d.%d'%((x>>24)&0xff,(x>>16)&0xff,(x>>8)&0xff,x&0xff)

# Parse the command line.
prog=os.path.basename(sys.argv[0])
ap=argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
The %(prog)s command accepts SUBNET arguments on the command line and
writes the corresponding host names to standard output.

SUBNET may take one of two forms:

    o Partial dotted decimal: For example "1.2.3", is translated into
      "1.2.3.0/24" and represents the range from 1.2.3.1 to 1.2.3.254. A
      range_spec value of "1.2.3.0" would have the same result.

    o CIDR notation: For example "4.5.0.0/16 or 6.7.8.224/27".
""")
ap.add_argument('-a','--aliases',dest='aliases',action='store_true',help='''Output DNS aliases as well as canonnical host names.''')
ap.add_argument('-b','--broadcast',dest='broadcast',action='store_true',help='''Allow "broadcast addresses" to be used within the given IP ranges. Without this option, the host portion of the address valued 1 and netmask-1 will be ignored.''')
ap.add_argument('--test',dest='test',action='store_true',help='''Run this program's internal tests, report the results, and quit.''')
ap.add_argument('-v',dest='verbosity',action='count',default=0,help='''Verbosity forces lines to be output even for addresses that are not found in DNS. Use -vv for extra verbosity to see what problem DNS has with each problematic address.''')
ap.add_argument('args',metavar='SUBNET',action='store',nargs='+',help="""List as many subnets as you're interested in.""")
opt=ap.parse_args()

if opt.test:
  import doctest
  doctest.testmod()
  sys.exit(0)

if len(opt.args)<1:
  ap.print_help()
  sys.exit(0)

for arg in opt.args:
  print('%s:'%arg)
  try:
    for addr in IPRange(arg,broadcast=opt.broadcast):
      try:
        name,aliases,addresses=socket.gethostbyaddr(addr)
        if opt.aliases and aliases:
          print('  %-15s %s (aka: %s)'%(addr,name,', '.join(aliases)))
        else:
          print('  %-15s %s'%(addr,name))
      except socket.herror as e:
        if opt.verbosity==1:
          print('  %-15s'%addr)
        elif opt.verbosity>1:
          print('  %-15s ERROR: %r'%(addr,e))
  except IPRange.Error as e:
    print(str(e))
    sys.exit(1)
