# HL2 - Perl extension Half-Life 2 (Source) engine Rcon interface
#
# $Id:$
#

package HL2;

use strict;
use warnings;
use IO::Socket;
use IO::Select;

# release version
our $VERSION = "0.05";

# constants for command type
sub CMD  { 2 }
sub AUTH { 3 }

# create class
sub new {
	my $class = shift;

	# create object with defaults
	my $self = {
		hostname	=> undef,
		port		=> 27015,
		password	=> undef,
		timeout		=> 5,
		connected	=> 0,
		authenticated	=> 0,
		socket		=> undef,
		sequence	=> 0,
	};

	# create object
	bless($self, $class);

	# initialize class instances
	$self->init();

	# parse constructor args
	while (my ($key, $val) = splice(@_, 0, 2)) {
		$key = lc($key);
		if    ($key eq "hostname") { $self->hostname($val) }
		elsif ($key eq "port")     { $self->port($val)     }
		elsif ($key eq "password") { $self->password($val) }
		elsif ($key eq "timeout")  { $self->timeout($val)  }
		else { print STDERR "Unknown attribute: $key\n" }
	}

	return $self;
}

# initialize class instances
sub init {
	my $self = shift;
	my $class = ref($self);

	# manipulate symbol table.. gotta love perl
	no strict "refs";
	no warnings;
	foreach my $instance (keys %$self) {
		*{"${class}::${instance}"} = sub {
			my $self = shift;
			my $value = shift;
			my $ref = \$self->{$instance};
			if (defined $value) {
				$$ref = $value;
				return $self;
			} else {
				return $$ref;
			}
		};
	}
}

# run a command and return its response
sub run {
	my $self = shift;
	my $command = shift;

	if (!$self->connected()) {
		$self->connect();
	}

	if (!$self->authenticated()) {
		$self->authenticate();
	}

	my $socket = $self->socket();
	if($socket->connected)
	{
		print $socket $self->packet(CMD, $command);
		return $self->response();
	}
	
	return;
}

# create tcp socket
sub connect {
	my $self = shift;

	my $socket = IO::Socket::INET->new(
		PeerAddr        => $self->hostname(),
		PeerPort        => $self->port(),
		Timeout		=> $self->timeout(),
		Proto           => "tcp",
		Type            => SOCK_STREAM,
	) || die "Failed to connect: $!\n";

	$self->socket($socket);
	$self->connected(1);
}

# authenticate rcon session
sub authenticate {
	my $self = shift;

	# send authentication packet to server
	my $socket = $self->socket();
	print $socket $self->packet(AUTH, $self->password());

	# auth response sends back an empty packet first
	$self->response();
	$self->response();

	$self->authenticated(1);
}

######################
# PROTOCOL FUNCTIONS #
######################

# rcon command protocol:
# (V)[size] (V)[requestID] (V)[command]    (0)[string1] (0)[string2]
#
# rcon response protocol:
# (V)[size] (V)[requestID] (V)[responseID] (0)[string1] (0)[string2]
#
# V = a 32-bit unsigned long int, little-endian (VAX/Intel)
# 0 = null-terminated string
#
# NOTE: string2 appears unused, so our functions ignore it

# create a packet of type (AUTH or CMD)
sub packet {
	my $self     = shift;
	my $type     = shift;
	my $payload  = shift;

	# sequence increments, but auth
	# packet is 2.. no idea why that is,
	# but tcpdump does not lie
	my $sequence;
	if ($type == AUTH) {
		$sequence = 2;
	} else {
		$sequence = $self->sequence();

		# increment for next use
		$self->sequence($sequence + 1);
	}

	my $packet = pack("VV", $sequence, $type) . "$payload\x00\x00";
	$packet    = pack("V", length($packet)) . $packet;

	return $packet;
}

# receive packet
sub response {
	my $self = shift;
	my $payload = $self->read();

	# remove protocol cruft and null terminators
	$payload =~ s/\x00{2}$//;

	return $payload;
}

# read length of bytes from socket with timeout
sub read {
	my $self = shift;
	my $length = shift;

	my $socket = $self->socket();
	my $timeout = $self->timeout();
	my $select = IO::Select->new($socket);

	my $reply = "";
	my $buffer;

	my ($size, $request_id, $command_response, $data);

	while ($select->can_read(0.5)) {
		$socket->recv($buffer, 4, MSG_PEEK);
		$size = unpack("V", $buffer);
		last if (!defined($size));
		$socket->recv($buffer, $size+4, MSG_WAITALL);

		($size, $request_id, $command_response, $data) =
			unpack('VVVZ*x', $buffer);

		$reply .= "$data";
	}

	return $reply;
}

1;

__END__

=head1 NAME

HL2 - Perl extension Half-Life 2 (Source) engine Rcon interface

=head1 SYNOPSIS

  use HL2;
  my $rcon = HL2->new(
  	hostname => "insub.org",
	password => "yourpass",
	timeout  => 3,
  );

  print $rcon->run("status");
  $rcon->run("changelevel de_dust");

=head1 DESCRIPTION

Use this module to send "rcon" (remote control) commands to a
Source server, such as Counter-Strike Source.

=head1 METHODS

=over 4

=item $rcon = HL2->new()

Create a new rcon object.  You can specify the hostname,
password and/or timeout in the constructor, or use the class
methods to change them (see SYNOPSIS).

=item $rcon->authenticated()

Returns true if session has succesfully authenticated.

=item $rcon->password()

Returns current password, or sets it.  Note that setting
this after authentication will not have any effect unless
you reconnect with $rcon->authenticated(0).

=item $rcon->hostname()

Returns current hostname, or sets it.

=item $rcon->port()

Returns current port, or sets it.  Defaults to 27015.

=item $rcon->sequence()

Returns the current command sequence.  This starts
at 0 and increases with each call.

=item $rcon->socket()

Returns the IO::Socket object for the session or
creates a new one if none exists.

=item $rcon->timeout()

Returns the TCP response timeout, or sets it.  Defaults
to 5.

=item $rcon->connect()

Connects to remote server.

=item $packet = $rcon->packet($type, $payload)

Creats a packet to send to the remote server.
Type should be either CMD or AUTH, e.g.:

  print $socket $rcon->packet(AUTH, $rcon->password())

=item $rcon->authenticate()

Authenticates with the rcon server.  This is done automatically
when you try to run a command.

=item $response = $rcon->run($command)

Runs a command on the remote server and returns its response

=item $response = $rcon->response()

Reads a response packet from the server.  This is called
authomatically when you use run() so you shouldn't need to
use this.

=back

=head1 CAVEATS

This module DOES NOT DO ANY COMMAND VALIDATION.  You are responsible for
sending sane commands to the server.  If you use this with CGI that allows
internet users to submit console commands, you MUST taint-check this.  Users
with RCON access can send anything to the console.  I highly recommend that you
restrict what console commands a user can send.

=head1 BUGS

As of this writing, there are some bugs with the rcon server itself.
One such bug is that some output goes to the console instead of to
the rcon client.  For example, the command "listid" causes the list
of banned users to spew to the physical console instead of back to
the rcon client, making it effectively useless.  If you are not getting
back a response you expected, please verify that it's not going to
the console (run srcds in screen so you can access it) before submitting
a bug report to me about it.  Or better yet, submit a bug report to Valve.

Authentication validation is currently unsupported.

=head1 SEE ALSO

 http://gruntle.org/projects/
 http://insub.org/cs/

=head1 AUTHOR

Chris Jones, E<lt>cjones@gruntle.orgE<gt>

=head1 COPYRIGHT AND LICENSE

 Copyright (C) 2004 by Chris Jones

 This library is free software; you can redistribute it and/or modify
 it under the same terms as Perl itself, either Perl version 5.8.5 or,
 at your option, any later version of Perl 5 you may have available.

=cut
