#!/usr/bin/perl -w
#
##
#
# ASCFILE - Openradius module that can read legacy-style clients, users, 
# realms, hints and huntgroups files
#
##
#
# Usage: ascfile [-d] file...
#	 ascfile -h
# 
# -d increases verbosity on stderr and allows module to run standalone
#
# Multiple files named on the command line are effectively concatenated.
#
# Comments are started by a # and ended by a newline.
#
# A record consists of a key, followed by whitespace, followed by a series of 
# A/V items, each separated by a comma or whitespace. If additional A/V items
# follow on subsequent lines, those lines must be indented with whitespace.
#
# Multiple entries having the same key are effectively concatenated.
#
# An A/V item can either be a bare value or a full A/V pair.
#
# A bare value will get the attribute 'int' if the value seems numeric, 'ip' 
# if the value is an IP address, and 'str' otherwise, to form complete pairs.
#
# The module uses the first 'str' attribute from the request as its key, and 
# *returns all pairs* associated with that key. That means it doesn't handle
# check items the way you're used to; it doesn't match them first but just 
# returns them. 
#
# If you want to compare items, and possibly use another key upon a mismatch,
# you can add a rule to do that in the behaviour file. Otherwise, you should 
# rewrite eg. a series of DEFAULT items, each with a check item like 
# Service-Type = Login, Framed, etc., as keyed on the service type instead.
#
# This module could have been written to deal with check items the way you're
# used to - but I feel that the type of logic that is needed there shouldn't 
# be placed outside the behaviour file. And in most cases, the files will be
# oriented on a single key anyway, so they can be easily rewritten.
#
# If you'd really need to have two keys, a users-type of file isn't the best
# solution anyway, as you'd get n x m entries that will probably all be quite 
# similar. Better split the information across two tables. But if you 
# absolutely cannot, you can merge your check items all into the key, like 
# steve:23555443:3, and call the module from the behaviour file like this:
# Ascfile(str=User-Name . ":" . Calling-Station-Id . ":" . Service-Type)
#
# After answering each request, the module checks if any of the files are 
# newer than when they were last read, and if so, it rereads all.
# (that's not implemented yet).
#
##
#
# Author:
# Emile van Bergen, emile@evbergen.xs4all.nl
#
# Permission to redistribute an original or modified version of this program
# in source, intermediate or object code form is hereby granted exclusively
# under the terms of the GNU General Public License, version 2. Please see the
# file COPYING for details, or refer to http://www.gnu.org/copyleft/gpl.html.
#
# History:
# 2001/10/16 - EvB - First version
# 2002/08/30 - Brian Candler - changed parsing so that any line which does
#              not start with whitespace is taken as a new key.
#
##


########
# USES #
########

use Getopt::Long;


###########
# GLOBALS #
###########

$debug = 0;
$single = 0;


#############
# FUNCTIONS #
#############

# Readfile
#
# Returns a reference to a hash keyed on the file key, of references to arrays 
# of pairs that are associated with that file key. Accepts a list of files as 
# the argument.

sub readfile {

    my %entries;
    foreach my $file (@_) {

	open(FH, $file) or 
		die("ascfile[$$]: ERROR: Could not open $file: $!\n");

	$debug > 1 and print STDERR "READING FILE '$file'\n";

	my $key;
	while(<FH>) {
		# Skip comment and blank lines
		next if /^\s*(#|$)/;

		# Extract optional key from beginning of line
		if (s/^\r?([^\s]+)//) {
			$key = $1;
			$entries{$key} = [] unless exists $entries{$key};
			$debug > 1 and print STDERR "---$key---\n";
		}

		# Process the rest as items, each of which is either a real 
		# A/V-pair or a bare value, and parsed as three parts:
		# 1. an optional attribute spec. as in [A-Za-z0-9-]+\s*=\s*
		# 2. - an (hexa)decimal number, as in [A-Fa-f0-9]+ 
		#      (we add int= if no attribute specified)
		#    - or an IP address as in \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
		#      (we add ip= if no attribute specified)
		#    - or a quoted string, as in "([^"]*|\\")*"
		#      (we add str= if no attribute specified)
		#    - or a bare string, as in [^\s,]+
		#      (we add str= if no attribute specified)
		# 3. mandatory whitespace, comma, or end-of-record.
		#    (this is important to make sure the pairs are separated,
		#    so a bare string 'Deadbody' won't generate two pairs:
		#    a hex number int=deadb and a string str=ody).

		while(s/^\s*(
			  ([A-Za-z0-9:-]+)\s*=\s*		# 1. attr. spec.
			)?
			(
			  ([A-Fa-f0-9]+)|			# 2. hex nr.
			  (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|	#    or IP addr
			  ("([^"]|\\")*[^\\]")|			#    or quoted
			  ([^\s,#]+)				#    or bare str
			)
			(
			  [\s]+|,|$				# 3. end of item
			)//x) {

			# Establish the full pair
			my $pair=($2 or $4 && 'int' or $5 && 'ip' or 'str') . 
				 " = $3";
			$debug > 1 and print STDERR "\t$pair\n"; 

			# Add to this key's array of pairs in hash
			push @{$entries{$key}}, $pair;
		}
		# ignore trailing space/comment
		next if /^\s*(#|$)/;
		warn("ascfile: $file line $.: Unable to parse (ignored): $_");
	}
	close(FH);
    }

    if ($debug) {
	print STDERR "ascfile[$$]: In memory after reading @_:\n";
	foreach my $key (keys %entries) {
		print STDERR "---$key---\n";
		foreach my $pair (@{$entries{$key}}) {
			print STDERR "\t$pair\n";
		}
	}
    }

    # Return ref to the created hash of array refs 
    return \%entries;	
}


########
# MAIN #
########

# Get options
Getopt::Long::Configure("bundling");
GetOptions("h"  => \$usage,
	   "s"  => \$single,
	   "d+" => \$debug);
if ($usage) { die("Usage: ascfile [-d] file...\n       ascfile -h\n"); }
if ($single) { warn("ascfile: warning: -s flag no longer has any effect\n"); }

# Check that rest of command line is not empty
unless (@ARGV) { die("ascfile: ERROR: no file(s) specified!\n"); }

# Check that we're running under OpenRADIUS, interface version 1
unless ($debug ||
	$ENV{'RADIUSINTERFACEVERSION'} &&
	$ENV{'RADIUSINTERFACEVERSION'} == 1) {
	die("ascfile: ERROR: not running under OpenRADIUS, interface v1!\n");
}

# Read the files mentioned on the command line
my $entries = &readfile(@ARGV);

# Set record separator to empty line and loop on input.
$/ = "\n\n";
$| = 1;			# Important - we're outputting to a pipe
while(<STDIN>) {

	# Parse pairs from server's request message
	chomp;
	/^\s*str\s*=\s*"(.*)"\s*$/m and my $str = $1;

	# Debugging
	$debug and print STDERR "ascfile[$$]: got request: $_\n" .
				"ascfile[$$]: last str in request: $str\n";

	# Output array of pairs for this key if we got one and it's in the hash
	if (defined $str && defined $entries->{$str}) {
		foreach my $pair (@{$entries->{$str}}) {
			print "$pair\n";
		}
		print "int=1\n";
	}
	else { print "int=0\n"; }

	# Done
	print "\n";
	undef $str;
}

