#!/usr/bin/perl
#    Big Sister network monitor
#    Copyright (C) 1998  Thomas Aeby
#
#    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., 675 Mass Ave, Cambridge, MA 02139, USA.
#

#=============================================================================
#
$BigSister::common::Usage	  = "[-D level] [-p port] [-i interval] [-b bigsisterdirectory] [-c] [-f cfgfile]";
#
#=============================================================================
@BigSister::common::options = ( "l:s", "p:s", "i:i", "c", "f:s" );

use lib "."; use lib "uxmon"; use lib "platforms"; 		#inslib
use BigSister::common;
proginit();

use strict;
use bbdisp;
use access;
use Socket;
use Carp;
use FileHandle;
use RotatingLog;

use Statusmon::Statusmon;
use Statusmon::DisplayCFG;

my $maxclients = 16;
my $maxconntime = 60;
my $port = "1984";
my $display_interval = 5*60;
my $noupdate_max = 15*60;
my $keep_logs = 8*7*24*3600;
my $display_updatetime = time;

my %agents = ();

my $dl = $BigSister::common::dl;
$display_interval = $BigSister::common::opt_i if( $BigSister::common::opt_i );
my $web_only = $BigSister::common::opt_c;
my $cfgfile = $BigSister::common::opt_f;

my $displayconfig = new Statusmon::DisplayCFG;
$displayconfig->setarg( "file", $cfgfile ) if( $cfgfile );
$displayconfig->start();

if( $displayconfig->get_current_config()->{"port"} ) {
    $port = $displayconfig->get_current_config()->{"port"};
}
$port = $BigSister::common::opt_p if( $BigSister::common::opt_p );

my $statuslog = new RotatingLog( "$BigSister::common::fs{'var'}/status.log" );
$statuslog->set_rotate_hook( sub {
    my( $fh, $filename ) = @_;
    print $fh (time)." 127.0.0.1: logrotate\n";
} );


my $security_file = $BigSister::common::fs{'adm'}."/hosts.allow";
my $permissions_file = $BigSister::common::fs{'adm'}."/permissions";

$BigSister::common::on_unix = Platforms::isunix();


my $access_list = new access( $security_file, $permissions_file );
my $access_lastupdate = 0;
my %last_update = ();
my %expiries = ();


my $orig_port = $port;
if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') }
unless( $port ) {
    BigSister::common::log( "err", "unknown port: $orig_port" );
    exit 1;
}

my $proto = getprotobyname('tcp');
unless( $proto ) {
    print STDERR "unknown protocol 'tcp'\n";
    exit 1;
}

$port = $1 if $port =~ /(\d+)/; # untaint port number
&background unless( $BigSister::common::dl || $web_only );
Platform::bswait();

&do_listen( $port ) unless( $web_only );



sub do_listen {
    my( $port ) = @_;
    my $paddr;
    my $rbits;
    my $nfound;
    my @conns = ();
    my @args = ();
    my( $fh, $txt );
    my( $iaddr, $name, $conn, $now, $res, $i, $item, $time, $now, $scalar );

    socket(Server, PF_INET, SOCK_STREAM, $proto)        || die "socket: $!";
    setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))   || die "setsockopt: $!"; 
    bind(Server, sockaddr_in($port, INADDR_ANY))        || die "bind: $!";
    listen(Server,SOMAXCONN)                            || die "listen: $!";

    # this was not preview ... but is useful:
    $BigSister::common::open_connections = \@conns;
    while( 1 ) {
	Platform::bswait_nohang();
	$rbits = "";
	vec( $rbits, fileno(Server), 1 ) = 1;
	foreach $conn (@conns) {
	    $fh = $$conn[0];
	    vec( $rbits, fileno($fh), 1 ) = 1;
	}
	if( time-$access_lastupdate > 300 ) {
	    $access_list->check_files();
	    $access_lastupdate = time;
	}
	print "entering select mode\n" if( $dl > 1 );
	$nfound = select( $rbits, undef, $rbits, 10 );
	print "select returned $nfound\n" if( $dl > 2 );

	if( $nfound <= 0 ) {
	    # send live signals to statuslog
	    $statuslog->log();
	}
	if( time>$display_updatetime ) {
	    my( $host, $what );
	    $displayconfig->run();
	    my $autoconn = $displayconfig->get_current_config()->{"autoconn"};
	    $display_updatetime = time + $display_interval ;
	    write_agentlog();

	    print "updateing display\n" if( $dl );
	    $now = time;
	    while( ($item,$time) = each %last_update ) {
		my $expiry = $expiries{$item};
		if( ($expiry && $expiry<$now) || (($now-$time > $noupdate_max) && ! $expiry) ) {
		    ($host,$what) = BigSister::common::separate_service( $item );
		    if( ($what eq "conn") && grep( $$_[1] eq $host, @$autoconn ) ) { 
			my $text = "red ".($scalar=localtime)." no status report";
			$statuslog->log( "127.0.0.1: status $item $text" );
		    }
		    else {
			my $text = "purple (".time.") ".(scalar localtime)." no status report";
			$statuslog->log( "127.0.0.1: status $item $text" );
		    }
		}
	    }
	}
	return if( $nfound < 0 );
	next unless( $nfound );

	$now = time;

	$rbits = "";
	vec( $rbits, fileno(Server), 1 ) = 1;
	$nfound = select( $rbits, undef, undef, 0 );
	if( $nfound ) {
	    $fh = FileHandle->new;
	    $paddr = accept( $fh, Server );
	    ($port,$iaddr) = sockaddr_in($paddr);
	    &autoconn( $iaddr );
	    if( ($#conns>$maxclients) || ! &allow_connect( $iaddr ) ) {
		close $fh;
	    }
	    else {
		push( @conns, [ $fh, "", $now, inet_ntoa($iaddr), $BigSister::common::permissions, [ @BigSister::common::security_context ] ] );
		$args[$#conns] = {};
	    }
	}

	for( $i=0; $i<=$#conns; $i++ ) {
	    $conn = $conns[$i];
	    $fh = $$conn[0];
	    $rbits = "";
	    $BigSister::common::permissions = $$conn[4];
	    @BigSister::common::security_context = @{$$conn[5]};
	    vec( $rbits, fileno($fh), 1 ) = 1;
	    $nfound = select( $rbits, undef, $rbits, 0 );
	    $res = 1;
	    if( $nfound ) {
		$res = sysread( $fh, $$conn[1], 30000, length( $$conn[1] ) );
		while( $$conn[1] =~ /^(.*)\n/ ) {
		   $$conn[1] = $';
		   &parse_line( $1, $args[$i], $$conn[3], $fh ) || undef $res;
		}
	    }
	    if( (! defined $res) || ($res==0) || ($$conn[2] + $maxconntime < $now) || (length( $$conn[1] ) > 30000 ) ) {
		print "connection closed ...\n" if( $dl>2 );
		&parse_line( $$conn[1], $args[$i], $$conn[3] );
		&parse_line( "END ", $args[$i], $$conn[3] );
		close $$conn[0];
		splice( @conns, $i, 1 );
		splice( @args, $i, 1 );
		$i--;
	    }
	}
	$BigSister::common::permissions = 0;
    }
}




sub parse_line {
    my( $line, $args, $agent, $socket ) = @_;
    my( $cmd, $host, $item, $text ) = @_;
    my( $groups, $group, @groups );

    print "line: $line\n" if( $dl );
    $line =~ s/\r//g;
    if( $args->{"instatus"} ) {
	if( $line =~ /^(join|event|perf|leave|status|status\+\d+|page|remove|END|displayname|set|ack|savelogs|sendlogs)([\t\s].*|)$/s ) {
	    chomp( $args->{"text"} );
	    $args->{"text"} =~ s/\n/\|\>/g;
	    if( $args->{"cmd"} eq "page" ) {
		$statuslog->log( "$agent: page ".($args->{"host"}).".".($args->{"item"})." ".($args->{"text"}) );
		&page( $args->{"text"} );
	    }
	    else {
		my $expiry = "+11";
		if( $args->{"expires"} ) {
		    $expiry = "+".($args->{"expires"});
		}
		$statuslog->log( "$agent: status$expiry ".($args->{"host"}).".".($args->{"item"})." ".($args->{"text"}) );
		&inject( $args->{"host"}, $args->{"item"}, $args->{"text"} );
	    }
	    $args->{"instatus"} = 0;
	}
	else {
	    $args->{"text"} .= "\n".$line;
	    return 1;
	}
    }
    if( $line =~ /^(join|leave) ([0-9a-z\-\:_,\.]+)\s([0-9a-z-\:_\s\*\.]+)$/i ) {
	($cmd,$host,$groups) = ($1,$2,$3);
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "grouping" ) );
	$statuslog->log( $agent.": ".$line );
	if( $groups eq "*" ) {
	    my $match = qr{^\Q$host.\E[^\.]+$};
	    foreach my $i (grep(m{$match}, keys %last_update)) {
		(print "Deleting last_update for $i\n") if ($BigSister::common::dl>4);
		delete $last_update{$i};
	    }
	    $statuslog->log( $agent.": remove $host.*" );
	}
	return 1;
    }
    if( $line =~ /^(remove) ([0-9a-z\-\:_,\.]+)\.([0-9a-z\-\:_,\*]+)$/i ) {
	($cmd,$host,$item) = ($1,$2,$3);
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "remove" ) );
	$statuslog->log( $agent.": ".$line );
	$item = qr{\Q$3\E};
	$item =~ s/\*/.*/;
	my $match = qr{\Q$host\E\Q.\E$item};
	foreach my $i (grep(m{$match}, keys %last_update)) {
	    (print "Deleting last_update for $i\n") if ($BigSister::common::dl>4);
	    delete $last_update{$i};
	}
	return 1;
    }
    elsif( $line =~ /^event \d+ [^\s]+ [^\s]+ (.*)$/ ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "event" ) );
	$statuslog->log( $agent.": ".$line );
	return 1;
    }
    elsif( $line =~ /^perf \d+ [^\s]+ (.*)$/ ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "perf" ) );
	$statuslog->log( $agent.": ".$line );
	return 1;
    }
    elsif( $line =~ /^set / ) {
	return 1;
    }
    elsif( $line =~ /^ack ([0-9a-z\-\:_,\.]+)\.([0-9a-z\-]+|\*) ([0-9:]+|forever) (.*?) (ack|del|ign|maint)\s*(.*)$/i ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "alarm_acking" ) );
	$statuslog->log( $agent.": ".$line );
	return 1;
    }
    elsif( $line =~ /^displayname ([0-9a-z\-\:_,\.]+)\s(.*)$/i ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "grouping" ) );
	$statuslog->log( $agent.": ".$line );
	return 1;
    }
    elsif( $line =~ /^page / ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "page" ) );
	$statuslog->log( $agent.": ".$line );
	return 1;
    }
    elsif( $line =~ /^savelogs(|\s[a-zA-Z0-9]+)$/ ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "archiving" ) );
	$statuslog->log( $agent.": ".$line );
	my $tag = $1;
	$tag =~ s/^ //;
	savelogs($tag);
	return 1;
    }
    elsif( $line =~ /^sendlogs\s([a-zA-Z0-9]+)$/ ) {
	return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "archiving" ) );
	$statuslog->log( $agent.": ".$line );
	return( sendlogs($1,$socket) );
    }
    return( 0 ) unless( $line =~ /^(status|status\+\d+|page) ([0-9a-z\-\:_,\.]+)\.([0-9a-z\-]+) (.*)$/i );
    return(0) unless( $access_list->is_allowed( $BigSister::common::permissions, "status" ) );
    ($cmd, $host, $item, $text) = ($1,$2,$3,$4,$5);
    if( $cmd =~ /\+(\d+)/ ) {
	$cmd = $`;
	$args->{"expires"} = $1;
	$expiries{"$host.$item"} = time + 60*$1;
    }
    else {
	delete $expiries{"$host.$item"};
    }
    $host =~ s/,/$BigSister::common::domain_encode/g;
    $args->{"cmd"} = $cmd;
    $args->{"host"} = $host;
    $args->{"item"} = $item;
    $args->{"text"} = $text;
    $args->{"instatus"} = 1;
    log_agent( $host, $item, $agent );
#    &inject( $host, $item, $text ) if( $cmd eq "status" );
    return 1;
}



sub log_agent {
    my( $host, $item, $agent ) = @_;

    my $now = time;
    $item = $host.".".$item;
    unless( defined $agents{$item} ) {
	$agents{$item} = [ [ $agent, $now ] ];
	return;
    }
    my $known_agents = $agents{$item};
    my @matched = grep( $agent eq $_->[0], @$known_agents );
    if( @matched ) {
	$matched[0]->[1] = $now;
    }
    else {
	push( @$known_agents, [ $agent, $now ] );
	BigSister::common::log( "notice", $item." is now monitored by $agent" );
    }
    my $i;
    $now -= 8*60;
    for( $i = $#$known_agents; $i>=0 ; $i-- ) {
	if( $known_agents->[$i]->[1] < $now ) {
	    splice( @$known_agents, $i, 1 );
	}
    }
}



sub inject {
    my( $host, $item, $text ) = @_;

    $last_update{"$host.$item"} = time;
}



sub autoconn {
    my( $addr ) = @_;
    my( $ref, $host, @hosts );

    $ref = $displayconfig->get_current_config()->{"autoconn"};
    @hosts = grep( $$_[0] eq $addr, @$ref );
    return unless( @hosts );
    foreach $ref( @hosts ) {
	$host = $$ref[1];
	next if( time-$last_update{$host.".conn"} < 60 );
	$statuslog->log( "127.0.0.1: status $host.conn green (".(time).") ".(scalar localtime)." connected to status collector" );
	&inject( $host, "conn", "green (".time.") ".(localtime)." connected to status collector" );
    }
}



sub allow_connect {
    my( $iaddr ) = @_;
    my( $mtime, $check, $allow, $regcheck );

    my @aliases;
    if( $displayconfig->get_current_config()->{"options"}->{"DNS"} ) {;
	my($name,$aliases,$addrtype,$length,@addrs) = gethostbyaddr( $iaddr, AF_INET );
        @aliases = split( " ", $aliases );
	push( @aliases, $name );
    }
    push( @aliases, inet_ntoa( $iaddr )."." );
    @aliases = map { "name ".$_ } @aliases;
    push( @aliases, "ip ".inet_ntoa( $iaddr ), "anonymous" );
    print "bbd client connect: client context: ".join( " ",@aliases )."\n" if( $dl>5 );
    @BigSister::common::security_context = @aliases;
    $BigSister::common::permissions = $access_list->get_perms( @aliases );
    return( $BigSister::common::permissions );
}
	



sub write_agentlog {
    my( $item, $agents );

    open( OUT, ">$BigSister::common::fs{'var'}/agent.log.$$" );
    while( ($item,$agents) = each %agents ) {
	print OUT $item;
	my $agent;
	foreach $agent (@$agents) {
	    print OUT "\t".($agent->[0])." (".($agent->[1])." ".(localtime($agent->[1])).")";
	}
	print OUT "\n";
    }
    close OUT;
    Platform::replace( "$BigSister::common::fs{'var'}/agent.log.$$", "$BigSister::common::fs{'var'}/agent.log" );
    unlink( "$BigSister::common::fs{'var'}/agent.log.$$" );
}




sub savelogs {
    my( $tag ) = @_;
    log_rotate( "$BigSister::common::fs{'var'}", "display.history", $tag );
}



sub log_rotate {
    my( $dir, $file, $tag ) = @_;

    return unless( -s $dir."/".$file );
    unless( $tag ) {
	my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time); 
	$year += 1900;
	$tag = sprintf( "%04d%02d%02d%02d%02d%02d", $year, $mon+1, $mday, $hour, $min, $sec );
    }
    return if( -s $dir."/".$file.".$tag" );
    my $fh = FileHandle->new();
    if( Platform::replace( $dir."/".$file, $dir."/".$file.".$tag" ) ) {
	print "log_rotate: renaming $file to $file.$tag\n" if( $BigSister::common::dl>1 );
	open( $fh, ">$dir/$file" );
	close( $fh );
    }
    opendir( $fh, $dir ) || return;
    my @files = sort readdir( $fh );
    closedir( $fh );
    while( @files ) {
	my $name = pop @files;
	if( $name =~ /^\Q$file\E\.([a-zA-Z0-9]+)$/ ) {
	    my $mtime = ((stat($dir."/".$name))[9]);
	    if( $mtime && (time-$mtime>$keep_logs) ) {
		print "log_rotate: removing old log $name\n" if( $BigSister::common::dl>1 );
		unlink( $dir."/".$name );
	    }
	}
    }
}


sub sendlogs {
    my( $tag, $socket ) = @_;
    my( $child );

    return(0) unless( -f "$BigSister::common::fs{'var'}/display.history.$tag" );
    #
    # NT does not support fork()
    if( ! Platforms::iswin32() ) {
	if( fork ) {
	    return(1);
	}
	my $conn;
	foreach $conn (@$BigSister::common::open_connections) {
	    unless( $conn->[0] eq $socket ) {
		close( $conn->[0] );
	    }
	}
	$child = 1;
    }
    open( IN, "<$BigSister::common::fs{'var'}/display.history.$tag" );
    eval {
	$SIG{'ALRM'} = sub { close $socket; die "timed out" };
	while( <IN> ) {
	    Platform::bsalarm(60);
	    print $socket $_;
	}
	print $socket "\n--END\n";
	Platform::bsalarm(0);
    };
    Platform::bsalarm(0);
    close IN;
    exit(0) if( $child );
}



sub event {
    ;
}


