Annotation of telnetbbs/telnetbbs.pl, revision 1.6

1.1       nick        1: #!/usr/bin/perl -wT
                      2: ################################################################################
                      3: ##
                      4: ## See end of script for comments and 'pod2man telnetbbs | nroff -man' to
                      5: ## view man page or pod2text telnetbbs for plain text.
                      6: ##
                      7: ##   Nicholas DeClario <nick@declario.com>
                      8: ##   October 2009
1.6     ! nick        9: ##     $Id: telnetbbs.pl,v 1.5 2010-12-13 05:54:50 nick Exp $
1.1       nick       10: ##
                     11: ################################################################################
                     12: BEGIN {
                     13:         delete @ENV{qw(IFS CDPATH ENV BASH_ENV PATH)};
                     14:         $ENV{PATH} = "/bin:/usr/bin";
                     15:         $|++;
1.6     ! nick       16:         $SIG{__DIE__} = sub { require Carp; Carp::confess(@_); }
1.1       nick       17:       }
                     18: 
                     19: use strict;
                     20: use Getopt::Long;
                     21: use Pod::Usage;
                     22: use Data::Dumper;
                     23: use POSIX qw/ mkfifo /;
                     24: use IO::File;
                     25: use Socket;
                     26: use IO::Socket::INET;
                     27: use Time::HiRes qw/ sleep setitimer ITIMER_REAL time /;
                     28: use threads;
                     29: use threads::shared;
                     30: 
                     31: ##
1.6     ! nick       32: ## Fetch our command line and configuration options
1.1       nick       33: ##
                     34: my %opts    = &fetchOptions( );
1.6     ! nick       35: my %cfg     = &fetchConfig( );
1.1       nick       36: my $EOL     = "\015\012";
1.3       nick       37: 
                     38: ## 
                     39: ## These will be moved in to a config file
                     40: ##
1.5       nick       41: my $BBS_NODE  = 0;
1.6     ! nick       42: my $pidFile   = $cfg{'pidfile'}    || "/tmp/telnetbbs.pid";
        !            43: my $port      = $opts{'port'}      || $cfg{'port'} || 23;
        !            44: my $DISPLAY   = $cfg{'display'}    || ":0.0";
        !            45: my $BBS_NAME  = $cfg{'bbs_name'}   || "Hell's Dominion BBS";
        !            46: my $DBCONF    = $cfg{'dosbox_cfg'} || "/tmp/dosbox-__NODE__.conf";
        !            47: my $BBS_CMD   = $cfg{'bbs_cmd'}    || "DISPLAY=$DISPLAY /usr/bin/dosbox -conf ";
        !            48: my $LOGGING   = $cfg{'logging'}    || 0;
        !            49: my $LOG       = $cfg{'log_path'}   || "/tmp/bbs.log";
        !            50: my $MAX_NODE  = $cfg{'nodes'}      || 1;
        !            51: my $DOSBOXT   = $cfg{'dosboxt'}    || "dosbox.conf.template";
        !            52: my $BASE_PORT = $cfg{'base_port'}  || 7000;
        !            53: my $LOCK_PATH = $cfg{'lock_path'}  || "/tmp";
1.1       nick       54: 
                     55: ##
                     56: ## Check that we are 'root' 
                     57: ##
                     58: die( "Must be root user to run this.\n" )
1.6     ! nick       59:        if ( getpwuid( $> ) ne "root" && $port < 1023 );
1.1       nick       60: 
                     61: ##
                     62: ## Check for a PID
                     63: ##
                     64: exit( 255 ) if ( ! &checkPID( $pidFile ) );
                     65: 
                     66: ##
                     67: ## Lets keep an eye on the forked children our network socket will be using
                     68: ##
                     69: $SIG{CHLD} = 'IGNORE';
                     70: 
                     71: ##
                     72: ## Catch any type of kill signals so that we may cleanly shutdown.  These
                     73: ## include 'kill', 'kill -HUP' and a 'CTRL-C' from the keyboard.
                     74: ##
                     75: local $SIG{HUP}  = $SIG{INT} = $SIG{TERM} = \&shutdown;
                     76: 
                     77: ##
1.3       nick       78: ## Open the Log
                     79: ##
1.6     ! nick       80: open LOG, ">>$LOG" if ( $LOGGING );
1.3       nick       81: &logmsg( "Starting telnetbbs server" );
                     82: 
                     83: ##
1.6     ! nick       84: ## Display running information
        !            85: ##
        !            86: &display_config_and_options( \%opts, "Options" );
        !            87: &display_config_and_options( \%cfg, "Configuration" );
        !            88: 
        !            89: ##
1.1       nick       90: ## Start the network server
                     91: ##
1.4       nick       92: my $netThread = threads->create( \&startNetServer( ) );
                     93: 
1.1       nick       94: 
                     95: while( 1 ) { sleep 1; }
                     96: 
                     97: ##
                     98: ## If we made it here, the main loop died, lets shutdown
                     99: ##
                    100: &shutdown( );
                    101: 
                    102: ###############################################################################
                    103: ###############################################################################
                    104: ##
                    105: ## Sub-routines begin here
                    106: ##
                    107: ###############################################################################
                    108: ###############################################################################
                    109: 
1.6     ! nick      110: sub logmsg 
        !           111: { 
        !           112:        my $message = "$0 $$ " . scalar( localtime( ) ) . ":@_\n";
        !           113:        print STDOUT $message;
        !           114:        print LOG $message if ( $LOGGING );
        !           115: }
        !           116: 
        !           117: 
        !           118: ###############################################################################
        !           119: ###############################################################################
        !           120: sub display_config_and_options
        !           121: {
        !           122:        my $hr    = shift || 0;
        !           123:        my $name  = shift || "Unknown";
        !           124:        my $title = "Displaying $name\n";
        !           125: 
        !           126:        return $hr if ( ! $hr );
        !           127: 
        !           128:        print LOG $title . Dumper( $hr ) if ( $LOGGING );
        !           129:        print STDOUT $title . Dumper( $hr ) if ( $opts{'verbose'});
        !           130: }
1.1       nick      131: 
                    132: ###############################################################################
                    133: ## startNetServer( );
                    134: ##
                    135: ##  We want to open the next free port when an incoming connection to the
                    136: ##  main port is made.  We need to set that up here.
                    137: ##
                    138: ###############################################################################
                    139: sub startNetServer 
                    140: {
                    141:        my $hostConnection;
                    142:        my $childPID;
1.4       nick      143:        my @nodes = ( );
1.1       nick      144: 
                    145:        my $server = IO::Socket::INET->new( 
                    146:                        LocalPort => $port,
                    147:                        Type      => SOCK_STREAM,
                    148:                        Proto     => 'tcp',
                    149:                        Reuse     => 1,
                    150:                        Listen    => 1,
                    151:                ) or die "Couldn't create socket on port $port: $!\n";
                    152:        
                    153:        &logmsg( "Listening on port $port" );
                    154: 
                    155:         ##
                    156:         ## We want to fork our connection when it's made so that we are ready
                    157:         ## to accept other incoming connections.
                    158:         ##
                    159:        REQUEST: while( $hostConnection = $server->accept( ) )
                    160:        {
1.3       nick      161:                ## 
                    162:                ## Find the next available node
                    163:                ##
                    164:                my $node = 0;
1.5       nick      165:                my $lock_file = "";
1.3       nick      166:                foreach (1 .. $MAX_NODE)
                    167:                {
1.5       nick      168: print "Searching for lock: " . $LOCK_PATH."/".$BBS_NAME."_node".$_.".lock\n";
                    169:                        next if ( -f $LOCK_PATH."/".$BBS_NAME."_node".$_.".lock" );
                    170: 
                    171:                        ##
                    172:                        ## Create node lock file
                    173:                        ##
                    174:                        $lock_file = $LOCK_PATH."/".$BBS_NAME."_node".$_.".lock";
                    175:                        open LOCK, ">$lock_file";
                    176:                        close( LOCK );
                    177:                        $node = $BBS_NODE = $_;
1.3       nick      178:                }
1.5       nick      179: print "Using lock: " . $LOCK_PATH."/".$BBS_NAME."_node".$node.".lock\n";
1.4       nick      180: 
1.3       nick      181:                ##
                    182:                ## Create our dosbox config
                    183:                ##
                    184:                open( DBT, "<$DOSBOXT" );
                    185:                        my @dbt = <DBT>;
                    186:                close( DBT );
                    187:                
                    188:                my $bpn = $BASE_PORT + $BBS_NODE;
                    189:                $DBCONF =~ s/__NODE__/$BBS_NODE/g;
                    190:                open( DBC, ">$DBCONF" );
                    191:                foreach( @dbt ) 
                    192:                {
                    193:                        $_ =~ s/__NODE__/$BBS_NODE/g;
                    194:                        $_ =~ s/__LISTEN_PORT__/$bpn/g;
                    195:                        print DBC $_;
                    196:                }
                    197:                close( DBC );
                    198: 
                    199:                &logmsg( "Connecting on node $BBS_NODE\n" );
                    200: 
1.1       nick      201:                my $kidpid;
                    202:                my $line;
                    203: 
                    204:                if( $childPID = fork( ) ) {
                    205:                        close( $hostConnection );
                    206:                        next REQUEST;
                    207:                }
                    208:                defined( $childPID ) || die( "Cannot fork: $!\n" );
                    209: 
1.5       nick      210:                select $hostConnection;
                    211: 
1.1       nick      212:                ##
                    213:                ## Default file descriptor to the client and turn on autoflush
                    214:                ##
                    215:                $hostConnection->autoflush( 1 );
                    216: 
1.5       nick      217:                print "Welcome to $BBS_NAME!" . $EOL;
1.3       nick      218: 
                    219:                ##
1.5       nick      220:                if ( ! $lock_file ) 
1.3       nick      221:                {
1.5       nick      222:                        print "No available nodes.  Try again later.".$EOL;
1.3       nick      223:                        exit;
                    224:                }
                    225: 
1.5       nick      226:                print "Starting BBS on node $BBS_NODE...$EOL";
1.1       nick      227: 
                    228:                ##
                    229:                ## Launch BBS via dosbox
                    230:                ##
                    231:                my $bbsPID = fork( );
                    232:                if ( $bbsPID ) 
                    233:                {
1.5       nick      234:                        select STDOUT;
1.3       nick      235:                        my $cmd = $BBS_CMD . $DBCONF;
1.5       nick      236:                        system( $cmd );
                    237:                        print "Shutting down node $BBS_NODE\n";
                    238:                        ##
                    239:                        ## Remove Lock
                    240:                        ##      
                    241:                        unlink( $lock_file );
                    242:                        unlink( $DBCONF );
1.1       nick      243:                        exit;
                    244:                }
1.3       nick      245: 
                    246:                ##
                    247:                ## We wait for dosbox to start and the BBS to start
                    248:                ## There really should be a better way to determine this
                    249:                ##
                    250:                sleep 5;
1.1       nick      251: 
                    252:                ##
                    253:                ## Create connection to BBS
                    254:                ##
                    255:                my $bbs = IO::Socket::INET->new (
                    256:                                PeerAddr        => 'localhost',
1.3       nick      257:                                Type            => SOCK_STREAM,
                    258:                                PeerPort        => $bpn,
1.1       nick      259:                                Proto           => 'tcp',
                    260:                        ) || die "Could not open BBS socket: $!\n";
                    261:                $bbs->autoflush( 1 );
                    262:                die "Can't fork BBS connection: $!\n" 
                    263:                        unless defined( $kidpid = fork( ) );
                    264: 
                    265:                if ( $kidpid ) 
                    266:                {
                    267:                        my $byte;
                    268:                        while ( sysread( $bbs, $byte, 1 ) == 1 ) 
                    269:                        {
                    270:                                print $hostConnection $byte;
                    271:                        }
1.4       nick      272:                        $nodes[$BBS_NODE] = 0;
                    273:                        kill( "TERM" => $kidpid );
1.1       nick      274:                }
                    275:                else 
                    276:                {
                    277:                        my $byte;
                    278:                        while( sysread( $hostConnection, $byte, 1 ) == 1 )
                    279:                        {
                    280:                                print $bbs $byte;
                    281:                        }
                    282:                }
1.3       nick      283: 
                    284:                unlink( $DBCONF );
1.1       nick      285:        }
                    286:        close( $hostConnection );
                    287:        exit;
                    288: 
1.3       nick      289: #      close( $server );
1.1       nick      290: }
                    291: 
                    292: ###############################################################################
                    293: ##
                    294: ## shutdown( $signame );
                    295: ##
                    296: ##  Call our shutdown routine which cleanly kills all the child processes 
                    297: ##  and shuts down.  Optionally, if a kill signal was received, it will be
                    298: ##  displayed.
                    299: ##
                    300: ###############################################################################
                    301: sub shutdown 
                    302: {
                    303:        my $signame = shift || 0;
                    304: 
1.5       nick      305:        select STDOUT;
                    306: 
1.3       nick      307:        &logmsg( "$0: Shutdown (SIG$signame) received.\n" )
1.1       nick      308:                if( $signame );
                    309:        
1.3       nick      310:        ##
                    311:        ## Close Log
                    312:        ##
1.6     ! nick      313:        close( LOG ) if ( $LOGGING );
1.3       nick      314: 
1.1       nick      315:        ##      
                    316:        ## Remove the PID
                    317:        ##
                    318:        unlink( $pidFile );
                    319: 
                    320:        ##
                    321:        ## Wait for the thread to shutdown
                    322:        ##
                    323: #      $netThread->detach( );  
                    324: 
                    325:        ##
                    326:        ## And time to exit
                    327:        ##
                    328:        &POSIX::_exit( 0 );
                    329: }
                    330: 
                    331: ###############################################################################
                    332: ##
                    333: ## my $result = checkPID( $pidFile );
                    334: ##
                    335: ##   We need to see if there is a PID file, if so if the PID inside is still
                    336: ## actually running, if not, re-create the PID file with the new PID.  
                    337: ## If there is no PID file to begin with, we create one.
                    338: ##
                    339: ## If this process is successfull a non-zero value is returned.
                    340: ##
                    341: ###############################################################################
                    342: sub checkPID
                    343: {
                    344:        my $pidFile = shift || return 0;
                    345:        my $pid     = 0;
                    346: 
                    347:         ##
                    348:         ## If there is no PID file, create it.
                    349:         ##
                    350:         if ( ! stat( $pidFile ) ) 
                    351:        {
                    352:             open PF, ">$pidFile" || return 0;
                    353:                 print PF $$;
                    354:             close( PF );
                    355: 
                    356:             return 1;
                    357:         }
                    358:         ##
                    359:         ## We have a PID file.  If the process does not actually exist, then
                    360:         ## delete the PID file.
                    361:         ##
                    362:         else 
                    363:        {
                    364:                open PIDFILE, "<$pidFile" || 
                    365:                        die( "Failed ot open PID file: $!\n" );
                    366:                        $pid = <PIDFILE>;
                    367:                close( PIDFILE );
                    368: 
                    369:                ##
                    370:                ## Unlink the file if the process doesn't exist 
                    371:                ## and continue with execution
                    372:                ##
                    373:                if ( &processExists( $pid, $0 ) ) 
                    374:                {
                    375:                        unlink( $pidFile );
                    376:                        open PF, ">$pidFile" || return 0;
                    377:                                print PF $$ . "\n";
                    378:                        close( PF );
                    379: 
                    380:                        return 2;
                    381:                }
                    382:                return 0;
                    383:         }
                    384: }
                    385: 
                    386: ###############################################################################
                    387: ##
                    388: ## sub processExists( );
                    389: ##
                    390: ##     Check the '/proc' file system for the process ID number listed in the
                    391: ##  PID file.  '/proc/$PID/cmdline' will be compared to the running process
                    392: ##  name.  If the process ID does exist but the name does not match we assume
                    393: ##  it's a dead PID file.
                    394: ##
                    395: ###############################################################################
                    396: sub processExists 
                    397: {
                    398:        my $pid   = shift || return 0;
                    399:        my $pname = shift || return 0;
                    400: 
                    401:        ##
                    402:        ## If the directory doesn't exist, there is no way the 
                    403:        ## process is running
                    404:        ##
                    405:        return 0 if ( ! -f "/proc/$pid" );
                    406: 
                    407:        ##
                    408:        ## However, if the directory does exist, we need to confirm that it is
                    409:        ## indeed the process we are looking.
                    410:        ##
                    411:        open CMD, "</proc/$pid/cmdline";
                    412:                my $cmd = <CMD>;
                    413:        close( CMD );
                    414: 
                    415:        ##
                    416:        ## Filter out leading PATH information
                    417:        ##
                    418:        $pname =~ s/^\.\///;
                    419:        $cmd   =~ s/.*\/(.*)/$1/;
                    420: 
                    421:        ##
                    422:        ## if we found the process, return 1
                    423:        ##
                    424:        return 1 if ( $cmd =~ m/$pname/ );
                    425: 
                    426:        return 0;
                    427: }
                    428: 
1.6     ! nick      429: ###############################################################################
        !           430: ###############################################################################
        !           431: sub fetchConfig 
        !           432: {
        !           433:        my %conf = ( );
        !           434:        my $cf   = &findConfig;
        !           435: 
        !           436:        if ( $cf ) 
        !           437:        {
        !           438:                open( CONF, "<$cf" ) or die ( "Error opening $cf: $!\n" );
        !           439:                        while( <CONF> ) 
        !           440:                        {
        !           441:                                next if ( $_ =~ m/^#/ );
        !           442:                                if ( $_ =~ m/(.*?)=(.*)/ )
        !           443:                                {
        !           444:                                        my $k = $1; my $v = $2;
        !           445:                                        $k =~ s/\s+//;
        !           446:                                        $v =~ s/\s+//;
        !           447:                                        $conf{$k} = $v;
        !           448:                                }
        !           449:                        }
        !           450:                close( CONF );
        !           451:        }
        !           452: 
        !           453:        return %conf;
        !           454: }
        !           455: 
        !           456: ###############################################################################
        !           457: ###############################################################################
        !           458: sub findConfig
        !           459: {
        !           460:        my $cf    = 0;
        !           461:        my @paths = qw| ./ ./.telnetbbs /etc /usr/local/etc |;
        !           462: 
        !           463:        foreach ( @paths ) 
        !           464:        {
        !           465:                my $fn = $_ . "/telnetbbs.conf";
        !           466:                return $fn if ( -f $fn );
        !           467:        }
        !           468: 
        !           469:        return $cf;
        !           470: }
1.1       nick      471: 
                    472: ###############################################################################
                    473: ##
                    474: ## &fetchOptions( );
                    475: ##
                    476: ##      Grab our command line arguments and toss them in to a hash
                    477: ##
                    478: ###############################################################################
                    479: sub fetchOptions {
                    480:         my %opts;
                    481: 
                    482:         &GetOptions(
                    483:                         "help|?"        => \$opts{'help'},
                    484:                         "man"           => \$opts{'man'},
1.2       nick      485:                        "port:i"        => \$opts{'port'},
1.6     ! nick      486:                        "verbose"       => \$opts{'verbose'},
1.1       nick      487:                    ) || &pod2usage( );
                    488:         &pod2usage( ) if defined $opts{'help'};
                    489:         &pod2usage( { -verbose => 2, -input => \*DATA } ) if defined $opts{'man'};
                    490: 
                    491:         return %opts;
                    492: }
                    493: 
                    494: __END__
                    495: 
                    496: =head1 NAME
                    497: 
1.2       nick      498: telnetbbs.pl - A telnet server designed to launch a multi-node BBS.
1.1       nick      499: 
                    500: =head1 SYNOPSIS
                    501: 
1.2       nick      502: telnetbbs.pl [options]
1.1       nick      503: 
                    504:  Options:
                    505:         --help,?        Display the basic help menu
                    506:         --man,m         Display the detailed man page
1.2       nick      507:        --port,p        Port to listen on, default 23.
1.1       nick      508: 
                    509: =head1 DESCRIPTION
                    510: 
                    511: =head1 HISTORY
                    512: 
                    513: =head1 AUTHOR
                    514: 
                    515: Nicholas DeClario <nick@declario.com>
                    516: 
                    517: =head1 BUGS
                    518: 
                    519: This is a work in progress.  Please report all bugs to the author.
                    520: 
                    521: =head1 SEE ALSO
                    522: 
                    523: =head1 COPYRIGHT
                    524: 
                    525: =cut

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