#!/usr/local/bin/perl

# read the lpd.conf file, and set up values from it

package LPRng;
require 5.003;
use Exporter ();


@ISA = qw(Exporter);
@EXPORT = qw(
Set_Debug
Setup_LPRng
Get_printer_name
FixStrVals
Setup_pc_entry
Real_printer
MatchHost
MakeMask
Read_printcap_file
CheckRecurse
Read_pc_entry
Dump_index
Dump_pc
Read_conf
Dump_conf
Fix_value
trimall
Get_remote_pr_host
getconnection
sendit
sendbuffer
sendfile
);

use strict;
use FileHandle;
use Sys::Hostname;
use Socket;
use English;

 sub FixStrVals( $ \% );
 sub Setup_pc_entry( $ );
 sub Real_printer( $ );
 sub MatchHost( \@ $ );
 sub MakeMask( $ );
 sub Read_printcap_file( $ \% \% $ $ \@ );
 sub CheckRecurse( $ \% \% $ $ \@ );
 sub Read_pc_entry( $ );
 sub Dump_index( $ \% );
 sub Dump_pc( $ \% );
 sub Read_conf( $ \% );
 sub Dump_conf( $ \% );
 sub Fix_value( $ );
 sub trimall( $ );

my(
$Debug, %Init_hash, %Pc_hash, %Pc_index, @Hostname, %Keyvals,
);

# permanent values
# Debug level
#
# %Init_hash: lpd.conf file values
# %Pc_hash:   printcap entries
# %Pc_index:  printcap entry names
# @Hostname:  hostname information, used for 'oh' printcap information
# 

# maximum depth of recursion for printcap file lookup
my($Max_depth) = 10;

sub trimall( $ )
{
	my($line) = @_;
	$line ||= "";
	$line =~ s/^\s+//;
	$line =~ s/\s+$//;
	return( $line );
}

# convert a printcap or config file value into
# a corresponding string or integer value

sub Fix_value( $ )
{
	my($value) = @_;
	if( $value =~ /^=/ or $value =~ /^#/ ){
		$value = trimall( substr( $value, 1 ) );
	} elsif ( $value =~ /^\@/ ){
		$value = 0;
	} else {
		$value = 1;
	}
	return $value;
}

# sub Read_conf( $conf_file, \%conf_values )
# Read a configuration file
#  $conf_file = configuration file
#  $conf_values = hash to store values in
#


sub Dump_conf( $ \% )
{
	my($title, $hash) = @_;
	my($key);
	print "$title config\n";
	foreach $key (sort keys %$hash ){
		print "  '$key'='". $hash->{$key} . "'\n";
	}
}

sub Read_conf( $ \% )
{
	my($conf_file,$conf_values) = @_;
	my($file,$key,$value,$line);

	# open the conf file
	$file = new FileHandle;
	if( not defined( $file->open("<$conf_file")) ){
		return "cannot open $conf_file - $!";
	}
	while( defined( $line = <$file>) ){
		chomp $line;
		next if not $line or $line =~ /^\s*#/;
		($key,$value) = ($line =~ /^\s*([\w-]*)(.*)/);
		$value = trimall($value);
		($key = trimall($key)) =~ s/-/_/g;
		print "key '$key'='$value'\n" if $Debug > 2;
		$conf_values->{$key} = Fix_value( $value );
		print "set key '$key'='" . $conf_values->{$key} . "'\n" if $Debug > 2;
	}
	$file->close;
	Dump_conf( "Read_conf", %$conf_values ) if $Debug > 1;
	return "";
}

# Dump_pc( $title, %Pc_hash )
#  dump the printcap hash 
#

sub Dump_pc( $ \% )
{
	my($title, $hash) = @_;
	my($key, $name);
	$name = ();
	$name = \@{$hash->{'NAME'}};
	print "Dump_pc: $title pc '". join( "','",@$name) . "'\n";
	foreach $key (sort keys %$hash ){
		print "  '$key'='". $hash->{$key} . "'\n";
	}
}

sub Dump_index( $ \% )
{
	my($title, $hash) = @_;
	my($key);
	print "Dump_index: $title index\n";
	foreach $key (sort keys %$hash ){
		print "  '$key'='". $hash->{$key} . "'\n";
	}
}

# sub Read_pc_entry( $file )
#  $file = filehandle
#  find and read a printcap entry
#

my($lastline);

sub Read_pc_entry( $ )
{
	my($file) = @_;
	my($hash,$state,$escape,$line,@lines,$len,$i,@names);
	my($key,$value,$add_next);
	$state = "";
	$hash = ();
	$add_next = 0;
	print "Read_pc_entry: starting\n" if $Debug > 1;
	while( $lastline or defined( $lastline = <$file> ) ){
		$line = trimall( $lastline );
		print "line '$line'\n" if $Debug > 3;
		if( not $line or $line =~ /^\s*#/ ){
			$lastline = "";
			next;
		}
		# beginning of next entry?
		last if not $add_next and $line =~ /^\s*\w/ and $state ne "";
		# we get rid of escapes at the end of the line
		$lastline = "";
		$add_next = 0;
		($line, $escape) = ($line =~ /^(.*?)(\\*)$/);
		if( defined( $escape ) ){
			print "escape '$escape'\n" if $Debug > 3;
			$len = length($escape);
			if( $len % 2 ){
				$escape = substr($escape,0,$len-1);
				$add_next = 1;
			}
			$line .= $escape;
		}
		last if( not $state and $line =~ /^\s*include\s/ );
		$state .= $line;
		print "state '$state'\n" if $Debug > 3;
	}
	print "Read_pc_entry: final state '$state'\n" if $Debug > 2;
	if( $state eq "" ){
		return undef;
	}
	@lines = split( /\s*:+/,$state);
	if( $Debug > 3 ){
		print "Read_pc_entry: split values=\n";
		for( $i = 0 ; $i < @lines; ++$i ){
			print "[$i] '$lines[$i]'\n";
		}
	}
	@names = split( /\s*\|+/, shift(@lines));
	@names = map { trimall($_) } @names;
	@{$hash->{'NAME'}} = @names;
	foreach $line (@lines){
		($key,$value) = ($line =~ /^\s*([\w-]*)(.*)/);
		$value = trimall($value);
		($key = trimall($key)) =~ s/-/_/g;
		print "  key '$key'='$value'\n" if $Debug > 3;
		$hash->{$key} = Fix_value( $value );
		print "  set key '$key'='" . $hash->{$key} . "'\n" if $Debug > 3;
	}
	Dump_pc( "Read_pc_entry: final value", %$hash ) if $Debug > 1;
	return $hash;
}

sub CheckRecurse( $ \% \% $ $ \@ )
{
	if( defined $lastline ){
		my($v,$file) = split( ' ', $lastline );
		if( $v eq 'include' ){
			$lastline = "";
			print "CheckRecurse: file '$file'\n" if $Debug>0;
			my( $pc_file, $Pc_hash, $Pc_index, $server, $depth, $hostname ) = @_;
			Read_printcap_file($file, %$Pc_hash, %$Pc_index, $server, $depth, @$hostname );
		}
	}
}

# sub Read_printcap_file(
#  $pc_file - file name
#  %Pc_hash - hash to store printcap values in
#  %Pc_index - index of all printcap names
#  $server   - if $server != 0 then a server, and use server printcap entries
#  $depth    - recursion depth
#  @Hostname - hostname information
#
#   read the printcap file and produce a
#   hash with pointers to hashes of printcap vars
#
# Algorithm:
#   open file
#   while (read a printcap entry){
#     decode the printcap entry
#     if printcap values exist then
#        merge values
#     else
#        create printcap entry
#     endif
#   endwhile

sub Read_printcap_file( $ \% \% $ $ \@ )
{
	my( $pc_file, $Pc_hash, $Pc_index, $server, $depth, $hostname ) = @_;
	my($file,$file_name,$hash,$key,$value,$names,$first,$name);
	my($i,@n,@Hostentry);

	# open the conf file
	$file = new FileHandle;
	++$depth;
	print "Read_printcap_file: file '$pc_file', depth $depth\n" if $Debug>0;
	if( $depth > $Max_depth ){
		return "nesting too deep for '$pc_file'";
	}
	# get either file or filter
	$file_name = trimall($pc_file);
	if( ($file_name =~ s/^\|//) ){
		$file_name = $file_name . '|';
	} else {
		$file_name = '<' . $file_name;
	}
	$file_name = FixStrVals( $file_name, %Keyvals );
	print "Read_printcap_file: opening '$file_name'\n" if $Debug>0;
	if( not defined( $file->open($file_name)) ){
		return "cannot open '" . $file_name . "' - $!";
	}
	for(; defined( $hash = Read_pc_entry($file) );
			CheckRecurse($pc_file, %$Pc_hash, %$Pc_index, $server, $depth, @$hostname ) ){
		Dump_pc( "Read_printcap_file: checking", %$hash ) if $Debug > 1;
		if( $hash->{'server'} and not $server ){
			print "Read_printcap_file: " .
				"server=(pc '$hash->{server}', need '$server')\n"
				if $Debug>1;
			next;
		}
		if( $hash->{'oh'} and not MatchHost( @$hostname, $hash->{'oh'} ) ){
			print "Read_printcap_file: " .
				"oh '$hash->{oh}' not matched\n" if $Debug>1;
			next;
		}
		$names = $hash->{'NAME'};
		$first = $names->[0]; 
		# find out if we need to add or merge printcap
		# entries
		my(%k) = ();
		for( $i = 1; $i < @$names; ++$i ){
			$name = $names->[$i];
			$k{$name} = $name;
		}
		$value = $Pc_hash->{$first}->{'NAME'};
		if( defined @$value ){
			for( $i = 1; $i < @$value; ++$i ){
				$name = $value->[$i];
				$k{$name} = $name;
			}
		}
		@n = ( $first, sort keys %k );
		@{$Pc_hash->{$first}->{'NAME'}} = @n;
		foreach $key (keys %$hash){
			$value = $hash->{$key};
			if( $key ne 'NAME' ){
				$Pc_hash->{$first}->{$key} = $value;
			}
		}
		foreach $name (@$names){
			$Pc_index->{$name} = $first;
		}
		if( not $Pc_index->{'FIRST'} ){
			$Pc_index->{'FIRST'} = $first;
		}
		if( $Debug > 1 ){
			Dump_index( "Read_printcap_file: after adding '$first'", %$Pc_index );
			foreach $name (sort keys %$Pc_hash){
				Dump_pc( "Read_printcap_file: after adding '$first'", %{$Pc_hash->{$name}} );
			}
		}
	}
	if( $Debug > 0 ){
		Dump_index( "Read_printcap_file: after '$pc_file'", %$Pc_index );
		foreach $name (sort keys %$Pc_hash){
			Dump_pc( "Read_printcap_file: after '$pc_file'", %{$Pc_hash->{$name}} );
		}
	}
}

sub MakeMask( $ )
{
	my($mask) = @_;
	my($mnum,$v,@v,$x,$i,$j,@d);
	if( defined $mask ){
		if( $mask =~ /\./ ){
			$mnum = inet_aton( $mask );
		} else {
			if( $mask < 32 and $mask >= 0 ){
				$v = pack( "N", (1 << $mask ) - 1);
				@v = reverse split( '', unpack( "B32", $v ));
				for( $i = 0; $i < 4; ++$i ){
					$x = 0;
					for( $j = 0; $j < 8; ++$j ){
						$x *= 2;
						$x += $v[$i*8+$j];
					}
					$d[$i] = $x;
				}
				$i = join(".", @d );
				#print "MakeMask: generated $mask = '$i'\n" if $Debug > 5;
				$mnum = inet_aton( $i );
			} else {
				$mnum = inet_aton( "255.255.255.255" );
			}
		}
	} else {
		$mnum = inet_aton( "255.255.255.255" );
	}
	print "MakeMask: $mask = '" . inet_ntoa( $mnum ) . "'\n" if $Debug > 5;
	return $mnum;
}

# sub MatchHost( @Hostinfo, $matches )
#   @Hostinfo is value returned by gethostbyname()
#    ($name, $alises, $addrtype, $length, @addrs )
#      0      1        2          3       4
#   matches has format:  ((glob|ip/mask),)*

sub MatchHost( \@ $ )
{
	my($hostinfo,$matches) = @_;
	my(@list,$value,$addr,$mask,$anum,$mnum,$null,@v,$i,$ipaddr);
	@list = split( '[,\s]', $matches );
	foreach $value ( @list ){
		print "Matchhost: '$value' to $hostinfo->[0]\n" if $Debug>2;
		if( $value =~ /^\d/ ){
			# we have addr/mask
			$null = inet_aton("0.0.0.0");
			($addr,$mask) = split( '/',$value );
			$anum = inet_aton( $addr );
			$mnum = MakeMask( $mask );
			print "Matchhost: addr '" . inet_ntoa($anum) . "', mask '"
				. inet_ntoa($mnum) . "'\n" if $Debug>3;
			for($i = 4; $i < @$hostinfo; ++$i ){
				$ipaddr = $hostinfo->[$i];
				print "Matchhost: ipaddr '" . inet_ntoa($ipaddr) . "'\n" if $Debug>3;
				$ipaddr = ($ipaddr ^ $anum) & $mnum;
				print "Matchhost: result '" . inet_ntoa($ipaddr) . "'\n" if $Debug>3;
				if( $ipaddr eq $null ){
					print "Matchhost: found '".inet_ntoa( $hostinfo->[$i])."'\n" if $Debug>3;
					return 1;
				}
			}
		} else {
			# we have glob str
			$value =~ s/\./\\./g;
			$value =~ s/\*/.*/g;
			print "Matchhost: new value '$value'\n" if $Debug>3;
			if( $hostinfo->[0] =~ /$value/ ){
				print "Matchhost: found\n" if $Debug>3;
				return 1;
			}
		}
	}
	return 0;
}

# sub Setup_pc_entry( $name )
#  1. look up the pc entry
#  2. set the initial values to configuration defaults
#  3. combine the pc values
# returns: hash of combined values

sub Real_printer( $ )
{
	my($name) = @_;
	$name = $Pc_index{$name};
	return $name;
}


sub Setup_pc_entry( $ )
{
	my($name ) = @_;
	my($real, %hash, $value, $key, $tc_val, @tc_list, %tc_hash );
	$real = Real_printer( $name );
	if( not $real ){
		return undef;
	}
	print "Setup_pc_entry: pr '$name', using real '$real'\n" if $Debug > 2;
	%hash = %Init_hash;
	Dump_pc( "Setup_pc_entry: after init", %hash ) if $Debug > 3;
		
	$value = $Pc_hash{$real};
	Dump_pc( "Setup_pc_entry: pc value for '$real'", %$value ) if $Debug > 3;
	foreach $key (keys %$value){
		print "Setup_pc_entry: setting '$key'='$value->{$key}'\n" if $Debug > 5;
		$hash{$key} = $value->{$key};
	}
	Dump_pc( "Setup_pc_entry: pr '$name', real '$real'; result", %hash ) if $Debug > 1;
	# now we have to resolve the TC values
	#
	$tc_val = $hash{'tc'};
	$hash{'tc'} = "";
	if( $tc_val ){
		push @tc_list, split( /[\s,;:]/, $tc_val ); 
	}
	while( @tc_list ){
		$tc_val = shift @tc_list; 
		print "Setup_pc_entry: tc '$tc_val'" if $Debug > 5;
		$real = Real_printer( $tc_val );
		if( $tc_hash{$tc_val} ){
			print STDERR "Setup_pc_entry: Printer '$name' has tc with multiple uses of '$tc_val', really '$real'";
			return undef;
		}
		$tc_hash{$tc_val} = 1;
		if( not defined $real ){
			print STDERR "Setup_pc_entry: Printer '$name' missing tc entry for '$tc_val', really '$real'";
			return undef;
		}
		$value = $Pc_hash{$real};
		foreach $key (keys %$value){
			print "Setup_pc_entry: setting '$key'='$value->{$key}'\n" if $Debug > 5;
			if( $key ne 'NAME' ){
				$hash{$key} = $value->{$key};
			}
		}
		Dump_pc( "Setup_pc_entry: pr '$name', after tc '$real'", %hash ) if $Debug > 1;
		$tc_val = $hash{'tc'};
		$hash{'tc'} = "";
		if( $tc_val ){
			push @tc_list, split( '\s,;:', $tc_val ); 
		}
	}
	return \%hash;
}

sub FixStrVals( $ \% )
{
	my($str, $hash ) = @_;
	my( $key, $val );
	while( $str =~ /%(.)/ ){
		$key = $1;
		print "FixStrVals: fixing '$key' in '$str'\n" if $Debug > 5;
		$val = $hash->{$key};
		$val = "" if( not defined $val );
		$str =~ s/%$key/$val/g;
	}
	print "FixStrVals: final '$str'\n" if $Debug > 5;
	return $str;
}

sub Get_printer_name( \% )
{
	my($Args) = shift;
	my($printer);
	$printer ||= $Args->{'P'};
	$printer ||= $Pc_index{'FIRST'};
	$printer ||= $Init_hash{'default_printer'};
	print "Get_printer_name: '$printer'\n" if $Debug>0;
	return( $printer );
}


sub Setup_LPRng( \% )
{
	my($Args) = @_;
	my($pc_path,$file,$key);
	# get the command line options
	# get the hostname information
	$key = hostname();
	@Hostname = gethostbyname( $key );
	# set up the key values
	$Keyvals{'H'} = $Hostname[0];
	#Read_conf("/var/tmp/LPD/lpd.conf", %Init_hash);
	Read_conf("/etc/lpd.conf", %Init_hash);
	$pc_path = "/etc/printcap";
	if( $Init_hash{'printcap_path'} ){
		$pc_path = $Init_hash{'printcap_path'};
	}
	foreach $file ( split( '[:,]', $pc_path ) ){
		$file = FixStrVals( $file, %Keyvals );
		Read_printcap_file($file, %Pc_hash, %Pc_index, 1, 0, @Hostname);
	}
}

sub Set_Debug( $ )
{
	my($v) = $Debug;
	$Debug = $_[0];
}

# sub Get_remote_pr_host( $Printer, $Pc_value );
#  returns: ($pr, $remote, $port)
#  $pr = remote printer, $remote = remote host, $port = port to use
#
#  if Pc_value 
#    we use the lp value
#    if no lp value, we use rp, rm value
#  else
#    we use the lp value
#  if the lp value then we split it up
#

sub Get_remote_pr_host( $ $)
{
	my( $prname, $pc ) = @_;
	my( $lp, $pr, $remote, $port );

	if( defined $pc ){
		$lp = $pc->{'lp'};
	} else {
		$lp = $prname;
	}
	# we now check to see if we have pr@host
	if( defined $lp ){
		if( $lp =~ /\@/ ){
			($pr, $remote ) = split( '@', $lp );
		} else {
			$pr = $prname
		}
	} elsif( defined $pc ){
		$pr = $pc->{'rp'};
		$remote = $pc->{'rm'};
	}
	if( not $pr ){
		$pr = $prname;
	}
	$pr = $prname if( $pr =~ /%P/ );

	if( not $remote ){
		if( defined $pc ){
			$remote = "localhost" if $pc->{'force_localhost'};
		} else {
			$remote = "localhost" if $Init_hash{'force_localhost'};
		}
	}
	if( not $remote ){
		if( defined $pc ){
			$remote = $pc->{'default_remote_host'};
		} else {
			$remote = $Init_hash{'default_remote_host'};
		}
	}
	if( not $remote ){
		$remote = "localhost";
	}

	($remote, $port ) = split( '%', $remote );

	if( not $port ){
		if( defined $pc ){
			$port = $pc->{'lpd_port'};
		} else {
			$port = $Init_hash{'lpd_port'};
		}
	}
	if( not $port ){
		$port = "printer";
	}
	if( $port + 0 == 0 ){
		$port = getservbyname( $port, "tcp" );
	}
	return( $pr, $remote, $port );
}

sub getconnection ($ $)
{
	my ($remote,$port) = @_;
	my ($iaddr,$paddr,$proto);
	my ($low_port, $high_port, $ports, $t, $euid ) if $Debug>0;
	$ports = $Init_hash{'originate_port'};
	if( $ports ){
		($low_port, $high_port) = split( /[\s,;]+/, $ports );
		print "low_port '$low_port', high_port '$high_port'\n" if $Debug>0;
	}
	$low_port += 0;
	$high_port += 0;
	print "num low_port '$low_port', high_port '$high_port'\n" if $Debug>0;

	$iaddr = inet_aton($remote) or die "no host: $remote";
	$paddr = sockaddr_in($port,$iaddr);
	$proto = getprotobyname('tcp');
	print "remote='$remote', port ='$port', iaddr='" . inet_ntoa($iaddr). "'\n" if $Debug;
	$t = 0;
	if( $low_port < $high_port and ($EUID == 0 or $UID == 0 ) ){
		$euid = $EUID;
		$EUID = 0;
		while( $t == 0 and $low_port < $high_port ){
			close(SOCK);
			socket(SOCK,PF_INET,SOCK_STREAM,$proto) or die "socket: $!";
			setsockopt( SOCK, SOL_SOCKET, SO_REUSEADDR, 1 )
				or warn "setsockopt failed - $!\n"; 
			if( bind( SOCK, sockaddr_in( $low_port, INADDR_ANY ) ) ){
				$t = 1;
			} else {
				print "bind to $low_port failed - $!\n";
				++$low_port;
			}
		}
		$EUID = $euid;
	}
	if( $t == 0 ){
		close(SOCK);
		socket(SOCK,PF_INET,SOCK_STREAM,$proto) or die "socket: $!";
		setsockopt( SOCK, SOL_SOCKET, SO_REUSEADDR, 1 ) or warn "setsockopt failed - $!\n"; 
	}
	connect(SOCK,$paddr) or die "connect: $!";
	print "connection made\n" if $Debug;
	# unbufferred IO
	select(SOCK); $| = 1; select(STDOUT);
	return \*SOCK;
}

sub sendit( $ $ )
{
	my( $SOCK, $line ) = @_;
	my( $count );
	print "sendit sending '$line'\n" if $Debug;
	print $SOCK $line or die "print to socket failed - $!\n";
	$line = "";
	$count = read $SOCK, $line, 1;
	print "sendit read $count\n" if $Debug;
	if( !defined($count) ){
		die "read error on socket - $!\n";
	}
	if( !$count ){
		die "EOF on socket\n";
	}
	$count = unpack( "C", $line );
	if( $count ){
		print "error: ";
		while( defined ( $line = <$SOCK> ) ){
			print $line;
		}
		print "\n";
		exit 1;
	}
	print "sendit no error\n" if $Debug;
}

sub sendbuffer( $ $ $ )
{
	my($SOCK, $line, $buffer ) = @_;
	my( $count );
	print "sendbuffer line '$line'\n" if $Debug;
	sendit( $SOCK, $line );
	print "sendbuffer buffer '$buffer'\n" if $Debug;
	print $SOCK $buffer;
	print $SOCK "\000";
	$line = "";
	$count = read $SOCK, $line, 1;
	print "sendbuffer read $count\n" if $Debug;
	if( !defined($count) ){
		die "read error on socket - $!\n";
	}
	if( !$count ){
		die "EOF on socket\n";
	}
	$count = unpack( "C", $line );
	if( $count ){
		print "error code: $count\n";
		while( defined($line = <$SOCK>) ){
			print $line;
		}
		print "\n";
		exit 1;
	}
	print "sendbuffer no error\n" if $Debug;
}

sub sendfile ( $ $ $ )
{
	my( $SOCK, $name, $filename ) = @_;
	my( $size, $line, $count );
	open( FILE, "<$filename") or die "cannot open file '$filename'\n";
	$size = -s FILE;
	print "sendfile: '$name' size $size\n" if $Debug;
	sendit( $SOCK, "\003$size $name\n" );
	print "sendfile: sending file\n" if $Debug;
	while( $size = read FILE, $line, 1024 ){
		print "read $size bytes\n" if $Debug;
		print $SOCK $line;
	}
	print "sendfile: finished\n" if $Debug;
	if( !defined( $size ) ){
		die "bad read from '$name' - $!\n";
	}
	print $SOCK "\000";
	$line = "";
	$count = read $SOCK, $line, 1;
	print "sendfile: read $count\n" if $Debug;
	if( !defined($count) ){
		die "read error on socket - $!\n";
	}
	if( !$count ){
		die "EOF on socket\n";
	}
	$count = unpack( "C", $line );
	if( $count ){
		print "error code: $count\n";
		while( defined($line = <$SOCK>) ){
			print $line;
		}
		print "\n";
		exit 1;
	}
	print "sendfile: no error\n" if $Debug;
}

$Debug = 0;
1;


syntax highlighted by Code2HTML, v. 0.9.1