#!/bin/nawk -f
# Usage:  mk-accessdb file ...
#
# Rewrites the "smtp.access" config file to standard output so that it
# can be piped into sendmail's "makemap" program and become the smtp.access.db
# sendmail database. A typical command line is:
#
#	$ mk-accessdb smtp.access | makemap hash smtp.access
#
# Exit status is 0 if no errors were found, else 1. This makes the
# program usable for syntax checking, one can redirect stdout to /dev/null
# and just test the exit status.

#
# Author:	Gjermund Srseth <gjermund@xyzzy.no>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.



#########################################################################
#                                                                       #
#         General IP-address & netmask manipulating functions           #
#                                                                       #
#########################################################################


# Does string argument "s" contain in it a valid dotted decimal IP address
# consisting of at least "octets" number of octets? A valid octet is a
# 1-3 digit positive decimal number less then 256 which may be padded
# with leading 0's. The string may contain whitespace before and after
# the adress. Return value is 1 or 0.
#
# Example: Arguments "15.002" and "4" would return 0, but "15.002" and "1"
# would return 1. Argument "15.0002" or "312.44.5.109" or "foo" would return 0.
#
function is_ip_address(s, octets,   a,n)
{
	if (split(s, a) != 1)
		return 0

	s = a[1]		# Strip off surrounding whitespace.

	if ( (n = split(s, a, ".")) < octets+0 || n > 4 )
		return 0

	for ( ; n; --n ) {

		if ( a[n] !~ /^[0-9][0-9]?[0-9]?$/ || a[n]+0 > 255 )
			return 0
	}

	return 1
}

# Takes a string argument "s" containing a dotted IP address (1-4 octets)
# and returns it canonified - i.e. four octets with no leading 0's.
# Example:  "192.055.002.33" --> "192.55.2.33" and "193.5" --> "193.5.0.0"
# If the input is not a valid IP address, return "".
#
function canonify_ip_address(s,   a,n,m)
{
	if ( !is_ip_address(s, 1) )
		return ""

	split(s, a)		# Strip off surrounding whitespace.
	s = a[1]		#

	n = split(s, a, ".")

	# Remove leading 0's from the octets that are there
	# and append a ".0" for each octet left out:
	#
	s = a[1] + 0

	for (m = 2; m <= 4; ++m)
		s = s "." (m <= n ? a[m]+0 : 0)

	return s
}


# This function takes one string argument "s", which must contain a dotted
# decimal IP address (1-4 octets) optionaly followed by a netmask which
# can be of the "/nn" type or the dotted "255.255.192.0" type. If the
# mask is missing, it is implied by the number of octets in the IP address.
#
# Examples of valid input:  "193.6.55.0/26" or "193.6.55   /26"
# or "193.006.55.000 255.255.255.192" or "193.6.055/26" or "178.33".
#
# This function fetches the IP address and retuns it on a "A.B.C.D"
# canonical format (see canonify_ip_address()). It returns "" if it
# detects an error in s.
#
function get_ip_address(s,   a)
{
	split(s, a)			# Get the address part of the string

	sub(/\/.*/, "", a[1])		# Strip away any "/nn" mask

	return canonify_ip_address(a[1])
}


# This function takes one string argument "s", which must contain a dotted
# decimal IP address (1-4 octets) optionaly followed by a netmask which
# can be of the "/nn" type or the dotted "255.255.192.0" type. If the
# mask is missing, it is implied by the number of octets in the IP address.
#
# Examples of valid input:  "193.6.55.0/26" or "193.6.55   /26"
# or "193.006.55.000 255.255.255.192" or "193.6.055/26" or "178.33".
#
# This function fetches the netmask and retuns it as a number in the
# range 0-32, the number of "1" bits in the mask counted from the left.
# It returns -1 if it detects errors in the mask spesification in s.
#
function get_ip_mask(s,   a,b,n,m)
{
	if ( match(s, /\/[0-9]*/) ) {		# If "/nn" style mask
						#
		s = substr(s, RSTART + 1, RLENGTH - 1)

		return (RLENGTH > 3 || RLENGTH < 2 || s+0 > 32) ? -1 : s+0

	} else if ( (n = split(s, a)) == 2) {	# If "255.255.192.0" style mask
						#
		if ( (s = canonify_ip_address(a[2])) == "" )
			return -1

		split(s, a, ".")

		for (n = 1; n < 4 && a[n] == 255; ++n)
			;

		# Below "n" in array "a" there are now only 255's, above
		# n there should be only 0's.

		b[0]   = 0; b[128] = 1; b[192] = 2; b[224] = 3; b[240] = 4;
		b[248] = 5; b[252] = 6; b[254] = 7; b[255] = 8;

		for (m = n + 1; m <= 4 && a[m] == 0; ++m)
			;

		return (m < 5 || !(a[n] in b)) ? -1 : 8 * (n - 1) + b[ a[n] ]
		
	} else if (n == 1) {			# Mask implied from IP address

		s = a[1]

		return is_ip_address(s, 1) ? split(s, a, ".") * 8 : -1
	}

	return -1
}


# Generate a list of "partial" ip addresses (strings containing 1-4 octets)
# into array "a" for a given range (ip-address/netmask) input in string "s".
#
# This list will contain all octets that ip-addresses in the given range
# will begin with. This list makes it easy to later test weather any ip
# address is within that address/mask range, one just needs to test if the
# ip address starts with any of those octet combinations that were generated.
#
# Examples:
# Given the input "86.160.0.0/14", print "86.160", "86.161", "86.162"
# and "86.163" since all ip addresses in the range given by this address and
# mask will start with those two octets. Given the input "204.50.128.0/18",
# print all 128 strings "204.50.128" through "204.50.255".
#
# If a "value" is given, it will become the array's value for all the
# generated octet-string keys.
#
function gen_ip_range(s,a,value,   addr,prefix,octet,b,n,i,fmt)
{
	addr	= get_ip_address(s)
	prefix	= get_ip_mask(s)

	if (addr == "" || prefix < 0)
		return "syntax error in address/mask"

	# In which of the 4 octets in "addr" does the prefix end
	# and how long is the prefix inside that octet:
	#
	octet = int((prefix - 1) / 8) + 1

	if (octet > 1)
		prefix = (prefix - 1) % 8 + 1

	# Check if all the bits in addr after erasing the bits corresponding
	# to the prefix are all 0's:
	# 
	split(addr, b, ".")

	for (n = octet + 1; n <= 4 && b[n] == 0; ++n)
		;

	if ( n < 5 || b[octet] % 2^(8 - prefix) > 0) {
		return "address not consistent with mask"
	}

	# Output
	#
	n = 2^(8 - prefix)

	fmt = (octet == 1) ? "%d"		: \
	      (octet == 2) ? "%d.%d"		: \
	      (octet == 3) ? "%d.%d.%d"		: \
			     "%d.%d.%d.%d"

	for (i = 0; i < n; ++i) {

		a[ sprintf(fmt, b[1], b[2], b[3], b[4]) ] = value

		b[octet]++
	}

	return ""
}




#########################################################################
#                                                                       #
#                          Main program                                 #
#                                                                       #
#########################################################################

function error(s) { print "Error in file " s | "cat 1>&2" }


		{ sub( /#.*/, " " ) }

NF == 0		{ next }

		{
		  s = $0

		  n = split($0, quote, "\"")
		  sub( /".*/, "" )

		  if (n != 1 && n != 3 || $1 !~ /^(permit|deny)$/) {

			err++
			error( FILENAME " line " FNR ":\n\t" s )

		  } else if (NF == 2 && $2 ~ /@/) {

			# "deny user@domain" or "deny @domain"
			#
			print $2, (n == 3) ? quote[2] : $1

		  } else if (NF <= 3 && $2 ~ /^[0-9\/.]*[0-9]$/) {

			# "deny 192.40.0" or "deny 192.40.0.0 255.255.255.0"
			# or "deny 192.40.0.0/24"
			#
			mask = get_ip_mask( $2 " " $3 )

			addr[ mask ] = addr[ mask ] "|" $2 " " $3
			perm[ mask ] = perm[ mask ] "|" $1

		  } else {

			err++
			error( FILENAME " line " FNR ":\n\t" s)
		  }

		}

END		{ 
		  for (i = -1; i <= 32; i++) {

			n = split( addr[i], a, "|" )
			    split( perm[i], p, "|" )

			for (j = 2; j <= n; j++) {

				s = gen_ip_range( a[j], ranges, p[j] )

				if ( s != "") {
					err++
					error(":\n\t" s ": " a[j])
				}
			}
		  }

		  for (i in ranges)
			print i, ranges[i]

		  exit( (err > 0) ? 1 : 0)
		}
