package KKrcon;
#
# KKrcon Perl Module - execute commands on a remote Half-Life server using Rcon.
# http://kkrcon.sourceforge.net
#
# Synopsis:
#
#   use KKrcon;
#   $rcon = new KKrcon(Password=>PASSWORD, [Host=>HOST], [Port=>PORT], [Type=>"new"|"old"]);
#   $result  = $rcon->execute(COMMAND);
#   %players = $rcon->getPlayers();
#   $player  = $rcon->getPlayer(USERID);
# 
# 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;

# Release version number
my $VERSION = "2.11s";

##
## Constants for source engine
##
my $SERVERDATA_EXECCOMMAND = 0x2;
my $SERVERDATA_AUTH = 0x03;
my $SERVERDATA_RESPONSE_VALUE = 0x0;
my $SERVERDATA_AUTH_RESPONSE = 0x2;

##
## Main
##

#
# Constructor
#
sub new
{
	my $class_name = shift;
	my %params = @_;
	my $self = {};
	bless($self, $class_name);
	
	my %server_types = (new=>1, old=>2, source=>3);
	
	# Check parameters
	$params{"Host"} = "127.0.0.1"  unless($params{"Host"});
	$params{"Port"} = 27015        unless($params{"Port"});
	$params{"Type"} = "new"        unless($params{"Type"});
	
	# Initialise properties
	$self->{"rcon_password"} = $params{"Password"} or die("KKrcon: a Password is required\n");
	$self->{"server_host"}   = $params{"Host"};
	$self->{"server_port"}   = int($params{"Port"}) or die("KKrcon: invalid Port \"" . $params{"Port"} . "\"\n");
	$self->{"server_type"}   = ($server_types{$params{"Type"}} || 1);
	
	$self->{"socket"};
	$self->{"error"} = "";
	
	# Set up socket parameters
	$self->{"_proto"}  = ($self->{"server_type"} != 3) ? getprotobyname("udp") : getprotobyname("tcp");
	$self->{"_ipaddr"} = gethostbyname($self->{"server_host"}) or die("KKrcon: could not resolve Host \"" . $self->{"server_host"} . "\"\n");
	
	$self->s_init_socket() if($self->{"server_type"} == 3);
	return $self;
}

#
# Execute an Rcon command and return the response
#
sub execute
{
	my ($self, $command) = @_;
	my $msg;
	my $ans;
	
	if ($self->{"server_type"} == 1)
	{
		# version x.1.0.6+ HL 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";
		}
	}
	elsif ($self->{"server_type"} == 2)
	{
		# QW/Q2/Q3 or old HL server
		$msg = "\xFF\xFF\xFF\xFFrcon " . $self->{"rcon_password"} . " $command\n\0";
		$ans = $self->_sendrecv($msg);
	}
	elsif ($self->{"server_type"} == 3)
	{
		# Source Engine
		$ans = $self->s_sendrecv($command);
	}
	
	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("KKrcon(141): socket: $!\n");
	my $hispaddr = sockaddr_in($port, $ipaddr);
	
	die("KKrcon: 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;
}

#
# Source engine rcon functions
#
sub s_init_socket
{
	my ($self) = @_;
	my $host = $self->{"server_host"};
	my $port = $self->{"server_port"};
	my $ipaddr = $self->{"_ipaddr"};
	my $proto  = $self->{"_proto"};

	if(!socket($self->{"socket"}, PF_INET, SOCK_STREAM, $proto))
	{
		$self->{error} = "Unable to open socket to $ipaddr:$port : $!";
		return;
	}
	
	my $paddr = sockaddr_in($port, $ipaddr);
	if(!connect($self->{"socket"}, $paddr))
	{
		$self->{error} = "Unable to connect to host : $!";
		return;
	}
	return 1;
}

sub s_close_socket
{
	my ($self) = @_;
	# Close socket (if tcp)
	close($self->{"socket"}) if ($self->{"server_type"} == 3);
}


sub s_sendrecv
{
	my ($self, $msg) = @_;
	my $auth = 0;
	my $response = "";
	my ($id, $command);
	$self->s_init_socket() if (!$self->{"socket"});
	
	$self->{error} = "";
	# Sending password
	if($self->send_rcon(20,$SERVERDATA_AUTH, $self->{"rcon_password"},""))
	{
		$self->{error} = "Couldn't send password";
		return;
	}
	
	while( $auth == 0)
	{
		my ($id, $command, $response) = $self->recieve_rcon();
		if($command == $SERVERDATA_AUTH_RESPONSE)
		{
			if($id == 20)
			{
				$auth = 1;
			}
			elsif( $id == -1)
			{
				$self->{error} = "Rcon password refused";
				return;
			}
			else
			{
				$self->{error} = "Bad password response id=$id";
				return;
			}
		}
		elsif($command == -1)
		{
			if ($self->{"server_type"} == 3)
			{
				$self->s_close_socket();
				$self->s_init_socket();
			}
			$self->{error} = "Server timed out!";
			return;
		}
	}
	
	if($self->send_rcon(20,$SERVERDATA_EXECCOMMAND, $msg))
	{
		return;
	}
	$command = 0;
	while($command != -1)
	{
		# Read until timeout, to get all response packages
		my $tmp;
		($id, $command, $tmp) = $self->recieve_rcon();
		$response .= $tmp if($tmp != -1);
	}
	return $response;
}

#
# 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 to long to send!";
		return 1;
	}
	my $tmp = pack("V", $size) .$tmp;
	unless(defined(send($self->{"socket"},$tmp,0)))
	{
		die("KKrcon: 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;
	
	foreach my $line (@lines)
	{
		if ($line =~ /^\#\s*
			(\d+)\s+		# userid
			"(.+)"\s+		# name
			(.+)\s+		    # uniqueid
			([\d:]+)\s+		# time
			(\d+)\s+		# ping
			(\d+)\s+		# loss
			(.+)\s+			# state
			([^:]+):    	# addr
			(\S+)           # port
		   $/x)
		{
			my $userid   = $1;
			my $name     = $2;
			my $uniqueid = $3;
			my $time     = $4;
			my $ping     = $5;
			my $loss     = $6;
			my $state    = $7;
			my $address  = $8;
			my $port     = $9;
			
			$players{$uniqueid} = {
				"Name"       => $name,
				"UserID"     => $userid,
				"UniqueID"   => $uniqueid,
				"Time"       => $time,
				"Ping"       => $ping,
				"Loss"       => $loss,
				"State"      => $state,
				"Address"    => $address,
				"ClientPort" => $port
			};
		}
	}
	return %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
