#!/usr/bin/perl -w
# @(#) bsdprint.pl			BSD print client (Ref: RFC 1179).
#					Feeds jobs to remote print server
#					without generating spool files.
#
# Copyright (c) 2002 Graham Jenkins <grahjenk@au1.ibm.com>. All rights reserved.
# This program is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.

use strict;
use IO::Socket;
use File::Basename;
use Sys::Hostname;
use Env qw(LOGNAME TIMEOUT);
use vars qw($VERSION);
$VERSION = "1.03";
my (@dests,$invalid,$debug,$prev,$start,
    $sock,$buffer,$hostname,$sequence,$bytes,$file,$fno,$cfl,$j);
my ($copies,$bufsiz,$timeout,$user) = (1,512,3600,"nobody");
foreach my $j (@ARGV) {			# Untaint and validate the arguments.
  if ($j =~ /^([-=:\@\w.]+)$/){ unshift(@dests,$1)                      }
  else                        { die "Bad data in $ARGV[$j]\n"           }
  if ( $dests[0]=~/^=\d+$/)   { $copies=$dests[0];$copies=~s/^=//;
                                shift(@dests); next                     }
  if ( $dests[0]=~/^-/    )   { $debug=1; $dests[0]=~s/^-//             } 
  if ( $dests[0]=~/^\d+$/ )   { $bufsiz=$dests[0]; shift(@dests); next  }
  if ( $dests[0]!~/:/     )   { $invalid=1; last                        }
}

$0 = "bsdprint.pl" if $0 =~ m%^/dev/%;	# Perform usage check.
die "Usage: ".basename($0).
            " server1:printer1 [server2:printer2] .. [size] [=copies]\n".
  "Where: 'size' is the maximum size (kb) of a data file and may be\n".
  "       negated for debug purposes. Up to 2704 data files will be created.\n".
  "e.g.:  ".basename($0)." pserver1:lp1 500\n"
  if ( ($invalid) or ($#dests<0) or ($bufsiz<1) or($copies<1) );

do {$timeout=$TIMEOUT} if ( (defined $TIMEOUT) && ($TIMEOUT=~/^\d+$/) );
do {$user=$LOGNAME   } if ( (defined $LOGNAME) && ($LOGNAME=~/^\w+$/) );
$prev=$start=time;		
while ( (my $t=$start + $timeout - time) >=0 ) {
  print STDERR "Timeout will occur in: $t seconds.\n" if $debug;
  for (my $port=721;$port<=731;$port++){# Open a local port.
    foreach my $dest (reverse(@dests)) {# Connect to a server.
      my ($server,$printer,$junk) = split(/:/,$dest);
      print STDERR "Opening LocalPort: $port  to: $server\n" if $debug;
      $sock = new IO::Socket::INET(LocalPort=>$port,
                                   PeerAddr=>$server.":515",Reuse=>1) or last;
      print STDERR "Sending Rcv-print-job request for: $printer\n" if $debug;
      if (defined send_ack("\002".$printer."\n")) {
        shutdown($sock,2); close $sock; next
      }
      $hostname=hostname() or $hostname="myhost";
      $hostname=gethostbyaddr($j,AF_INET) if ($j=gethostbyname($hostname));
      $sequence=get_seq(1000);
      while (length($sequence)<3) {$sequence="0".$sequence}
      $sequence=$sequence.$hostname;
      $fno=-1; $cfl=0;
      while ( read(STDIN, $buffer, $bufsiz*1024) >0 ) {
        $bytes=length($buffer);		# Fill buffer, generate data-file name,
        $file=get_name($fno);		# send "receive-data-file" directive.
        print STDERR "Sending data file: $file  $bytes bytes\n" if $debug;
        send_ack("\003".$bytes." ".$file."\n", "Receive-data-file");
        send_chk($buffer, "Data-file");	# Send data-file, get acknowledgment.
        send_ack("\000","Date-file-end");
        $cfl+= (1+$copies)*(length($file)+2) + $copies*(length("Nstdin_fA")+1)
      }
      print STDERR "Sending cntrl file: cfA$sequence  user: $user\n" if $debug;
      $buffer=$buffer."H".$hostname."\nP".$user."\n";
      send_ack("\002".(length($buffer)+$cfl)." cfA$sequence\n","Rcv-cntl-file");
      for ( my $i=1;$i<=$copies;$i++ ) {	
        $j=-1;
        while ($j<$fno)	 {		# Send control-file.
          $file=get_name($j);
          $buffer=$buffer."l".$file."\n";
          $buffer=$buffer."U".$file."\n" if $i==$copies;
          $buffer=$buffer."Nstdin_".substr($file,1,2)."\n";
          send_chk($buffer,"Ctrl-file");# Construct and send records relating
          $buffer=""			# to each data-file individually so that
        }				# buffer length is never too large.
      }
      send_ack("\000","Control-file-end");
      print STDERR "Closing LocalPort: $port!\n" if $debug;
      close $sock;			# Get acknowledgment, and exit.
      exit
    }
  }
  sleep 7 if $timeout > 15
}

sub get_seq {				# Usage: get_seq($j);
  use File::Spec;			# Returns next in sequence of $j
  use Fcntl qw(:DEFAULT :flock);	# numbers starting at 0.
  use Env qw(TEMP TMP TMPDIR);
  foreach my $dir ( $TEMP, $TMP, $TMPDIR, "/var/tmp", "/tmp") {
    if ( (defined $dir) && (-d $dir) ) {
      my $file=File::Spec->catfile($dir,basename($0).".seq");
      for (my $try=0;$try<=2;$try++) {
        close FH; sleep $try;
        sysopen(FH, $file, O_RDWR|O_CREAT) or next;
        eval {flock(FH, LOCK_EX)}; next if ! $@ =~ /unimplemented/;
        my $seq = <FH>                     || 0;
        chomp($seq);
        seek(FH, 0, 0)                     or next;
        truncate(FH, 0)                    or next;
        print FH ($seq+1)%$_[0], "\n"      or next;
        close FH                           or last;
        return $seq
      }
    }
  }
  die "Could not open sequence file!\n"
}

sub get_name {				# Usage: get_name($j);
  my @alpha1=("f".."z","a".."e","A".."Z");
  my @alpha2=("A".."Z","a".."z");
  return my $file="d".$alpha1[$_[0]/52].$alpha2[$_[0]%52].$sequence
    if $_[0]++ <= 2702;			# Increments $j and returns filename
  die "Input too large!\n"		# according to LPRng sequencing
}					# scheme.

sub send_ack {				# Usage: send_ack($string, $comment);
  my $stat;
  return if ( (send($sock,$_[0],0) == length($_[0])) &&
              (defined recv($sock,$stat,1,0))        &&
              ($stat eq "\000")                         );
  die "Failed whilst doing: $_[1]\n" if defined $_[1]
}

sub send_chk {				# Usage: send_chk($string, $comment);
  return if send($sock,$_[0],0) == length($_[0]);
  die "Failed whilst doing: $_[1]\n"
}
__END__

=head1 NAME

bsdprint - BSD print client (Ref: RFC 1179)

=head1 README

bsdprint is a BSD print client which 
feeds jobs to a remote print server
without generating spool files.

=head1 DESCRIPTION

C<bsdprint> is a BSD print client which feeds jobs to a
remote print server without generating spool files. This
is accomplished through the use of multiple data files
per job, with the size of these being such that each can
be accomodated in memory. Several alternate destinations
can be given in the command line.

C<bsdprint> can be invoked via an input filter on BSD-based
systems, or called in place of 'netpr' or similar programs
on Solaris or other System V platforms.

=head1 USAGE

=over 4

=item bsdprint server1:lp1 [server2:lp2] .. [size] [=copies]

=back

The 'size' parameter gives the maximum data file size in kb;
a value of 512 will be used by default. Up to 2704 data files
will be constructed as necessary. Some servers will not
accept this many data files.

If a negative value is given for 'size', the absolute value
will be used, and progress messages will be written to STDERR.

Some servers (e.g. Solaris, JetDirect) are unwilling or unable
to print more than 1 copy.

Windows users may find it convenient to use a utility like
'redmon' <www.cs.wisc.edu/~ghost/redmon> for feeding print
jobs to this program.

=head1 PREREQUISITES

C<bsdprint> should be executed with root priveleges so
that it can use source ports 721 through 731 as
specified in RFC 1179.

=head1 SCRIPT CATEGORIES

Networking
UNIX/System_administration

=head1 AUTHOR

Graham Jenkins <grahjenk@au1.ibm.com>

=head1 COPYRIGHT

Copyright (c) 2002 Graham Jenkins. All rights reserved.
This program is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.

=cut
