File:  [Local Repository] / telnetbbs / telnetbbs.pl
Revision 1.11: download - view: text, annotated - select for diffs
Mon Dec 20 20:51:39 2010 UTC (13 years, 4 months ago) by nick
Branches: MAIN
CVS tags: r_0_5, HEAD
Set defaults in configuration files.
Fixed bug that off-set the base_port value by 1.
Touched up some documentation, still need to write the installation section.

#!/usr/bin/perl -wT
################################################################################
##
## See end of script for comments and 'pod2man telnetbbs | nroff -man' to
## view man page or pod2text telnetbbs for plain text.
##
##   Nicholas DeClario <nick@declario.com>
##   October 2009
##	$Id: telnetbbs.pl,v 1.11 2010/12/20 20:51:39 nick Exp $
##
################################################################################
BEGIN {
        delete @ENV{qw(IFS CDPATH ENV BASH_ENV PATH)};
        $ENV{PATH} = "/bin:/usr/bin";
        $|++;
#        $SIG{__DIE__} = sub { require Carp; Carp::confess(@_); }
      }

use strict;
use Getopt::Long;
use Pod::Usage;
use Data::Dumper;
use POSIX qw/ mkfifo /;
use IO::File;
use Socket;
use IO::Socket::INET;
use Time::HiRes qw/ sleep setitimer ITIMER_REAL time /;
use threads;
use threads::shared;

##
## Fetch our command line and configuration options
##
my %opts    = &fetchOptions( );
my %cfg     = &fetchConfig( );
my $EOL     = "\015\012";

## 
## These are read in from the config file
##
my $BBS_NODE  = 0;
my $pidFile   = $cfg{'pidfile'}    || "/tmp/telnetbbs.pid";
my $port      = $opts{'port'}      || $cfg{'port'} || 23;
my $DISPLAY   = $cfg{'display'}    || ":0.0";
my $BBS_NAME  = $cfg{'bbs_name'}   || "Hell's Dominion BBS";
my $DBCONF    = $cfg{'dosbox_cfg'} || "/tmp/dosbox-__NODE__.conf";
my $BBS_CMD   = $cfg{'bbs_cmd'}    || "DISPLAY=__DISPLAY__ /usr/bin/dosbox -conf ";
my $LOGGING   = $cfg{'logging'}    || 0;
my $LOG       = $cfg{'log_path'}   || "/tmp/bbs.log";
my $MAX_NODE  = $cfg{'nodes'}      || 1;
my $DOSBOXT   = $cfg{'dosboxt'}    || "dosbox.conf.template";
my $BASE_PORT = $cfg{'base_port'}  || 7000;
my $LOCK_PATH = $cfg{'lock_path'}  || "/tmp";
   $BBS_CMD   =~ s/__DISPLAY__/$DISPLAY/g;

##
## Check that we are 'root' 
##
die( "Must be root user to run this.\n" )
	if ( getpwuid( $> ) ne "root" && $port < 1023 );

##
## Check for a PID
##
exit( 255 ) if ( ! &checkPID( $pidFile ) );

##
## Lets keep an eye on the forked children our network socket will be using
##
$SIG{CHLD} = 'IGNORE';

##
## Catch any type of kill signals so that we may cleanly shutdown.  These
## include 'kill', 'kill -HUP' and a 'CTRL-C' from the keyboard.
##
local $SIG{HUP}  = $SIG{INT} = $SIG{TERM} = \&shutdown;

##
## Open the Log
##
open LOG, ">>$LOG" if ( $LOGGING );
&logmsg( "Starting telnetbbs server" );

##
## Display running information
##
&display_config_and_options( \%opts, "Options" );
&display_config_and_options( \%cfg, "Configuration" );

##
## Start the network server
##
my $netThread = threads->create( \&startNetServer( ) );


while( 1 ) { sleep 1; }

##
## If we made it here, the main loop died, lets shutdown
##
&shutdown( );

###############################################################################
###############################################################################
##
## Sub-routines begin here
##
###############################################################################


###############################################################################
##
## &logmsg( "string" );
##
##  This takes a string and prepends the process name, ID and timestamp
##  to the message.  It then displays it to STDOUT and logs it if enabled.
##
###############################################################################
sub logmsg 
{ 
	my $message = "$0 $$ " . scalar( localtime( ) ) . ":@_\n";
	print STDOUT $message;
	print LOG $message if ( $LOGGING );
}


###############################################################################
##
## &display_config_and_options( %hash );
##
##  This will display via Data::Dumper a hash that is passed to it.
##  If verbose is enabled it will got to STDOUT and if logging is enabled
##  it will be logged.
##
##  This is called only once during startup.
##
###############################################################################
sub display_config_and_options
{
	my $hr    = shift || 0;
	my $name  = shift || "Unknown";
	my $title = "Displaying $name\n";

	return $hr if ( ! $hr );

	print LOG $title . Dumper( $hr ) if ( $LOGGING );
	print STDOUT $title . Dumper( $hr ) if ( $opts{'verbose'});
}

###############################################################################
## startNetServer( );
##
##  We want to open the next free port when an incoming connection to the
##  main port is made.  We need to set that up here.
##
###############################################################################
sub startNetServer 
{
	my $hostConnection;
	my $childPID;
	my @nodes = ( );

	my $server = IO::Socket::INET->new( 
			LocalPort => $port,
			Type	  => SOCK_STREAM,
			Proto	  => 'tcp',
			Reuse	  => 1,
			Listen	  => 1,
		) or die "Couldn't create socket on port $port: $!\n";
	
	&logmsg( "Listening on port $port" );

        ##
        ## We want to fork our connection when it's made so that we are ready
        ## to accept other incoming connections.
        ##
	REQUEST: while( $hostConnection = $server->accept( ) )
	{
		## 
		## Find the next available node
		##
		my $node = 0;
		my $lock_file = "";
		my $cnt  = 0;
		while ( ! $node && $cnt < $MAX_NODE )
		{
			$cnt++;
			$lock_file = $LOCK_PATH . "/" . $BBS_NAME . 
				     "_node" . $cnt . ".lock";
print "Checking for node lock: $lock_file\n";
			next if ( -f $lock_file );

			##
			## Create node lock file
			##
			open LOCK, ">$lock_file";
			close( LOCK );
			$BBS_NODE = $node = $cnt;
print "Using node: $node\n";
		}

		##
		## Create our dosbox config
		##
		open( DBT, "<$DOSBOXT" );
			my @dbt = <DBT>;
		close( DBT );
		
		my $bpn = $BASE_PORT + $BBS_NODE - 1;
		$DBCONF =~ s/__NODE__/$BBS_NODE/g;
		open( DBC, ">$DBCONF" );
		foreach( @dbt ) 
		{
			$_ =~ s/__NODE__/$BBS_NODE/g;
			$_ =~ s/__LISTEN_PORT__/$bpn/g;
			print DBC $_;
		}
		close( DBC );

		&logmsg( "Connecting on node $BBS_NODE\n" );

		my $kidpid;
		my $line;

		if( $childPID = fork( ) ) {
			close( $hostConnection );
			next REQUEST;
		}
		defined( $childPID ) || die( "Cannot fork: $!\n" );

		select $hostConnection;

		##
		## Default file descriptor to the client and turn on autoflush
		##
		$hostConnection->autoflush( 1 );

		print "Welcome to $BBS_NAME!" . $EOL;

		##
		if ( ! $node ) 
		{
			print "No available nodes.  Try again later.".$EOL;
			exit;
		}

		print "Starting BBS on node $BBS_NODE...$EOL";

		##
		## Launch BBS via dosbox
		##
		my $bbsPID = fork( );
		if ( $bbsPID ) 
		{
			select STDOUT;
			my $cmd = $BBS_CMD . $DBCONF;
			system( $cmd );
			print "Shutting down node $BBS_NODE\n";

			##
			## Remove Lock
			##	
			unlink( $lock_file );
			unlink( $DBCONF );
			close( $hostConnection );
			close( $server );
			kill( "TERM" => $bbsPID );
			exit;
		}

		##
		## We wait for dosbox to start and the BBS to start
		## There really should be a better way to determine this
		##
		sleep 5;

		##
		## Create connection to BBS
		##
		my $bbs = IO::Socket::INET->new (
				PeerAddr 	=> 'localhost',
				Type	 	=> SOCK_STREAM,
				PeerPort	=> $bpn,
				Proto		=> 'tcp',
			) || die "Could not open BBS socket: $!\n";
		$bbs->autoflush( 1 );
		die "Can't fork BBS connection: $!\n" 
			unless defined( $kidpid = fork( ) );

		if ( $kidpid ) 
		{
			my $byte;
			while ( sysread( $bbs, $byte, 1 ) == 1 ) 
			{
				print $hostConnection $byte;
			}
			$nodes[$BBS_NODE] = 0;
			kill( "TERM" => $kidpid );
		}
		else 
		{
			my $byte;
			while( sysread( $hostConnection, $byte, 1 ) == 1 )
			{
				print $bbs $byte;
			}
		}

		unlink( $DBCONF );
	}
	close( $hostConnection );
	close( $server );

	exit;
}

###############################################################################
##
## shutdown( $signame );
##
##  Call our shutdown routine which cleanly kills all the child processes 
##  and shuts down.  Optionally, if a kill signal was received, it will be
##  displayed.
##
###############################################################################
sub shutdown 
{
	my $signame = shift || 0;

	select STDOUT;

	&logmsg( "$0: Shutdown (SIG$signame) received.\n" )
		if( $signame );
	
	##
	## Close Log
	##
	close( LOG ) if ( $LOGGING );

	##	
	## Remove the PID
	##
	unlink( $pidFile );

	##
	## Remove node lock files
	##
	foreach (1 .. $MAX_NODE)
	{
		my $node_lock = $LOCK_PATH."/".$BBS_NAME."_node".$_.".lock";
		unlink( $node_lock ) if ( -f $node_lock );
	}

	##
	## Wait for the thread to shutdown
	##
#	$netThread->detach( );	

	##
	## And time to exit
	##
	&POSIX::_exit( 0 );
}

###############################################################################
##
## my $result = checkPID( $pidFile );
##
##   We need to see if there is a PID file, if so if the PID inside is still
## actually running, if not, re-create the PID file with the new PID.  
## If there is no PID file to begin with, we create one.
##
## If this process is successfull a non-zero value is returned.
##
###############################################################################
sub checkPID
{
	my $pidFile = shift || return 0;
	my $pid     = 0;

        ##
        ## If there is no PID file, create it.
        ##
        if ( ! stat( $pidFile ) ) 
	{
            open PF, ">$pidFile" || return 0;
                print PF $$;
            close( PF );

            return 1;
        }
        ##
        ## We have a PID file.  If the process does not actually exist, then
        ## delete the PID file.
        ##
        else 
	{
        	open PIDFILE, "<$pidFile" || 
			die( "Failed ot open PID file: $!\n" );
          		$pid = <PIDFILE>;
        	close( PIDFILE );

		##
		## Unlink the file if the process doesn't exist 
		## and continue with execution
		##
		if ( &processExists( $pid, $0 ) ) 
		{
			unlink( $pidFile );
			open PF, ">$pidFile" || return 0;
				print PF $$ . "\n";
			close( PF );

			return 2;
		}
            	return 0;
        }
}

###############################################################################
##
## sub processExists( );
##
##	Check the '/proc' file system for the process ID number listed in the
##  PID file.  '/proc/$PID/cmdline' will be compared to the running process
##  name.  If the process ID does exist but the name does not match we assume
##  it's a dead PID file.
##
###############################################################################
sub processExists 
{
	my $pid   = shift || return 0;
	my $pname = shift || return 0;

	##
	## If the directory doesn't exist, there is no way the 
	## process is running
	##
	return 0 if ( ! -f "/proc/$pid" );

	##
	## However, if the directory does exist, we need to confirm that it is
	## indeed the process we are looking.
	##
	open CMD, "</proc/$pid/cmdline";
		my $cmd = <CMD>;
	close( CMD );

	##
	## Filter out leading PATH information
	##
	$pname =~ s/^\.\///;
	$cmd   =~ s/.*\/(.*)/$1/;

	##
	## if we found the process, return 1
	##
	return 1 if ( $cmd =~ m/$pname/ );

	return 0;
}

###############################################################################
##
## %config_hash = &fetchConfig( );
##
##  This reads in a file in the format of "key = value" and stores them
## in to a hash of $hash{$key} = $value.  Lines starting with '#' are 
## considered comments and ignored.
##
###############################################################################
sub fetchConfig 
{
	my %conf = ( );
	my $cf   = &findConfig;

	if ( $cf ) 
	{
		open( CONF, "<$cf" ) or die ( "Error opening $cf: $!\n" );
			while( <CONF> ) 
			{
				next if ( $_ =~ m/^#/ );
				if ( $_ =~ m/(.*?)=(.*)/ )
				{
					my $k = $1; my $v = $2;
					$k =~ s/\s+//;
					$v =~ s/\s+//;
					$conf{$k} = $v;
				}
			}
		close( CONF );
	}

	return %conf;
}

###############################################################################
##
## my $file = &fetchConfig( );
##
##  This function will look for 'telnetbbs.conf' or whatever was specified
##  on the command line.  It will search the @paths below for the default
##  filename if none is specifed.
##
###############################################################################
sub findConfig
{
	my $cf    = 0;
	my @paths = qw| ./ ./.telnetbbs /etc /usr/local/etc |;

	return $opts{'config'} if defined $opts{'config'};

	foreach ( @paths ) 
	{
		my $fn = $_ . "/telnetbbs.conf";
		return $fn if ( -f $fn );
	}

	return $cf;
}

###############################################################################
##
## &fetchOptions( );
##
##      Grab our command line arguments and toss them in to a hash
##
###############################################################################
sub fetchOptions {
        my %opts;

        &GetOptions(
			"config:s"	=> \$opts{'config'},
                        "help|?"        => \$opts{'help'},
                        "man"           => \$opts{'man'},
			"port:i"	=> \$opts{'port'},
			"verbose"	=> \$opts{'verbose'},
                   ) || &pod2usage( );
        &pod2usage( ) if defined $opts{'help'};
        &pod2usage( { -verbose => 2, -input => \*DATA } ) if defined $opts{'man'};

        return %opts;
}

__END__

=head1 NAME

telnetbbs.pl - A telnet server designed to launch a multi-node BBS.

=head1 SYNOPSIS

telnetbbs.pl [options]

 Options:
	--config,c	Specify the configuration file to use
        --help,?        Display the basic help menu
        --man,m         Display the detailed man page
	--port,p	Port to listen on, default 23.
	--verbose,v	Enable verbose output

=head1 DESCRIPTION

=head1 HISTORY

=head1 AUTHOR

Nicholas DeClario <nick@declario.com>

=head1 BUGS

This is a work in progress.  Please report all bugs to the author.

=head1 SEE ALSO

=head1 COPYRIGHT

=cut

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>