457 lines
14 KiB
Perl
457 lines
14 KiB
Perl
|
#!/usr/bin/perl
|
||
|
# HLstatsX Community Edition - Real-time player and clan rankings and statistics
|
||
|
# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com)
|
||
|
# http://www.hlxcommunity.com
|
||
|
#
|
||
|
# HLstatsX Community Edition is a continuation of
|
||
|
# ELstatsNEO - Real-time player and clan rankings and statistics
|
||
|
# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org)
|
||
|
# http://ovrsized.neo-soft.org/
|
||
|
#
|
||
|
# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX
|
||
|
# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2
|
||
|
# http://www.hlstatsx.com/
|
||
|
# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com)
|
||
|
#
|
||
|
# HLstatsX is an enhanced version of HLstats made by Simon Garner
|
||
|
# HLstats - Real-time player and clan rankings and statistics for Half-Life
|
||
|
# http://sourceforge.net/projects/hlstats/
|
||
|
# Copyright (C) 2001 Simon Garner
|
||
|
#
|
||
|
# 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.
|
||
|
#
|
||
|
# For support and installation notes visit http://www.hlxcommunity.com
|
||
|
|
||
|
use strict;
|
||
|
use DBI;
|
||
|
use IO::Socket;
|
||
|
use IO::Select;
|
||
|
use Getopt::Long;
|
||
|
use Time::Local;
|
||
|
|
||
|
no strict 'vars';
|
||
|
|
||
|
##
|
||
|
## Settings
|
||
|
##
|
||
|
|
||
|
# $opt_configfile - Absolute path and filename of configuration file.
|
||
|
$opt_configfile = "./hlstats.conf";
|
||
|
|
||
|
# $opt_libdir - Directory to look in for local required files
|
||
|
# (our *.plib, *.pm files).
|
||
|
$opt_libdir = "./";
|
||
|
$heartbeat = 30;
|
||
|
|
||
|
##
|
||
|
##
|
||
|
################################################################################
|
||
|
## No need to edit below this line
|
||
|
##
|
||
|
require "$opt_libdir/ConfigReaderSimple.pm";
|
||
|
do "$opt_libdir/HLstats.plib";
|
||
|
|
||
|
$|=1;
|
||
|
Getopt::Long::Configure ("bundling");
|
||
|
|
||
|
binmode STDIN, ":utf8";
|
||
|
binmode STDOUT, ":utf8";
|
||
|
|
||
|
# Variables
|
||
|
my %srv_list = ();
|
||
|
my ($datagram,$flags);
|
||
|
my $oldtime = (time + $heartbeat);
|
||
|
|
||
|
$usage = <<EOT
|
||
|
Usage: hlstats.pl [OPTION]...
|
||
|
Collect statistics from one or more Half-Life2 servers for distribution
|
||
|
to sub-daemons (hlstats.pl).
|
||
|
|
||
|
-h, --help display this help and exit
|
||
|
-d, --debug enable debugging output (-dd for more)
|
||
|
-c, --configfile Specific configfile to use, settings in this file can't
|
||
|
be overided with commandline settings.
|
||
|
|
||
|
HLstatsX: Community Edition http://www.hlxcommunity.com
|
||
|
EOT
|
||
|
;
|
||
|
|
||
|
# Read Config File
|
||
|
|
||
|
if ($opt_configfile && -r $opt_configfile) {
|
||
|
$conf = ConfigReaderSimple->new($opt_configfile);
|
||
|
$conf->parse();
|
||
|
|
||
|
%directives = (
|
||
|
"DBHost", "db_host",
|
||
|
"DBUsername", "db_user",
|
||
|
"DBPassword", "db_pass",
|
||
|
"DBName", "db_name",
|
||
|
"BindIP", "s_ip",
|
||
|
"Port", "proxy_port",
|
||
|
"DebugLevel", "g_debug",
|
||
|
);
|
||
|
|
||
|
&doConf($conf, %directives);
|
||
|
} else {
|
||
|
&printEvent("CONFIG", "-- Warning: unable to open configuration file '$opt_configfile'", 1);
|
||
|
}
|
||
|
|
||
|
# Read Command Line Arguments
|
||
|
GetOptions(
|
||
|
"help|h" => \$opt_help,
|
||
|
"configfile|c=s" => \$configfile,
|
||
|
"debug|d+" => \$g_debug
|
||
|
) or die($usage);
|
||
|
|
||
|
if ($opt_help) {
|
||
|
print $usage;
|
||
|
exit(0);
|
||
|
}
|
||
|
|
||
|
if ($configfile && -r $configfile) {
|
||
|
$conf = '';
|
||
|
$conf = ConfigReaderSimple->new($configfile);
|
||
|
$conf->parse();
|
||
|
&doConf($conf, %directives);
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# assignDaemon(string ipaddr, string ipport, hash daemon, hash srv_list)
|
||
|
#
|
||
|
# Round-Robin kind of way of spreading the load to different daemons.
|
||
|
#
|
||
|
sub assignDaemon
|
||
|
{
|
||
|
my ($ipaddr, $ipport, $daemon, $srv_list) = @_;
|
||
|
my $next = "";
|
||
|
|
||
|
if (defined($$srv_list{'rr-next'})) {
|
||
|
$next = $$srv_list{'rr-next'};
|
||
|
} else {
|
||
|
$next = 0;
|
||
|
}
|
||
|
|
||
|
my $max = keys %$daemon;
|
||
|
|
||
|
if (!defined($$srv_list{$ipaddr}{$ipport})) {
|
||
|
if ($next eq $max) {
|
||
|
$next = 1;
|
||
|
} else {
|
||
|
$next++;
|
||
|
}
|
||
|
$$srv_list{'rr-next'} = $next;
|
||
|
|
||
|
$$srv_list{$ipaddr}{$ipport}{'dest_ip'} = $$daemon{$next}{'ip'};
|
||
|
$$srv_list{$ipaddr}{$ipport}{'dest_port'} = $$daemon{$next}{'port'};
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# checkHeartbeat (hash daemon, string proxy_key)
|
||
|
#
|
||
|
# Prints and update the state of the perl daemons, if they are up or not.
|
||
|
#
|
||
|
sub checkHeartbeat
|
||
|
{
|
||
|
my ($daemon, $proxy_key) = @_;
|
||
|
|
||
|
my $state = '';
|
||
|
foreach my $key (keys(%$daemon)) {
|
||
|
my $value = $$daemon{$key};
|
||
|
my $socket = IO::Socket::INET->new( Proto=>"udp",
|
||
|
PeerHost=>$$daemon{$key}{'ip'},
|
||
|
PeerPort=>$$daemon{$key}{'port'}
|
||
|
);
|
||
|
$packet = "C;HEARTBEAT;";
|
||
|
$socket->send("PROXY Key=$proxy_key PROXY $packet");
|
||
|
|
||
|
if(IO::Select->new($socket)->can_read(4)) { # 4 second timeout
|
||
|
$socket->recv($msg,1024);
|
||
|
if ($msg =~ /Heartbeat OK/) {
|
||
|
$state = "up";
|
||
|
} else {
|
||
|
$state = "down";
|
||
|
}
|
||
|
}
|
||
|
if ($$daemon{$key}{'curstate'} eq "") {
|
||
|
$$daemon{$key}{'curstate'} = "n/a";
|
||
|
}
|
||
|
|
||
|
$$daemon{$key}{'oldstate'} = $$daemon{$key}{'curstate'};
|
||
|
$$daemon{$key}{'curstate'} = $state;
|
||
|
|
||
|
&printEvent("HEARTBEAT", "Sending HB to $$daemon{$key}{'ip'}:$$daemon{$key}{'port'}... state: $$daemon{$key}{'curstate'} (old: $$daemon{$key}{'oldstate'})", 1);
|
||
|
$state = '';
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# string retunrServerList(hash srv_list)
|
||
|
#
|
||
|
# Return a list of servers to requestor (udp package C;SERVERLIST).
|
||
|
#
|
||
|
sub returnServerList
|
||
|
{
|
||
|
my ($srv_list) = @_;
|
||
|
#$srv_list{$ipaddr}{$ipport}{'dest_ip'}
|
||
|
|
||
|
for my $ip (keys(%srv_list)) {
|
||
|
for my $port (keys(%{$srv_list{$ip}})) {
|
||
|
$msg = $msg . "$ip:$port -> $srv_list{$ip}{$port}{'dest_ip'}:$srv_list{$ip}{$port}{'dest_port'}\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $msg;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# string theTime(int sec, int min, int hour, int mday, int year, int wday, int yday, int isdst)
|
||
|
#
|
||
|
# Makes a pretty timestampformat to output
|
||
|
#
|
||
|
sub theTime
|
||
|
{
|
||
|
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
||
|
$year = $year + 1900;
|
||
|
$mon = $mon + 1;
|
||
|
|
||
|
if ($mon <= 9) { $mon = "0$mon"; }
|
||
|
if ($mday <= 9) { $mday = "0$mday"; }
|
||
|
if ($hour <= 9) { $hour = "0$hour"; }
|
||
|
if ($min <= 9) { $min = "0$min"; }
|
||
|
if ($sec <= 9) { $sec = "0$sec"; }
|
||
|
|
||
|
my $time = "[$year-$mon-$mday $hour:$min:$sec] ";
|
||
|
|
||
|
return $time;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# string reloadDaemon(hash daemon, string proxy_key)
|
||
|
#
|
||
|
# Sends reload package to all daemons specified in hlstats.Options.Proxy_Daemons
|
||
|
#
|
||
|
sub reloadDaemon
|
||
|
{
|
||
|
my ($daemon, $proxy_key) = @_;
|
||
|
my $fake_ip = "127.0.0.1";
|
||
|
my $fake_port = "30000";
|
||
|
my $msg = '';
|
||
|
$packet = "C;RELOAD;";
|
||
|
|
||
|
foreach my $key (keys(%$daemon)) {
|
||
|
if ($$daemon{$key}{'curstate'} eq "up") {
|
||
|
&printEvent("CONTROL", "Sending RELOAD packet to $$daemon{$key}{'ip'}:$$daemon{$key}{'port'}", 1);
|
||
|
$msg = $msg . &theTime() . "Sending RELOAD packet to $daemon{$key}{'ip'}:$daemon{$key}{'port'}\n";
|
||
|
|
||
|
# Sedning actual message to the daemon.
|
||
|
my $cmd = IO::Socket::INET->new( Proto=>"udp",
|
||
|
PeerHost=>$$daemon{$key}{'ip'},
|
||
|
PeerPort=>$$daemon{$key}{'port'}
|
||
|
);
|
||
|
$cmd->send("PROXY Key=$proxy_key PROXY $packet");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $msg;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# string getProxyKey ()
|
||
|
#
|
||
|
# Get the value for Proxy_Key
|
||
|
#
|
||
|
sub getProxyKey
|
||
|
{
|
||
|
my $query = "SELECT `value` FROM hlstats_Options WHERE `keyname` = 'Proxy_Key'";
|
||
|
my $result = &doQuery($query);
|
||
|
my ($proxy_key) = $result->fetchrow_array;
|
||
|
$result->finish;
|
||
|
|
||
|
return $proxy_key;
|
||
|
}
|
||
|
|
||
|
sub is_number ($) { ( $_[0] ^ $_[0] ) eq '0' }
|
||
|
|
||
|
|
||
|
############## Main program ##############
|
||
|
$g_stdin = 0;
|
||
|
|
||
|
# Connect yo mysql DB to get required settings
|
||
|
&doConnect();
|
||
|
|
||
|
my $proxy_key = &getProxyKey();
|
||
|
|
||
|
# Get the daemons you will use
|
||
|
$query = "SELECT `value` FROM hlstats_Options WHERE `keyname` = 'Proxy_Daemons'";
|
||
|
$result = &doQuery($query);
|
||
|
|
||
|
my ($daemonlist) = $result->fetchrow_array;
|
||
|
$result->finish;
|
||
|
my @proxy_daemons = split(/,/, $daemonlist);
|
||
|
my $total_daemons = scalar(@proxy_daemons);
|
||
|
|
||
|
my %daemon = ();
|
||
|
my $i = 1;
|
||
|
|
||
|
while ($i <= $total_daemons) {
|
||
|
($daemon{$i}{'ip'}, $daemon{$i}{'port'}) = split(/:/, $proxy_daemons[$i-1]);
|
||
|
$daemon{$i}{'oldstate'} = "";
|
||
|
$daemon{$i}{'curstate'} = "";
|
||
|
$i++;
|
||
|
}
|
||
|
|
||
|
# Setting up the proxy port to listen on.
|
||
|
my $server = IO::Socket::INET->new( LocalPort=>$proxy_port,
|
||
|
Proto=>"udp"
|
||
|
) or die "Can't create UDP server: $@";
|
||
|
|
||
|
# It went ok, lets start recive messages...
|
||
|
&printEvent("DAEMON", "HlstatsX Proxy Daemon up and running on port: $proxy_port, key: $proxy_key", 1);
|
||
|
|
||
|
# Do initial heartbeat check.
|
||
|
&checkHeartbeat(\%daemon, $proxy_key);
|
||
|
|
||
|
# Reload all child daemons config
|
||
|
&reloadDaemon(\%daemon, $proxy_key);
|
||
|
|
||
|
while ($server->recv($datagram,1024,$flags)) {
|
||
|
my $control = 0;
|
||
|
# Checks the subdaemons every 30 sec if they are alive.
|
||
|
# the interval can be changed by modify $heartbeat value in beginning of script.
|
||
|
if (time > $oldtime) {
|
||
|
&checkHeartbeat(\%daemon, $proxy_key);
|
||
|
$oldtime = (time + $heartbeat);
|
||
|
}
|
||
|
|
||
|
my $ipaddr = $server->peerhost;
|
||
|
my $ipport = $server->peerport;
|
||
|
|
||
|
if ($ipaddr eq "127.0.0.1" && $datagram =~/C;HEARTBEAT;/) {
|
||
|
$control = 1;
|
||
|
$msg = '';
|
||
|
$msg = "Heartbeat OK";
|
||
|
&printEvent("CONTROL", "Sending Heartbeat to $ipaddr:$ipport", 1);
|
||
|
} elsif ($ipaddr eq "127.0.0.1" && $datagram =~/C;SERVERLIST;/) {
|
||
|
$control = 1;
|
||
|
$msg = '';
|
||
|
$msg = returnServerList($srv_list);
|
||
|
$msg = "ServerList\n$msg";
|
||
|
&printEvent("CONTROL", "Sending Serverlist to $ipaddr:$ipport", 1);
|
||
|
} elsif ($ipaddr eq "127.0.0.1" && $datagram =~/C;RELOAD;/) {
|
||
|
$control = 1;
|
||
|
$msg = '';
|
||
|
$msg = &reloadDaemon($daemon);
|
||
|
}
|
||
|
|
||
|
if ($ipaddr eq "127.0.0.1" && $control == 1) {
|
||
|
# Sending actual message to the daemon.
|
||
|
my $dest = sockaddr_in($ipport, inet_aton($ipaddr));
|
||
|
my $bytes = send($server, $msg, 0, $dest);
|
||
|
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
if ($datagram =~ /PROXY Key=(.+) (.*)PROXY (.+)/) {
|
||
|
if ($proxy_key eq $1) {
|
||
|
if ($3 =~ /C;HEARTBEAT;/) {
|
||
|
$msg = '';
|
||
|
$msg = "Heartbeat OK";
|
||
|
&printEvent("CONTROL", "Sending Heartbeat to $ipaddr:$ipport", 1);
|
||
|
} elsif ($3 =~ /C;SERVERLIST;/) {
|
||
|
$msg = '';
|
||
|
$msg = returnServerList($srv_list);
|
||
|
$msg = "ServerList\n$msg";
|
||
|
&printEvent("CONTROL", "Sending Serverlist to $ipaddr:$ipport", 1);
|
||
|
&printEvent("CONTROL", $msg, 1);
|
||
|
} elsif ($3 =~ /C;RELOAD;/) {
|
||
|
$msg = '';
|
||
|
$msg = &reloadDaemon($daemon);
|
||
|
}
|
||
|
} else {
|
||
|
$msg = "FAILED PROXY REQUEST ($ipaddr:$ipport)\n";
|
||
|
&printEvent("E403", "Sending FAILED PROXY REQUEST to $ipaddr:$ipport", 1);
|
||
|
}
|
||
|
|
||
|
|
||
|
# Sedning actual message to the daemon.
|
||
|
my $dest = sockaddr_in($ipport, inet_aton($ipaddr));
|
||
|
my $bytes = send($server, $msg, 0, $dest);
|
||
|
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
if (defined($srv_list{$ipaddr}{$ipport})) {
|
||
|
# Check the oldstate, curstate of your logging daemon
|
||
|
foreach my $key (keys %daemon) {
|
||
|
if ($srv_list{$ipaddr}{$ipport}{'dest_ip'} eq $daemon{$key}{'ip'} && $srv_list{$ipaddr}{$ipport}{'dest_port'} eq $daemon{$key}{'port'}) {;
|
||
|
if ($daemon{$key}{'curstate'} eq "up" && $daemon{$key}{'oldstate'} eq "down") {
|
||
|
# Recovering, should do a reload of some kind here.
|
||
|
%srv_list = ();
|
||
|
|
||
|
} elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "up") {
|
||
|
# Daemon died, assing a new daemon to server
|
||
|
|
||
|
delete $srv_list{$ipaddr}{$ipport};
|
||
|
($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list);
|
||
|
&printEvent("BALANCE", "down - up: Re-Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1);
|
||
|
} elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "down") {
|
||
|
# DOWN, should already reassinged the daemon.
|
||
|
|
||
|
delete $srv_list{$ipaddr}{$ipport};
|
||
|
($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list);
|
||
|
&printEvent("BALANCE", "down-down: Re-Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1);
|
||
|
} elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "n/a") {
|
||
|
# Daemon down when we started proxy, assing another daemon.
|
||
|
|
||
|
delete $srv_list{$ipaddr}{$ipport};
|
||
|
($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list);
|
||
|
&printEvent("BALANCE", "down - na: Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport from down/na", 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
# Assign a logging daemon for your server:port
|
||
|
delete $srv_list{$ipaddr}{$ipport};
|
||
|
&assignDaemon($ipaddr, $ipport, \%daemon, \%srv_list);
|
||
|
&printEvent("BALANCE", "Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
if ($datagram =~ /.*rcon from.*: command "status".*/ || $datagram =~ /.*rcon from.*: command "stats".*/ || $datagram =~ /.*rcon from.*: command "".*/) {
|
||
|
# skip messages that looks like this, to ease the load on the sub daemons alittle
|
||
|
&printEvent("NOTICE", "Skipping message...", 1) if ($g_debug > 1);
|
||
|
} else {
|
||
|
if (defined($srv_list{$ipaddr}{$ipport}{'dest_ip'}) && defined($srv_list{$ipaddr}{$ipport}{'dest_port'})) {
|
||
|
$datagram =~ s/^.*RL /RL /g;
|
||
|
|
||
|
&printEvent("NOTICE", "Sending $datagram to daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'}", 1) if ($g_debug > 1);
|
||
|
# Sedning actual message to the daemon.
|
||
|
my $forward = IO::Socket::INET->new( Proto=>"udp",
|
||
|
PeerHost=>$srv_list{$ipaddr}{$ipport}{'dest_ip'},
|
||
|
PeerPort=>$srv_list{$ipaddr}{$ipport}{'dest_port'}
|
||
|
);
|
||
|
$forward->send("PROXY Key=$proxy_key $ipaddr:".$ipport."PROXY $datagram");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|