#!/usr/local/bin/perl
#
# sendpage is the tool that will handle all the paging functions
#
# $Id: sendpage,v 1.53 2001/11/04 23:02:35 nemies Exp $
#
# Copyright (C) 2000,2001 Kees Cook
# cook@cpoint.net, http://outflux.net/
#
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# http://www.gnu.org/copyleft/gpl.html

=head1 NAME

sendpage - listen for pages via SNPP, and send pages via modem

=head1 SYNOPSIS

sendpage [OPTIONS] [recipient ...]

=head1 OPTIONS

=over 4

=item -bd

Start sendpage in "daemon mode" where it will start all the Paging
Central queues and wait for pages to be delivered.  When sendpage
runs as a daemon, it must be running as the 'sendpage' user as specified
in the sendpage.cf file.

=item -bp

Display all the pages waiting in the Paging Central queues.

=item -bv

Try to expand the "recipient" name, using the recipient aliases specified
in the configuration file.

=item -bs

Shutdown the running sendpage daemon and all its children.  If a Paging
Central is in the middle of delivering a page, it will finish up and
exit as soon as its current page is handled.

=item -br

This will send a SIGHUP to the master daemon.  When the master gets the
SIGHUP, it will re-read its configuration file, and restart all the
Paging Centrals.  It will wait for any busy Paging Centrals to finish
before continuing.

=item -bq

This displays the state of the running daemons: Running or Not running.
If a pid file is stale (the file exists, but the process doesn't), it
will mark that pid as "Stale".

=item -q[R pc]

This will send a SIGUSR1 signal to either the master daemon, or,
if the Paging Central is specified, just that Paging Central in particular.
When the master gets a SIGUSR1, it will send it to each of the running Paging
Centrals.  If the Paging Central is not busy, it will immediately start a
queue run.

=item -C FILE

Read the configuration file FILE instead of the default /etc/sendpage.cf

=item -h

Display a summary of all the available command line options.

=item -d

Turn on debugging (like "debug=true" in /etc/sendpage.cf)

=item -f USER

Show that the sent page is coming from USER.  Default is the current user.

=item -m MESSAGE

Send the given MESSAGE instead of reading text from stdin.

=item -n

Do not notify the 'from' user about the status of the page.

=back

=head1 DESCRIPTION

Sendpage can run as the delivery agent, or as a client to insert a page
into the paging queue.  For the various command-line arguments, 
the idea here was to use sendmail-style arguments where I can, not to
fully implement every option that sendmail has.  I just want the
learning curve of sendpage to be small for people already familiar
with sendmail.

=head1 FILES

=over 4

=item F</etc/sendpage.cf>

Default location for sendpage.cf, which holds all the configuration
information for sendpage, including Paging Central definitions,
recipients, and various other behaviors.

=item F</var/spool/sendpage>

Default directory for all the Paging Central queues and pid files.

=item F</var/lock>

Default directory to keep the UUCP-style device locks.

=back

=head1 AUTHOR

Kees Cook <cook@cpoint.net>

=head1 BUGS

Oh, I bet this code is crawling with them.  :)  I've done my best to 
test this code, but I'm only one person.  If you find strange behavior,
please let me know.

=head1 COPYRIGHT

sendpage is free software; it can be used under the terms of the GNU
General Public License.

=head1 SEE ALSO

perl(1), kill(1), Device::SerialPort(3), Mail::Send(3),
Sendpage::KeesConf(3), Sendpage::KeesLog(3),
Sendpage::Modem(3), Sendpage::PagingCentral(3), Sendpage::PageQueue(3),
Sendpage::Page(3), Sendpage::Recipient(3), Sendpage::Queue(3)

=cut

# we need at least this version.  FIXME: I forgot why, though.  :P
require 5.005;

# Global variables;
$VERSION="0.9.14"; # our version!
my $config;	# holds the configuration object
undef $log;	# holds logging object

# Module-global variables
my %CHILDREN;	# who the childrens are
my $SHUTDOWN;	# when to shutdown
my $RELOAD;	# when we're reloading
my $DEBUG;	# for debugging
my $DEBUG_SELECT;# for debugging select loop
my $DEBUG_SNPP;	# for debugging SNPP issues
my %opts;	# holds the command line args hash

# Global SNPP server variables
undef $server;	# holds the SNPP server obj
undef $s;	# holds our Select set

# Global queue run variables
my $PC;		# holds name of PC for queue runners
my $sleeptime;  # holds sleeptime for next queue delay

# FIXME: load modules in a nice error-correcting fashion (borrow from mr house)
use POSIX;
use Getopt::Std;
use Sendpage::Modem;
use Sendpage::KeesConf;
use Sendpage::PagingCentral;
use Sendpage::PageQueue;
use Sendpage::Page;
use Sendpage::Recipient;
use Sendpage::KeesLog;
use Sendpage::SNPPServer;
use Socket;
use IO::Select;
use IO::Pipe;
use Sys::Hostname;

sub Usage {
	die "Usage: $0 [OPTIONS] [alias ...]
version $VERSION

General Options
	-h		you're reading it.  :)
	-d		turn debug on
	-C FILE		use FILE as the sendpage.cf file
	-t		test all configured modems

Daemon Options
	-bd		run in daemon mode
	-bp		display the queues
	-bv		verify addresses
	-bs		shutdown server
	-br		have server reload configurations
	-bq		query state of daemons
	-q		force a queue run
	-qR PC		force a queue run only for the PC paging central

Page Queuing Options
	-f USER         force page to be from USER (default is current user)
	-m MESSAGE      message to send (reads from stdin by default)
	-n              no email carboning to 'from' for this page

";
}

# Start logging immediately
$log=Sendpage::KeesLog->new(Syslog => 0);

# get our options
if (!getopts('htvdqC:b:R:f:m:n:',\%opts) || $opts{h}) {
	Usage();
}

# build default configuration, with any command line info
$config=initConfig(\%opts);

# load configuration
Initialize();

# Restart logging
$log->reconfig(Syslog => $config->get("syslog"),
		Opts  => $config->get("syslog-opt"),
		Facility => $config->get("syslog-facility"));

# Mode of operation selection
#
#	Modes:
#		- daemon (spawn queue runners, listen for pages)
#		- queue display
#		- address expansion
#		- force a queue run (optionally for only a certain PC)
#
if ($opts{t}) {
	DaemonInit();
	exit(0);
}
if ($opts{b}) {
	QueryDaemons(1) if ($opts{b} eq "q");
	SendHUP() if ($opts{b} eq "r");
	ShutdownEverything() if ($opts{b} eq "s");
	BecomeDaemon() if ($opts{b} eq "d");
	DisplayQueue() if ($opts{b} eq "p");
	VerifyAddress(@ARGV) if ($opts{b} eq "v");

	warn "Unknown run mode: '$opts{b}'\n";
	Usage();
}
if ($opts{q}) {
	my $ret=SendSignal('USR1',$opts{R} ? $opts{R} : "");

	# FIXME: should run the queue by hand if no one else can
	die "Failed to notify queue manager: $!\nMaybe you should run me with -bd?\n" if (!defined($ret) || $ret != 0);
	exit;
}


die "Must be root to write pages directly to queue.  Please use 'snpp' instead.\n"
	if (!VerifyIdentities());
DropPrivs();

if (!@ARGV) {
	Usage();
}
else {
	my $msg=$opts{m};

	# it's time to write a file directly to the queue

	# who is it from?
	if (!defined($opts{f}) && !$opts{n}) {
		$opts{f}=scalar(getpwuid($<))."\@";
		$opts{f}.=hostname();
	}

	if (!defined($msg)) {
		my $line;
		while ($line=<STDIN>) {
			$msg.=$line;
		}
	}

	# generate errors to the stderr
	ArrayDig(@ARGV);

	# turn on syslog for queue logging
	$log->on();

	# try to write the pages
	exit Sendpage::SNPPServer->write_queued_pages(undef,$opts{f},$msg,
			$config,$log,$DEBUG,@ARGV);
}

sub QueryDaemons {
	my($display)=@_;
	my(@check,$pc,$pid,$state,$running,$disabled);

	undef $running;
	@check=@pcs;
	unshift(@check,"");
	foreach $pc (@check) {
		if ($pc ne "" && $config->get("pc:$pc\@enabled")==0) {
			$disabled=1;
		}
		else {
			$disabled=0;
		}
		$pid=PidOf($pc,1);
		if ($pid==0) {
			$state="Not running";
		}
		else {
			undef $!;
			kill 0, $pid;
			if ($! == ESRCH) {
				$state="Stale: not running";
			}
			else {
				$state="Running";
				$running=1;
			}
		}
		printf("%-6d %20s : %s%s\n",$pid,
			($pc eq "") ? 'Queue Manager' : $pc, $state,
			$disabled ? " (disabled)" : "")
				if ($display);
	}

	exit(0) if ($display);

	return $running;
}

sub SendHUP {
	my $ret=SendSignal('HUP',"");
	die "Failed to notify queue manager: $!\n" if (!defined($ret) || $ret != 0);
	exit;
}

sub ShutdownEverything {
	# there's no need for individual killing is there?
	#my $ret=SendSignal('QUIT',$opts{R} ? $opts{R} : "");
	my $ret=SendSignal('QUIT',"");
	warn "Failed to notify queue manager: $!\n" if (!defined($ret) || $ret != 0);
	exit;
}

sub Initialize {
	&loadConfig();
	$DEBUG=$config->get("debug");
	$DEBUG_SELECT=$config->get("debug-select");
	$DEBUG_SNPP=$config->get("debug-snpp");
	@pcs=$config->instances("pc");
	undef %pcs; grep($pcs{$_}=1,@pcs);
	@modems=$config->instances("modem");
}

# plops down a pid file
sub RecordPidFile {
	my($name,$pid)=@_;
	my($file);	

	$name=".$name" if ($name ne "");
	$file=$config->get("pidfileprefix").$name.".pid";
	open(FILE,">$file") || $log->do('err',"Cannot write to '$file': $!");
	print FILE $pid,"\n";
	close(FILE);
}

# deletes a pid file by name
sub YankPidFile {
	my($name)=@_;
	my($file);

	$name=".$name" if ($name ne "");
	$file=$config->get("pidfileprefix").$name.".pid";
	unlink($file) || $log->do('err',"Cannot unlink '$file': $!");
}

# sends a signal to the specified PID
# returns non-0 on failure
sub SendSignal {
	my($sig,$pid)=@_;

	if ($pid !~ /^\d+$/) {
		$pid=PidOf($pid);
	}

	return undef if ($pid == 0);

	$log->do('debug',"signalling '$sig' to pid '$pid'") if ($DEBUG);
	undef $!;
	kill $sig, $pid;
	return $!+0;
}

# tries to find the PID of a certain sendpage
# return PID, or 0 or failure
sub PidOf {
	my($name,$quiet)=@_;
	my($file,$pid);

	if ($name ne "") {
		if (!defined($pcs{$name})) {
			$log->do('warning',"No such PC '$name'");
			return 0;
		}
		$name=".$name";
	}

	$pid=0;
	$file=$config->get("pidfileprefix").$name.".pid";
	if (-f $file) {
		my $line;

		open(FILE,"<$file") || $log->do('err',"Cannot read '$file': $!");
		chomp($line=<FILE>);

		# this is used to untaint for a sendpage -q
		if ($line=~/^(\d+)$/) {
			$pid=$1;
		}
		close(FILE);
	}
	else {
		my $warning=sprintf("No pid file found for sendpage%s!",
			$name);
		$log->do('warning',$warning) if (!defined($quiet));
	}
	return ($pid+0);
}
	
sub NiceShutdown {
	$SIG{QUIT}=$SIG{INT}=DEFAULT;

	$SHUTDOWN=1;
	if ($PC eq "") {
		my($pc,$cnxn);

		foreach $pc (@pcs) {
			if ($config->get("pc:$pc\@enabled")==0) {
				next;
			}
			$log->do('debug',"Signalling '$pc' ...") if ($DEBUG);
			SendSignal('QUIT',$pc);
		}
		foreach $cnxn (keys %PIPES) {
			kill 'QUIT', $cnxn;
		}
	}
	else {
		$log->do('debug',"Shutting down nicely: '$PC'") if ($DEBUG);
	}
}

sub ImmediateShutdown {
	$SIG{TERM}=DEFAULT;
	$log->do('debug',"Shutting down immediately") if ($DEBUG);
	exit(0);
}

sub QueueRun {
	#$log->do('debug',"Pid $$ heard signal USR1") if ($DEBUG);

	# we need to signal all the PC's if we're master
	if ($PC eq "") {
		my $pc;
		foreach $pc (@pcs) {
			if ($config->get("pc:$pc\@enabled")==0) {
				next;
			}
			$log->do('debug',"Signalling '$pc' ...") if ($DEBUG);
			SendSignal('USR1',$pc);
		}
	}
	else {
		# perform queue run
		$log->do('debug',"QueueRun requested for '$PC' ...")
			if ($DEBUG);

		# if we get a request for this DURING a queue run, we
		#  should immediately rescan our queue.  To do this,
		#  we set our next sleeptime to 0
		$sleeptime=0;
	}
}

sub DisplayQueue {
	my($queue, $waiting, $page, $recip);

	foreach $pc (@pcs) {
		$queue=Sendpage::PageQueue->new($config,$config->get("queuedir")."/$pc");

		if (($waiting=$queue->ready())>-1) {
			print "\nin the '$pc' queue: ".($waiting+1)."\n";
			while (defined($page=$queue->getPage())) {
				print "\tqueue filename: ".$queue->file()."\n";
				print "\tdeliverable:    ".$page->deliverable()."\n";
				print "\tattempts:       ".$page->attempts()."\n";
				print "\tfrom:           ".$page->option('from')."\n"
					if ($page->option('from') ne "");
				for ($page->reset(), $page->next();
				     defined($recip=$page->recip());
                                     $page->next()) {
					print "\tdest: '".$recip->name()."' (pin '".$recip->pin()."', email '".$recip->datum('email-cc')."')\n";
				}
				$queue->fileDone();
				print "\n";
			}
		}
	}
	exit(0);
}

sub VerifyAddress {
	my ($fail,@recips);
	($fail,@recips)=ArrayDig(@ARGV);
	if ($fail != 0) {
		exit(1);
	}
	foreach $recip (@recips) {
		print "deliverable: ".$recip->name()." as ".$recip->pin().
			" via ".$recip->pc()." (email is '".$recip->datum('email-cc')."')\n";
	}
	exit(0);
}

sub DaemonInit {
	my $modref;

	# test modems, keeping functioning ones in a list for the PCs to pick
	if ($DEBUG) {
		grep($log->do('debug',"found listing for modem: $_"),@modems);
		grep($log->do('debug',"found listing for pc: $_"),@pcs);
	}

	# should be limit which modems we're using?
	if (defined($modref=$config->get("modems",1))) {
		# we should limit the modem list
		@modems=@{ $modref };
		grep($log->do('debug',"using specified modem: $_"),@modems)
			if ($DEBUG);
	}
	else {
		# pull from instance list
		@modems=$config->instances("modem");
	}
	# check the modems
	@modems=verifyModems(@modems);
	grep($log->do('debug',"found functioning modem: $_"),@modems)
		if ($DEBUG);

	$log->do('alert',"no functioning modems!") if (!defined($modems[0]));
}

sub BlockSignals {
	# define the signals to block
	my $sigset = POSIX::SigSet->new(SIGINT,  SIGTERM, SIGQUIT, 
					SIGUSR1, SIGHUP, SIGPIPE);

	# start blocking signals
	unless (defined sigprocmask(SIG_BLOCK, $sigset)) {
		$log->do('alert',"Could not block signals!");
	}

	return $sigset;
}

sub UnblockSignals {
	# define the signals to block
	my $sigset = POSIX::SigSet->new(SIGINT,  SIGTERM, SIGQUIT, 
					SIGUSR1, SIGHUP, SIGPIPE);

	# stop blocking signals
	unless (defined sigprocmask(SIG_UNBLOCK, $sigset)) {
		$log->do('alert',"Could not unblock signals!");
	}
}

# start one or all the children
sub SpawnChildren {
	my($which)=@_;

	my($pid,$pc,@which,$sigset);

	if (defined($which)) {
		undef @which;
		push(@which,$which);
	}
	else {
		@which=@pcs;
		undef %CHILDREN;
		undef %STARTED;
		$log->do('info',"starting Queue Manager (sendpage v$VERSION)");
	}

	# build new loop selector
	$s=IO::Select->new();

	# there was a race condition here between the fork
	# 	 and the call to "SignalInit" where a child could
	#	 think it was still the manager, and receive yet
	#	 another signal, and act on it.   Now we block signals.

	# spawn PCs
	foreach $pc (@which) {
		if ($config->get("pc:$pc\@enabled")==0) {
			next;
		}

		# start blocking signals
		$sigset=&BlockSignals();

		$pid=fork();
		if ($pid<0) {
			# failure
			$log->do('emerg',"Cripes!  Cannot spawn process: $!");
		}
		elsif ($pid>0) {
			# parent
			$log->do('debug',"spawned child: $pid for PC '$pc'")
				if ($DEBUG);
			$CHILDREN{$pid}=$pc;
			$STARTED{$pc}=time;
			RecordPidFile($pc,$pid);
		}
		else {
			# child
			
			# set up
			&SignalInit();
			&DropPrivs(1);

			# stop blocking signals
			&UnblockSignals($sigset);

			&StartQueue($pc);	
			$log->do('crit',"PC '$pc' died!  Whoops, that can't ".
				"happen!");
			exit(1);
		}

		# stop blocking signals
		&UnblockSignals($sigset);
	}
}

# should they get serial port rights?
sub DropPrivs {
	my ($serial)=@_;

	my $grouplist="nobody";

	if (!defined($setUID)) {
		$log->do('crit',"Effective User ID unknown -- aborting!");
		exit(1);
	}
	if (defined($serial)) {
		if (!defined($lockGID)) {
			$log->do('crit',"Effective Group ID for locking unknown -- aborting!");
			exit(1);
		}
		if (!defined($ttyGID)) {
			$log->do('crit',"Effective Group ID for tty read/write unknown -- aborting!");
			exit(1);
		}
		$grouplist="$lockGID $ttyGID";
	}

	$(=$)=$grouplist;
	if ($( != $grouplist) {
		$log->do('crit',"Could not setgid to '$grouplist': $! -- aborting!");
	}

	$<=$>=$setUID;
	if ($< != $setUID) {
		$log->do('crit',"Could not setuid: $! -- aborting!");
	}
}

sub Respawn {
        my($undef,$forwarded) = @_;
	my($pid,$pc,$now);

	if ($forwarded) {
		$pid=$forwarded;
	}
	else {
		$pid=wait;
	}

	# quit out if we're done (wait will sleep)
	return if ($SHUTDOWN==1);

	if ($pid==-1) {
		if ($!==ECHILD) {
			$log->do('warning',"No children on SIGCHLD?!  Shutting down...");
			$SHUTDOWN=1;
			return;
		}
		else {
			$log->do('warning',"Oops: waitpid spat totally unexpected error: $!");
			return;
		}
	}	
	if (defined($CHILDREN{$pid})) {
		$log->do('debug', "pid $pid died: '".$CHILDREN{$pid}."'")
			if ($DEBUG);

		$pc=$CHILDREN{$pid};
		$now=time;

		# restart within the same 10 seconds??
		if ($now<($STARTED{$pc}+10)) {
			$log->do('alert',"Ugly nasty problem with $CHILDREN{$pid} queue manager!");
			$log->do('alert',"The same PC has died twice rather quickly.  Shutting it down.");
		}
		else {
			$log->do('err',"Whoa!  The '$CHILDREN{$pid}' PC died unexpectedly -- restarting it.");
			SpawnChildren($CHILDREN{$pid});
		}
	}
	elsif (defined($PIPES{$pid})) {
		$log->do('debug',"SNPP connection (pid $pid) finished") if ($DEBUG_SNPP);
		$s->remove($PIPES{$pid}); # don't select on this handle anymore
		if ($DEBUG_SELECT) {
			$log->do('debug',"(select removing pipe ".fileno($PIPES{$pid}).")");
		}
		PipeShutdownPipe($PIPES{$pid});
		PipeShutdownPid($pid);
	}
	else {
		# defunct children?  no!  bastards!  :)
		$log->do('warning',"Bastard child detected!  Unknown PID '$pid' was reaped.");
	}
}

sub SignalInit {
	# set up signal handlers for children
	#$SIG{'CHLD'}='IGNORE';
	$SIG{'HUP'}='IGNORE';
	$SIG{'USR1'}='IGNORE';
	$SIG{'PIPE'} = 'IGNORE';
	$SIG{'INT'}=$SIG{'QUIT'}=\&NiceShutdown;
	$SIG{'TERM'}=\&ImmediateShutdown;
}

sub VerifyIdentities {
        my($user,$group,$name);

        # check for our setuid user
        $user=$config->get('user');
        ($name,undef,$setUID)=getpwnam($user);
        if (!defined($name)) {
                $log->do('crit',"There is no such user named '$user'!  Aborting...");
		return undef;
        }

        # check for our locking group
        $group=$config->get('group-lock');
        ($name,undef,$lockGID)=getgrnam($group);
        if (!defined($name)) {
                $log->do('crit',"There is no such group named '$group'!  Aborting...");
		return undef;
        }

        # check for our tty r/w group
        $group=$config->get('group-tty');
        ($name,undef,$ttyGID)=getgrnam($group);
        if (!defined($name)) {
                $log->do('crit',"There is no such group named '$group'!  Aborting...");
		return undef;
        }

        # are we root?
        if (0 != $<) {
		return undef;
        }

	return 1;
}

sub BecomeDaemon {
	# check to see if we're already running
	if (QueryDaemons()) {
		warn "Already running:\n";
		QueryDaemons(1);
	}

	if (!VerifyIdentities()) {
                $log->do('crit',"Must be running as 'root' to daemonize!  Aborting...");
                exit(1);
	}

	# daemon mode starts here
	DaemonInit();

	SignalInit();

	$PC="";

	# close file handles.  (unless debugging)
	close(STDIN);
	close(STDOUT);
	close(STDERR) unless ($DEBUG || $DEBUG_SELECT || $DEBUG_SNPP);

	# Become a daemon
	my $pid = fork;
	exit if $pid;
	die "Couldn't fork: $!" unless defined($pid);
	POSIX::setsid() or die "Can't start a new session: $!";

	# reconfig, and reopen syslog connection
	$log->reconfig(Syslog => $config->get("syslog"),
		Opts  => $config->get("syslog-opt"),
		Facility => $config->get("syslog-facility"));
	$log->on();

	$0="sendpage: accepting connections";
	$SHUTDOWN=0;
	RecordPidFile($PC,$$);

	# start the queue runners
	SpawnChildren();

	# listen for USR1 to send USR1s
	$SIG{'USR1'} = \&QueueRun;
	# listen for reload info
	$SIG{'HUP'}=\&Reload;
	# listen for children death
	#$SIG{'CHLD'}=\&Respawn;
	
	# handle the SNPP stuff now
	ListenForSNPP();

	# handle children
	#ListenForChildren();
	
	die "parent died: this should never have happened: $!\n";
}

sub initSNPP {
	my $host=$config->get("snpp-addr");
	my $port=$config->get("snpp-port");

	# need to use "create" so that the "accept"s don't call the constructor
	$server=Sendpage::SNPPServer->create(Addr => $host, Port => $port);

	if (defined($server)) {
		$log->do('debug',"SNPP listener running on %s:%d",
			$server->sockhost,
			$server->sockport) if ($DEBUG_SNPP);
	}
	else {
		$log->do('crit',"SNPP listener for '$host:$port' failed: $!");
	}

	# Expand our snpp ACLs so we don't have to during each connection
	@ACLs=();
	my $item;
        foreach $item (@{$config->get("snpp-acl")}) {
                my ($netmask,$way) = split(/:/,$item,2);
                $way=uc($way);
                my ($net_str,$mask_str) = split(/\//,$netmask,2);
                $log->do('debug',"ACL loaded: '$net_str'/'$mask_str' is '$way'")
                        if ($DEBUG_SNPP);

                my $net  = inet_aton($net_str);
                my $mask = inet_aton($mask_str);

		push(@ACLs,[ $net, $mask, $net_str, ($way eq "ALLOW") ]);
	}
}

sub StopSNPP {
	$log->do('debug',"SNPP listeners shutting down")
		if ($DEBUG_SNPP);
	$log->do('debug',"(select dropping ".fileno($server).")")
		if ($DEBUG_SELECT);
	$s->remove($server);
	undef $server;
}

sub StartSNPP {
	$log->do('info',"starting SNPP listeners");
	initSNPP();
	if (defined($server)) {
		$s->add($server);
		$log->do('debug',"(select adding server ".fileno($server).")")
			if ($DEBUG_SELECT);
	}
}

sub RestartSNPP {
	StopSNPP();
	StartSNPP();
}

# FIXME: have all the ACLs pre-expanded for us...
sub IPAllowed {
	my $sock=shift;

        # Verify that this connection is allowed

	my $peer=$sock->peerhost();

        my $other_end = $sock->peername();
        if (!defined($other_end)) {
                $log->do('alert',"SNPP client '$peer' failed getpeername!");
                return undef;
        }
        my ($port, $iaddr) = unpack_sockaddr_in($other_end);
        my $other_ip_address = inet_ntoa($iaddr);

        # Compare this IP address to our ACL list
        my $item;
        my $allowed=0;
	my $found=0;
        foreach $item (@ACLs) {
                my ($net,$mask,$net_str,$allow) = @{$item};

                # Drop the peer IP through the mask
                my $net_check = ($iaddr & $mask);

                my $check_str=inet_ntoa($net_check);

                if ($DEBUG_SNPP) {
			my $mask_str=inet_ntoa($mask);
	                $log->do('debug',"ip: '$peer' mask: '$mask_str' ".
				"masked: '$check_str' net: '$net_str'");
		}

                # if result is our network, we have a hit
                if ($check_str eq $net_str) {
			$found=1;
                        $log->do('debug', "Matched ACL") if ($DEBUG_SNPP);
                        if ($allow==1) {
                                $allowed=1;
                        }
                        # if not allow, then reject
                        last;
                }
        }
	if ($DEBUG_SNPP && $found == 0) {
                $log->do('debug',"No ACL matched '$peer'");
	}
        if ($allowed != 1) {
		$sock->command("421 Connection denied");
                $log->do('info',"SNPP client '$peer' rejected");
                return undef;
        }
	return 1;
}

sub ListenForSNPP {
	my($fh,$read,$exc,$pipe,$sigset,$match,$pid);

	StartSNPP();

	while ($SHUTDOWN!=1) {
		if (!defined($server)) {
			$log->do('crit',"Cannot start any SNPP listeners -- ".
				"aborting!");
			NiceShutdown();
			YankPidFile("");
			exit(1);
		}

		# reset my containers
		$read=$exc=undef;

		if ($DEBUG_SELECT) {
			grep($log->do('debug',"select set: ".fileno($_)),
				$s->handles());
		}

		$match=0;
		$!=0;
		$log->do('debug',"(select starting)") if ($DEBUG_SELECT);

		# handle children dying
                while (($pid = waitpid(-1,&WNOHANG))>0) {
			&Respawn('',$pid);
		}


		($read,undef,$exc)=IO::Select->select($s,undef,$s,1.0);

		if ($! != 0) {
			if ($! == &EINTR()) {
				$match=1;
				$log->do('debug',"select loop: $! -- continuing")
					if ($DEBUG_SELECT);
			}
			else {
				$log->do('warning',"select loop failed: $!");
			}
		}
		$log->do('debug',"(select finished)") if ($DEBUG_SELECT);

		foreach $fh (@$read) {
			if ($fh == $server) {
				my $pid;
				my $sock = $fh->accept;

				$match=1;
	
				if (!defined($sock)) {
					$log->do('err',"SNPP accept: $!");
					next;
				}

				$log->do('debug',"got connection from ".
					$sock->peerhost) if ($DEBUG_SNPP);

				if (!IPAllowed($sock)) {
					close($sock);
					next;
				}

				$pipe = new IO::Pipe;

				$sigset=&BlockSignals();
			
				if(($pid = fork())>0) {
					# Parent

					# close other side of pipe
					$pipe->reader();
					# close our forked socket
					close($sock);

					$s->add($pipe);
					$log->do('debug',"(select adding pipe ".fileno($pipe).")")
						if ($DEBUG_SELECT);
					PipeRemember($pipe,$pid);
	
					# pause for a bit (no DoSing)
					#select(undef,undef,undef,0.1);
				}
				elsif($pid==0) {
					# Child

					# close other side of pipe
					$pipe->writer();
					$pipe->autoflush(1);

					# close master socket
					close $fh;
					# FIXME: close ALL snpp listeners
		
					# set up identity
					$PC="SNPP client";
					$0="sendpage: SNPP client: ".
						$sock->peerhost;
					&SignalInit();
					&DropPrivs();

					# we will unblock signals in
					#  the snpp handler
					$sock->HandleSNPP(
						"SNPP Sendpage $VERSION",
						$pipe, $config, $log,
						$DEBUG_SNPP, $sigset);
					$log->do('debug',
						"leaving SNPP client cnxn")
						if ($DEBUG_SNPP);
	
					exit(0);
						
				}
				else {
					$log->do('err',"SNPP fork: $!");
					# error on fork
					close($sock);
				}

				&UnblockSignals($sigset);
			}
			elsif (defined($pid=$CNXNS{$fh})) {
				$match=1;

				# is this pipe shutdown?
				if ($fh->eof()) {
					PipeShutdownPipe($fh);
					$s->remove($fh);
					$log->do('debug',"(select removing pipe ".fileno($fh).")")
						if ($DEBUG_SELECT);
					close($fh);
				}
				# something is readable from a pipe
				else {
					chomp(my $pc=<$fh>);
			
					SendSignal('USR1',$pc) if ($pc ne "");
				}
			}
			else {
				$match=1;

				# toss any straggling pipes
				$log->do('debug',"Removing stale selectable file handle: ".fileno($fh))
					if ($DEBUG_SELECT);
				$s->remove($fh);
				close($fh);
			}
		}
		foreach $fh (@$exc) {
			if ($fh == $server) {
				$match=1;

				$log->do('err',"Whoa!  Server socket took a hit!  -- reopening it");
				RestartSNPP();
			}
			else {
				$match=1;

				$log->do('warning',"SNPP connection took a hit!");
				$s->remove($fh);
				$log->do('debug',"(select removing server ".fileno($fh).")")
					if ($DEBUG_SELECT);
				close($fh);
			}
		}

#		if ($match==0) {
#			$log->do('warning',"Select produced an unmatched file descriptor?!");
#		}
	}
	$log->do('info',"stopping Queue Manager and SNPP listeners (sendpage v$VERSION)");
	StopSNPP();
	YankPidFile("");
	exit(0);
}

sub PipeShutdownPipe {
	my $pipe = shift;

	delete $CNXNS{$pipe};
}

sub PipeShutdownPid {
	my $pid = shift;

	delete $PIPES{$pid};
}

sub PipeRemember {
	my($pipe,$pid)=@_;

	$PIPES{$pid}=$pipe;
	$CNXNS{$pipe}=$pid;
}


sub StartQueue {
	my($name)=@_;

	# Queue-runner variables
	my($rundelay,$pc,$waiting);
	$rundelay=$config->get("pc:${name}\@rundelay");
	#warn "run delay: $rundelay\n";

	$PC=$name;
	$pc=Sendpage::PagingCentral->new($config,$PC,\@modems);

	# rename myself
	$0="sendpage: $PC queue";

	# set up handler
	$SIG{'USR1'} = \&QueueRun;

	$log->do('debug', "starting queue runs for '$PC'") if ($DEBUG);

	my $dir=$config->get("queuedir")."/$PC";
	if (! -d $dir) {
		if (!mkdir($dir,0700)) {
			$log->do('alert',"Cannot mkdir '$dir': $!");
			exit(1);
		}
	}

	$queue=Sendpage::PageQueue->new($config, $dir);

	if (!defined($queue)) {
		$log->do('alert',"Failed to open queue for '$PC'  --  exiting");
		exit(1);
	}

	while ($SHUTDOWN!=1) {
		# reset our sleep time
		$sleeptime=$rundelay;

		# search queue, gathering pages
		$waiting=$queue->ready()+1;
		if ($waiting>0) {
			while (defined($page=$queue->getPage())) {
				if ($page->deliverable()) {
					$pc->deliver($page);

					if ($page->has_recips()) {
						$log->do('debug',"$PC: rewriting page to queue") if ($DEBUG);
						# something requires rerun
						$queue->writePage($page);
						$queue->fileDone();
					}
					else {
						$log->do('debug',"$PC: tossing queue file") if ($DEBUG);
						$queue->fileToss();
					}
				}
				else {
					$queue->fileDone();
				}
			}
		}

		# don't hang up if need to rescan our queue
		if ($sleeptime != 0) {
			$pc->disconnect();


			# strange eval needed to wake up on USR1 signals
			eval {
				local $SIG{USR1} = sub { die "usr1\n" };

				# pause for the next queue run
				sleep($sleeptime);
			};
			if ($@) {
				QueueRun();
			}
			else {
				# nothing: we're done sleeping
			}
		}

		# check and see if we should shutdown (parent is init)
		if (!$SHUTDOWN && getppid==1) {
			$log->do('err',"Parent process died -- aborting");
			$SHUTDOWN=1;
		}
	}
	$log->do('debug', "Queue runner for '$PC' shutting down") if ($DEBUG);
	# remove our pid file
	YankPidFile($PC);
	exit(0);
}

sub DoNothing {
	# no code here, but just have HAVING a signal handler, I'll wake up
	# during a SIGCHLD for my waitpid
}

sub Reload {
	my($pid);

	$log->do('info', "initiating reload ...");

	if ($PC eq "") {
		my %OLD=%CHILDREN;
		undef %CHILDREN;
		undef %STARTED;

		StopSNPP();

		$RELOAD=1;

		my $finished=0;

		# shutdown the PCs
		foreach $pc (@pcs) {
			if ($config->get("pc:$pc\@enabled")==0) {
				next;
			}
			$log->do('debug', "Signalling '$pc' ...") if ($DEBUG);
			SendSignal('QUIT',$pc);
		}
		# shut down all the SNPP clients too
		foreach $pc (keys %PIPES) {
			$log->do('debug',"Signalling SNPP client '$pc' ...")
				if ($DEBUG);
			SendSignal('QUIT',$pc);
			$finished++;
		}

		# Note:
		# can't do "DaemonInit" until everyone is dead because we
		# need to re-init (to validate) all the modems.

		my @keys=keys %OLD;
		# how many PC children do we need to kill?
		$finished+=$#keys+1;

		undef $!;
		$log->do('debug', "Waiting for PCs to die off...") if ($DEBUG);
		while ($finished!=0) {
			$pid=wait;
			if ($pid==-1) {
				if ($!==ECHILD) {
					$log->do('warning',"Ran out of children too early?!  Continuing anyway...");
					$finished=0;
				}
				else {
					$log->do('warning',"Oops: waitpid spat totally unexpected error: $!");
				}
			}	
			elsif ($pid==0) {
				$log->do('warning',"Got 0 pid somehow");
			}
			elsif (defined($OLD{$pid})) {
				$log->do('debug', "Letting old PC '$OLD{$pid}' rest in peace") if ($DEBUG);
				$finished--;
			}
			elsif (defined($PIPES{$pid})) {
				PipeShutdownPid($pid);
				$log->do('debug',"SNPP connection (pid $pid) finished") if ($DEBUG || $DEBUG_SNPP);
				$finished--;
			}
			else {
				$log->do('warning',"Strange, I got an unknown child PID: '$pid'");
			}
		}

		$log->do('debug', "Reinitializing...") if ($DEBUG);

		$RELOAD=0;

		# reload our configurations
		Initialize();
		DaemonInit();

		# Restart logging
		$log->reconfig(Syslog => $config->get("syslog"),
			Opts  => $config->get("syslog-opt"),
			Facility => $config->get("syslog-facility"));
		$log->on();

		# Start up all our children
		SpawnChildren();
		StartSNPP();
	}
	else {
		$log->do('warning',"Weird: PC '$PC' caught a Reload signal somehow.");
	}
}

sub verifyModems {
	my (@totest) = @_;

	# find all our valid modems, keeping those that are either in use (e.g.
	# we have been HUPd) or respond to initialization
	my $m;
	my $result;
	my $modem;
	my @okay;

	foreach $modem (@totest) {
		$m=Sendpage::Modem->new(Name => $modem,
			Dev => $config->get("modem:${modem}\@dev"),
			Lockprefix => $config->get("lockprefix"),
			Debug => $config->get("modem:${modem}\@debug"),
			Log => $log,
			Baud => $config->get("modem:${modem}\@baud"),
			Parity => $config->get("modem:${modem}\@parity"),
			Data => $config->get("modem:${modem}\@data"),
			Stop => $config->get("modem:${modem}\@stop"),
			Flow => $config->get("modem:${modem}\@flow"),
			Init => $config->get("modem:${modem}\@init"),
			InitOK => $config->get("modem:${modem}\@initok"),
			InitWait => $config->get("modem:${modem}\@initwait"),
			InitRetry => $config->get("modem:${modem}\@initretries"),
			Error => $config->get("modem:${modem}\@error"),
			Dial => $config->get("modem:${modem}\@dial"),
			DialOK => $config->get("modem:${modem}\@dialok"),
			DialWait => $config->get("modem:${modem}\@dialwait"),
			DialRetry => $config->get("modem:${modem}\@dialretries"),
			NoCarrier => $config->get("modem:${modem}\@no-carrier"),
			DTRToggleTime => $config->get("modem:${modem}\@dtrtime"),
			CarrierDetect => $config->get("modem:${modem}\@carrier-detect",1),
			AreaCode => $config->get("modem:${modem}\@areacode",1),
			LongDist => $config->get("modem:${modem}\@longdist"),
			DialOut =>  $config->get("modem:${modem}\@dialout")
			);
		if (!defined($m)) {
			$log->do('warning',"Cannot find modem '$modem'");
			next;
		}
		if (!defined($result=$m->init())) {
			$log->do('alert',"Cannot initialize modem '$modem'");
			next;
		}
		undef $m;
		push(@okay,$modem);
	}

	return @okay;
}

sub RecipDig {
	my($recip,$seen)=@_;
	my($dests,$one,%hash,$result);

	if (!defined($seen)) {
		my %holder;
		$holder{$recip->name()}=1;
		$seen=\%holder;
	}
	else {
		$seen->{$recip->name()}++;
	}

	if ($seen->{$recip->name()}>1) {
		$log->do('alert',"Loop found in alias expansion!  Culprit recip: '".
			$recip->name()."'");
		exit(1);
	}

	# no alias, just return this one (leaf node)
	return ($recip) if (!$recip->alias());

	$log->do('debug',"from: '%s'",$recip->name())
		if ($config->get("alias-debug"));

	# get expanded list
	$dests=$recip->dests();

	# dump list
	grep($log->do('debug',"starting with: '$_'"),@{$dests})
		if ($config->get("alias-debug"));

	# expand each one
	foreach $one (@{ $dests }) {
		$log->do('debug',"expanding: '$one'") if ($config->get("alias-debug"));
		my %copy=%{$seen};
		my $r=Sendpage::Recipient->new($config,$one,$recip->data());
		if (!defined($r)) {
			$log->do('err',"undeliverable: '$one'");
		}
		else {
			my @results=RecipDig($r,\%copy);

			# add them to our hash
			foreach $result (@results) {
				$log->do('debug',"got: '%s'",$result->name())
					if ($config->get("alias-debug"));
				$hash{$result->name()}=$result;
			}	
		}
	}
		
	undef @results;
	foreach $one (keys %hash) {
		$log->do('debug',"passing back: '%s'",$hash{$one}->name())
			if ($config->get("alias-debug"));
		push(@results,$hash{$one});
	}

	return @results;
}

sub ArrayDig {
	my(@array)=@_;
	my ($one,$result,@results,$fail);

	# did a look-up fail?
	$fail=0;
	# dump list
	grep($log->do('debug',"starting with: '$_'"),@array)
		if ($config->get("alias-debug"));

	# expand each one
	foreach $one (@array) {
		$log->do('debug',"expanding: '$one'")
			if ($config->get("alias-debug"));
		my $recip=Sendpage::Recipient->new($config,$one);
		if (!defined($recip)) {
			$log->do('err',"undeliverable: '$one'");
			$fail=1;
		}
		else {
			my @results=RecipDig($recip);

			# add them to our hash
			foreach $result (@results) {
				$log->do('debug',"got: '%s'",$result->name())
					if ($config->get("alias-debug"));
				$hash{$result->name()}=$result;
			}	
		}
	}
		
	undef @results;
	foreach $one (keys %hash) {
		$log->do('debug',"passing back: '%s'",$hash{$one}->name())
			if ($config->get("alias-debug"));
		push(@results,$hash{$one});
	}

	return ($fail,@results);
}

sub initConfig {
	my(%opts) = %{ $_[0] };

	# set up default values  (this is ignored by KeesConf...)
	my %cfg=(
		PEDANTIC=> 1,
		CASE	=> 1,
		CREATE  => 1,
		GLOBAL  => {
			DEFAULT   => "<unset>",
			ARGCOUNT  => ARGCOUNT_ONE,
			},
		);
	my $config = Sendpage::KeesConf->new(\%cfg);

# global variables
$config->define("cfgfile",   { DEFAULT => "/etc/sendpage.cf" });
$config->define("pidfileprefix",{ DEFAULT => "/var/spool/sendpage/sendpage" });
$config->define("lockprefix",{ DEFAULT => "/var/lock/LCK.." });
$config->define("queuedir", { DEFAULT => "/var/spool/sendpage" });
$config->define("mail-agent", { DEFAULT => "sendmail" });
$config->define("user", { DEFAULT => "sendpage" });
$config->define("group-lock", { DEFAULT => "uucp" });
$config->define("group-tty", { DEFAULT => "tty" });
$config->define("page-daemon", { DEFAULT => "sendpage" });
$config->define("cc-on-error", { ARGCOUNT => 0, DEFAULT => 1 });
$config->define("modems",   { ARGCOUNT => 2 });
$config->define("alias-debug",    { ARGCOUNT => 0, DEFAULT => 0 });
$config->define("debug",    { ARGCOUNT => 0,
				DEFAULT => $opts{d} ? 1 : 0 });
$config->define("debug-select",    { ARGCOUNT => 0,
				DEFAULT => $opts{d} ? 1 : 0 });
$config->define("debug-snpp",    { ARGCOUNT => 0,
				DEFAULT => $opts{d} ? 1 : 0 });
# should the sender be notified of failures?
$config->define("fail-notify", { ARGCOUNT => 0, DEFAULT => 1 });
# sender should be notified how on every X temp fails? (0=never)
$config->define("tempfail-notify-after", { DEFAULT => 5 });
# how many temp fails does it take to produce a perm failure?
$config->define("max-tempfail", { DEFAULT => 20 });
# default email CC domain
$config->define("fallback-email-domain", { DEFAULT => undef });
# command to run after each successful or failed page
$config->define("completion-cmd", { UNSET => 1 });
# syslog toggle: strerr is used if not syslog
$config->define("syslog", { DEFAULT => 1, ARGCOUNT => 0 });
# syslog logopt words (any of "pid", "ndelay", "cons", "nowait")
$config->define("syslog-opt", { DEFAULT => "pid" });
# syslog facility to log with (one of "auth", "authpriv", "cron", "daemon",
#	"kern", "local0" through "local7", "lpr", "mail", "news", "syslog",
#	"user", or "uucp"
$config->define("syslog-facility", { DEFAULT => "local6" });
# SNPP settings
$config->define("snpp-port", { DEFAULT => "444" });
$config->define("snpp-addr", { DEFAULT => "localhost" });
$config->define("snpp-acl", { ARGCOUNT => 2, DEFAULT => [ "127.0.0.1/255.255.255.255:ALLOW" ] });

# aliases defaults
#$config->define("recip:", { ARGCOUNT => 2 });
# where to send email-cc's of pages.  none if blank, defaults to 
#	ALIAS @ fallback-email-domain if unset
$config->define("recip:email-cc", { UNSET => 1 });
# page is PIN@PC, alias is just the recip name again
$config->define("recip:dest", { ARGCOUNT => 2 });

# modem defaults
$config->define("modem:debug",  { ARGCOUNT => 0, 
					DEFAULT => $opts{d} ? 1 : 0 });
$config->define("modem:baud",   { DEFAULT => 9600 });
$config->define("modem:data",   { DEFAULT => 7 });
$config->define("modem:parity", { DEFAULT => "even" });
$config->define("modem:stop",   { DEFAULT => 1 });
$config->define("modem:flow",   { DEFAULT => "rts" });
$config->define("modem:dev",    { DEFAULT => "/dev/null" });
$config->define("modem:carrier-detect", { DEFAULT => "on" });
# time to force DTR down during reset (0 = don't do it at all)
$config->define("modem:dtrtime", { DEFAULT => 0.5 });
$config->define("modem:init",   { DEFAULT => "ATZ" });
$config->define("modem:initok", { DEFAULT => "OK" });
$config->define("modem:initwait",{ DEFAULT => 4 });
$config->define("modem:initretries",{ DEFAULT => 2 });
$config->define("modem:dial",   { DEFAULT => "ATDT" });
$config->define("modem:dialok", { DEFAULT => "CONNECT.*\r" });
$config->define("modem:dialwait",{ DEFAULT => 60 });
$config->define("modem:error", { DEFAULT => "ERROR" });
$config->define("modem:no-carrier",
	{ DEFAULT => "ERROR|NO CARRIER|BUSY|NO DIAL|VOICE" });
$config->define("modem:areacode", { UNSET => 1 });
$config->define("modem:longdist", { DEFAULT => 1 });
$config->define("modem:dialout", { DEFAULT => "" });

# FIXME:
# currently not implemented -- perhaps never, better to stall and try again
$config->define("modem:dialretries",{ DEFAULT => 3 });

# paging central defaults
#     Paging centrals can override baud, data, parity, stop, flow, dialwait,
#       dialretries
#  modem connect info
$config->define("pc:enabled",{ ARGCOUNT => 0, DEFAULT => 1 });
$config->define("pc:debug",  { ARGCOUNT => 0, 
					DEFAULT => $opts{d} ? 1 : 0 });
$config->define("pc:page-daemon", { UNSET => 1 });
$config->define("pc:cc-on-error",  { ARGCOUNT => 0, UNSET => 1 });
$config->define("pc:fail-notify", { ARGCOUNT => 0, UNSET => 1 });
$config->define("pc:tempfail-notify-after", { UNSET => 1 });
$config->define("pc:max-tempfail", { UNSET => 1 });
# command to run after each successful or failed page
$config->define("pc:completion-cmd", { UNSET => 1 });
$config->define("pc:modems", { ARGCOUNT => 2 });
$config->define("pc:baud",   { DEFAULT => 9600 });
$config->define("pc:data",   { DEFAULT => 7 });
$config->define("pc:parity", { DEFAULT => "even" });
$config->define("pc:stop",   { DEFAULT => 1 });
$config->define("pc:flow",   { DEFAULT => "rts" });
$config->define("pc:phonenum",{DEFAULT => "" });
$config->define("pc:areacode",{ UNSET => 1 });
# how many chars per page before auto-splitting?
$config->define("pc:maxchars",{DEFAULT => 1024 });
# how many page splits allowed per page?
$config->define("pc:maxsplits",{DEFAULT => 6 });
# PC uses it's own dialwait for delaying
$config->define("pc:dialwait",{ UNSET => 1 });
$config->define("pc:rundelay",{DEFAULT => 30 });
$config->define("pc:dialretries",{ DEFAULT => 3 });
# allow for redefining the proto version
$config->define("pc:proto",{DEFAULT => "PG1" });
# allow for forced multiple fields in BlockTrans
$config->define("pc:fields",{DEFAULT => 2 });
$config->define("pc:password",{DEFAULT => "000000" });
#  proto establishment info
$config->define("pc:answerwait", { DEFAULT => 2 });
$config->define("pc:answerretries", { DEFAULT => 3 });
#  protocol settings
#   MUST have the leading "<CR>" for each answer?
$config->define("pc:stricttap",		{ DEFAULT => 0, ARGCOUNT => 0 });
#   chars less than 0x20 are allowed in a field
$config->define("pc:ctrl",		{ DEFAULT => 0, ARGCOUNT => 0 });
#   chars CAN be escaped (if false, "LF" is allowed, it seems?)
$config->define("pc:esc",  		{ DEFAULT => 0, ARGCOUNT => 0 });
#   is LF allowed (some PCs allow it, but no other ctrl chars)
$config->define("pc:lfok",		{ DEFAULT => 0, ARGCOUNT => 0 });
#   fields cannot be split across blocks? (FIXME: unimplemented)
$config->define("pc:fieldsplits",	{ DEFAULT => 1, ARGCOUNT => 0 });
#  paging central limits
#   max blocks per connection (0 = unlimited)
$config->define("pc:maxblocks",		{ DEFAULT => 0 });
#   max pages per connection (0 = unlimited)
$config->define("pc:maxpages",		{ DEFAULT => 0 });
#   max chars per block: 250 is protocol standard: 256 - 3 ctrl - 3 chksum
$config->define("pc:chars-per-block",	{ DEFAULT => 250 });

	return $config;
}

sub loadConfig {
	my($cfgfile);

	$cfgfile=$config->get("cfgfile");
	$cfgfile=$opts{C} if (defined($opts{C}));

	# toss our config
	$config->dump();


	# yes, this seems silly, but we allow cmdline options to change
	# various defaults, including this one
	$config->file($cfgfile);
}


#
# file locking example from the Perl Cookbook
#
# use Fcntl qw(:DEFAULT :flock);
# 
# sysopen(FH, "numfile", O_RDWR|O_CREAT)
#                                     or die "can't open numfile: $!";
# flock(FH, LOCK_EX)                  or die "can't write-lock numfile: $!";
# # Now we have acquired the lock, it's safe for I/O
# $num = <FH> || 0;                   # DO NOT USE "or" THERE!!
# seek(FH, 0, 0)                      or die "can't rewind numfile : $!";
# truncate(FH, 0)                     or die "can't truncate numfile: $!";
# print FH $num+1, "\n"               or die "can't write numfile: $!";
# close(FH)                           or die "can't close numfile: $!";
#
#
