#!/usr/bin/perl -w
#
#	$Id: ipfmeta,v 1.4 2001/01/29 08:37:56 camield Exp $
#
# Copyright (c) 2000, 2001 Camiel Dobbelaar <cd@sentia.nl>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

use strict;

# Globals
my $Group = 0;
my $Maxrec = 10;
my $Objfile = "ipf.objs";
my $Obj; my @Objs;
my $Val; my %Vals;
my $Verbose = 0;

my $objfile = shift || $Objfile;
readobjects($objfile);

my @all_lines = <STDIN>;
@all_lines = cutw(@all_lines);

my $oline;
foreach (@all_lines) {
	if (m/^%/) {
		command($_);
		next;
	}

	if (m/^(#|$)/) {
		print "$_\n";
		next;
	}

	foreach $oline (expand(agroup($_))) {
		print "$oline\n";
	}
}

# end of main

sub agroup
{
	my $line = shift;

	if ($Group && ($line !~ m/group/)) {
		$line = "$line group $Group";
	}
	return $line;
}

sub command
{
	my $line = shift;

	for ($line) {
		m/verbose (\d+)/i and $Verbose = $1;
		m/group (\d+)/i   and $Group = $1;
		m/dump/i          and objdump();
	}
}

sub cutw
{
	my @ret_lines;

	foreach (@_) {
		push(@ret_lines, join(' ', split));
	}
	return @ret_lines;
}

sub expand
{
	my $line = shift;
	my $level = shift || 0;
	my @retlines = $line;

	# coarse protection
	if ($level > $Maxrec) {
		warn "ERR: recursion exceeds $Maxrec levels\n";
		return;
	}

	foreach $Obj (@Objs) {
		if ($line =~ m/$Obj/) {
			undef(@retlines);
			if ($level < $Verbose) {
				# add metarule as a comment
				push(@retlines, "# ".$line);
			}
			foreach $Val (@{$Vals{$Obj}}) {
				my $newline = $line;
				$newline =~ s/$Obj/$Val/;
				push(@retlines, expand($newline, $level+1));
			}
			last;
		}
	}
	return @retlines;
}

sub objdump
{
	unless (@Objs) {
		print "# dump: no objects\n";
		return;
	}
	foreach $Obj (sort @Objs) {
		print "# [$Obj]\n";
		foreach $Val (sort @{$Vals{$Obj}}) {
			print "# \t '$Val'\n";
		}
	}
}

sub readobjects
{
	my $objfile = shift;
	my @tokens;

	unless (open(FH, "$objfile")) {
		if ($objfile eq $Objfile) {
			return;
		} else {
			die "cannot open $objfile: $!\n";
		}
	}
	while (<FH>) {
		push(@tokens, tokenparse(cutw($_)));
	}
	close(FH) || die "cannot close $objfile: $!\n";
	# link objects with their values
	$Obj = "";
	while (@tokens) {
		my $token = shift(@tokens);
		if ($token =~ m/^\[([^\]]*)\]$/) {
			# new object
			$Obj = $1;
		} else {
			# new value
			push(@{$Vals{$Obj}}, $token) unless ($Obj eq "");
		}
	}
	# sort objects: longest first
	@Objs = sort { length($b) <=> length($a) } keys %Vals;
}

sub tokenparse
{
	my @ret_tokens;
	$_ = shift;

	while ($_) {
		next if (s/^\s+//);
		last if (m/^#/);
		my $token = "";
		if (s/^"//) {
			$token = $1 if (s/^([^"]*)"//);
		} elsif (s/^\[//) {
			$token = "[$1]" if (s/^([^\]]*)\]//);
		} else {
			$token = $1 if (s/^([^\s]+)//);
		}
		push(@ret_tokens, $token) unless ($token eq "");
	}
	return @ret_tokens;
}

__END__

=head1 NAME

B<ipfmeta> - use objects in IPfilter files

=head1 SYNOPSIS

B<ipfmeta> [F<objfile>]

=head1 DESCRIPTION

B<ipfmeta> is used to simplify the maintenance of your IPfilter
ruleset. It does this through the use of 'objects'. A matching
object gets replaced by its values at runtime. This is similar to
what a macro processor like m4 does.

B<ipfmeta> is specifically geared towards IPfilter. It is line
oriented: if an object has multiple values, the line with the object
is duplicated and substituted for each value. It is also recursive:
an object may have another object as a value.

Metarules to be processed are read from stdin, output rules go to
stdout.

Definition of the objects and their values is done in a separate
file; the filename defaults to F<ipf.objs>. An object is delimited
by square brackets. A value is delimited by whitespace, except when
it is enclosed by double-quotes. Comments start with '#' and end
with a newline. Empty lines and extraneous whitespace are allowed.
A value belongs to the object that precedes it.

B<ipfmeta> has a command mode. Metarules starting with '%' are
passed to the command processor. The commands are listed below.

It is recommended that you use all caps or another distinguishing
feature for object names. You can use B<ipfmeta> for NAT rules also,
for instance to keep them in sync with filter rules. Combine
B<ipfmeta> with a Makefile to save typing.

=head1 COMMANDS

=over 4

=item B<dump>

Include a list of all objects with their values in the output as
comments. This can be used to verify if the objectfile is parsed
correctly.

=item B<group> F<n>

Append 'group n' to subsequent output rules. Use 'group 0' to stop
appending groups to the output. This is also the default.

=item B<verbose> F<level>

Include expanded metarules in output as comments. The default is
0, do not add any comments. Higher verbosity levels cause deeper
levels of expanded metarules to be included.

=back

=head1 EXAMPLES

ipfmeta ipf.objs <ipf.metarules >ipf.rules

cat ipf.metarules | ipfmeta | ipf -I -Fa -vf -

=head1 AUTHOR

Camiel Dobbelaar <cd@sentia.nl>

=cut
