hlstatsx/scripts/BASTARDrcon.pm

385 lines
9.7 KiB
Perl

package BASTARDrcon;
#
# BASTARDrcon Perl Module - execute commands on a remote Half-Life 1 server using Rcon.
# A merge of the KKrcon library into HLstatsX
# Copyright (C) 2008-20XX Nicholas Hastings (nshastings@gmail.com)
# KKrcon Perl Module - execute commands on a remote Half-Life server using Rcon.
# http://kkrcon.sourceforge.net
#
# TRcon Perl Module - execute commands on a remote Half-Life2 server using remote console.
# http://www.hlstatsx.com
#
# Copyright (C) 2000, 2001 Rod May
# Enhanced in 2005 by Tobi (Tobi@gameme.de)
#
# 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.
#
use strict;
use sigtrap;
use Socket;
use Sys::Hostname;
use bytes;
##
## Main
##
#
# Constructor
#
sub new
{
my ($class_name, $server_object) = @_;
my ($self) = {};
bless($self, $class_name);
# Initialise properties
$self->{server_object} = $server_object;
$self->{rcon_password} = $server_object->{rcon} or die("BASTARDrcon: a Password is required\n");
$self->{server_host} = $server_object->{address};
$self->{server_port} = int($server_object->{port}) or die("BASTARDrcon: invalid Port \"" . $server_object->{port} . "\"\n");
$self->{socket} = undef;
$self->{error} = "";
# Set up socket parameters
$self->{_ipaddr} = gethostbyname($self->{server_host}) or die("BASTARDrcon: could not resolve Host \"" . $self->{server_host} . "\"\n");
return $self;
}
#
# Execute an Rcon command and return the response
#
sub execute
{
my ($self, $command) = @_;
my $msg;
my $ans;
# version x.1.0.6+ HL1 server
$msg = "\xFF\xFF\xFF\xFFchallenge rcon\n\0";
$ans = $self->_sendrecv($msg);
if ($ans =~ /challenge +rcon +(\d+)/)
{
$msg = "\xFF\xFF\xFF\xFFrcon $1 \"" . $self->{"rcon_password"} . "\" $command\0";
$ans = $self->_sendrecv($msg);
}
elsif (!$self->error())
{
$ans = "";
$self->{"error"} = "No challenge response";
}
if ($ans =~ /bad rcon_password/i)
{
$self->{"error"} = "Bad Password";
}
return $ans;
}
sub _sendrecv
{
my ($self, $msg) = @_;
my $host = $self->{"server_host"};
my $port = $self->{"server_port"};
my $ipaddr = $self->{"_ipaddr"};
my $proto = $self->{"_proto"};
# Open socket
socket($self->{"socket"}, PF_INET, SOCK_DGRAM, $proto) or die("BASTARDrcon(141): socket: $!\n");
my $hispaddr = sockaddr_in($port, $ipaddr);
die("BASTARDrcon: send $ipaddr:$port : $!") unless(defined(send($self->{"socket"}, $msg, 0, $hispaddr)));
my $rin = "";
vec($rin, fileno($self->{"socket"}), 1) = 1;
my $ans = "TIMEOUT";
if (select($rin, undef, undef, 0.5))
{
$ans = "";
$hispaddr = recv($self->{"socket"}, $ans, 8192, 0);
$ans =~ s/\x00+$//; # trailing crap
$ans =~ s/^\xFF\xFF\xFF\xFFl//; # HL response
$ans =~ s/^\xFF\xFF\xFF\xFFn//; # QW response
$ans =~ s/^\xFF\xFF\xFF\xFF//; # Q2/Q3 response
$ans =~ s/^\xFE\xFF\xFF\xFF.....//; # old HL bug/feature
}
# Close socket
close($self->{"socket"});
if ($ans eq "TIMEOUT")
{
$ans = "";
$self->{"error"} = "Rcon timeout";
}
return $ans;
}
#
# Send a package
#
sub send_rcon
{
my ($self, $id, $command, $string1, $string2) = @_;
my $tmp = pack("VVZ*Z*",$id,$command,$string1,$string2);
my $size = length($tmp);
if($size > 4096)
{
$self->{error} = "Command too long to send!";
return 1;
}
$tmp = pack("V", $size) .$tmp;
unless(defined(send($self->{"socket"},$tmp,0)))
{
die("BASTARDrcon: send $!");
}
return 0;
}
#
# Recieve a package
#
sub recieve_rcon
{
my $self = shift;
my ($size, $id, $command, $msg);
my $rin = "";
my $tmp = "";
vec($rin, fileno($self->{"socket"}), 1) = 1;
if(select($rin, undef, undef, 0.5))
{
while(length($size) < 4)
{
$tmp = "";
recv($self->{"socket"}, $tmp, (4-length($size)), 0);
$size .= $tmp;
}
$size = unpack("V", $size);
if($size < 10 || $size > 8192)
{
close($self->{"socket"});
$self->{error} = "illegal size $size ";
return (-1, -1, -1);
}
while(length($id)<4)
{
$tmp = "";
recv($self->{"socket"}, $tmp, (4-length($id)), 0);
$id .= $tmp;
}
$id = unpack("V", $id);
$size = $size - 4;
while(length($command)<4)
{
$tmp ="";
recv($self->{"socket"}, $tmp, (4-length($command)),0);
$command.=$tmp;
}
$command = unpack("V", $command);
$size = $size - 4;
my $msg = "";
while($size >= 1)
{
$tmp = "";
recv($self->{"socket"}, $tmp, $size, 0);
$size -= length($tmp);
$msg .= $tmp;
}
my ($string1,$string2) = unpack("Z*Z*",$msg);
$msg = $string1.$string2;
return ($id, $command, $msg);
}
else
{
return (-1, -1, -1);
}
}
#
# Get error message
#
sub error
{
my ($self) = @_;
return $self->{"error"};
}
#
# Parse "status" command output into player information
#
sub getPlayers
{
my ($self) = @_;
my $status = $self->execute("status");
my @lines = split(/[\r\n]+/, $status);
my %players;
# HL1
# name userid uniqueid frag time ping loss adr
# 1 "psychonic" 1 STEAM_0:1:4153990 0 00:33 13 0 192.168.5.115:27005
foreach my $line (@lines)
{
if ($line =~ /^\#\s*\d+\s+
"(.+)"\s+ # name
(\d+)\s+ # userid
([^\s]+)\s+\d+\s+ # uniqueid
([\d:]+)\s+ # time
(\d+)\s+ # ping
(\d+)\s+ # loss
([^:]+): # addr
(\S+) # port
$/x)
{
my $name = $1;
my $userid = $2;
my $uniqueid = $3;
my $time = $4;
my $ping = $5;
my $loss = $6;
my $state = "";
my $address = $7;
my $port = $8;
$uniqueid =~ s/^STEAM_[0-9]+?\://i;
# &::printEvent("DEBUG", "USERID: '$userid', NAME: '$name', UNIQUEID: '$uniqueid', TIME: '$time', PING: '$ping', LOSS: '$loss', ADDRESS:'$address', CLI_PORT: '$port'", 1);
if ($::g_mode eq "NameTrack") {
$players{$name} = {
"Name" => $name,
"UserID" => $userid,
"UniqueID" => $uniqueid,
"Time" => $time,
"Ping" => $ping,
"Loss" => $loss,
"State" => $state,
"Address" => $address,
"ClientPort" => $port
};
} elsif ($::g_mode eq "LAN") {
$players{$address} = {
"Name" => $name,
"UserID" => $userid,
"UniqueID" => $uniqueid,
"Time" => $time,
"Ping" => $ping,
"Loss" => $loss,
"State" => $state,
"Address" => $address,
"ClientPort" => $port
};
} else {
$players{$uniqueid} = {
"Name" => $name,
"UserID" => $userid,
"UniqueID" => $uniqueid,
"Time" => $time,
"Ping" => $ping,
"Loss" => $loss,
"State" => $state,
"Address" => $address,
"ClientPort" => $port
};
}
}
}
return %players;
}
sub getServerData
{
my ($self) = @_;
my $status = $self->execute("status");
my @lines = split(/[\r\n]+/, $status);
my $servhostname = "";
my $map = "";
my $max_players = 0;
foreach my $line (@lines)
{
if ($line =~ /^\s*hostname\s*:\s*([\S].*)$/x)
{
$servhostname = $1;
}
elsif ($line =~ /^\s*map\s*:\s*([\S]+).*$/x)
{
$map = $1;
}
elsif ($line =~ /^\s*players\s*:\s*\d+.+\((\d+)\smax.*$/)
{
$max_players = $1;
}
}
return ($servhostname, $map, $max_players, 0);
}
sub getVisiblePlayers
{
my ($self) = @_;
my $status = $self->execute("sv_visiblemaxplayers");
my @lines = split(/[\r\n]+/, $status);
my $max_players = -1;
foreach my $line (@lines)
{
# "sv_visiblemaxplayers" = "-1"
# - Overrides the max players reported to prospective clients
if ($line =~ /^\s*"sv_visiblemaxplayers"\s*=\s*"([-0-9]+)".*$/x)
{
$max_players = $1;
}
}
return ($max_players);
}
#
# Get information about a player by userID
#
sub getPlayer
{
my ($self, $uniqueid) = @_;
my %players = $self->getPlayers();
if (defined($players{$uniqueid}))
{
return $players{$uniqueid};
}
else
{
$self->{"error"} = "No such player # $uniqueid";
return 0;
}
}
1;
# end