Annotation of telnetbbs/telnetbbs.pl, revision 1.11

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

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