<?php
error_reporting( E_ALL );
interface IEventDispatcher
{
public function broadcastMessage( $event );
public function addListener( $listener );
public function removeListener( $listener );
}
interface IHLStreamListener
{
public function onData();
public function onConnect();
}
class EventDispatcher implements IEventDispatcher
{
protected $listeners;
public function __construct()
{
$this->listeners = array();
}
public function addListener( $listener )
{
if ( !in_array( $listener, $this->listeners ) )
{
$this->listeners[] = $listener;
return TRUE;
}
return FALSE;
}
public function removeListener( $listener )
{
if ( in_array( $listener, $this->listeners ) )
{
$n = count( $this->listeners );
for ( $i = 0; $i < $n; $i++ )
if ( $this->listeners[ $i ] == $listener )
{
array_splice( $this->listeners, $i, 1 );
return TRUE;
}
}
return FALSE;
}
public function broadcastMessage( $event )
{
$n = count( $this->listeners );
for ( $i = 0; $i < $n; $i++ )
call_user_func( array( &$this->listeners[ $i ], $event ) );
}
}
final class HLServerLog
{
private static $log;
public static function write( $message )
{
if ( !is_array( self::$log ) )
self::$log = array();
self::$log[] = array( time(), debug_backtrace(), $message );
}
public static function getHtml()
{
$html = '';
$n = count( self::$log );
for ( $i = 0; $i < $n; $i++ )
{
$file = basename(self::$log[ $i ][ 1 ][ 1 ][ 'file' ]);
$line = self::$log[ $i ][ 1 ][ 1 ][ 'line' ];
$call = self::$log[ $i ][ 1 ][ 1 ][ 'class' ].self::$log[ $i ][ 1 ][ 1 ][ 'type' ].self::$log[ $i ][ 1 ][ 1 ][ 'function' ];
$args = '';
foreach ( self::$log[ $i ][ 1 ][ 1 ][ 'args' ] as $arg )
$args .= $arg.', ';
$args = substr( $args, 0, -2 );
$html .= '('.date( 'H:i:s', self::$log[ $i ][0 ] ).' : '.str_pad( $line, 5, '0', STR_PAD_LEFT ).' : '.$file.') '.$call.'('.htmlspecialchars( $args ).'): '.htmlspecialchars( self::$log[ $i ][ 2 ] ).'<br />';
}
return $html;
}
}
class HLStream extends EventDispatcher
{
protected $fp;
protected $ip;
protected $port;
protected $timeout;
protected $stream;
protected $pointer;
protected $eof;
const D_BYTE = 0;
const D_CHAR = 1;
const D_SHORT = 2;
const D_LONG = 3;
const D_FLOAT = 4;
const D_STRING = 5;
const M_DATA = 'onData';
const M_CONNECT = 'onConnect';
public function __construct( $ip, $port, $timeout = 10 )
{
HLServerLog::write( 'HLStream constructed.' );
$this->ip = $ip;
$this->port = $port;
$this->timeout = $timeout;
$this->pointer = 0;
$this->stream = '';
$this->eof = TRUE;
$this->listeners = array();
}
public function connect()
{
$this->fp = fsockopen( 'udp://'.$this->ip, $this->port );
if ( !$this->fp )
throw new Exception( 'Could not connect to server.' );
$this->broadcastMessage( self::M_CONNECT );
}
public function get( $type = 0 )
{
if ( $this->eof )
return FALSE;
$result = -1;
switch ( $type )
{
case self::D_BYTE:
$result = ord ( $this->stream{ $this->pointer++ } );
break;
case self::D_CHAR:
$result = $this->stream{ $this->pointer++ };
break;
case self::D_SHORT:
$short = unpack( 'sint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } );
$result = $short[ 'int' ];
$this->pointer += 2;
break;
case self::D_LONG:
$long = unpack( 'iint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } . $this->stream{ $this->pointer + 2 } . $this->stream{ $this->pointer + 3 } );
$result = $long[ 'int' ];
$this->pointer += 4;
break;
case self::D_FLOAT:
$float = unpack( 'fint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } . $this->stream{ $this->pointer + 2 } . $this->stream{ $this->pointer + 3 } );
$result = $float[ 'int' ];
$this->pointer += 4;
break;
case self::D_STRING:
$string = '';
while ( ( $char = $this->stream{ $this->pointer++ } ) != "\x00" )
$string .= $char;
$result = $string;
break;
default:
throw new Exception( 'Illegal data type.' );
}
if ( $this->pointer > strlen( $this->stream ) )
$this->eof = TRUE;
else
$this->eof = FALSE;
return $result;
}
private function getRaw( $type = 0 )
{
switch ( $type )
{
case self::D_BYTE:
return ord( fread( $this->fp, 1 ) );
case self::D_LONG:
$long = unpack( 'iint', fread( $this->fp, 4 ) );
return $long[ 'int' ];
default:
throw new Exception( 'Illegal data type.' );
}
}
public function query( $query )
{
$recall = TRUE;
while ( $recall )
{
try
{
fwrite( $this->fp , $query );
$this->readPackages();
HLServerLog::write( 'Recieved UDP packets on first try.' );
$recall = FALSE;
}
catch( Exception $e )
{
$recall = TRUE;
HLServerLog::write( 'Try again. UDP Package error: '.$e->getMessage() );
}
}
$this->broadcastMessage( self::M_DATA );
}
private function resetStream()
{
$this->pointer = 0;
$this->stream = '';
$this->eof = TRUE;
}
private function read( $count = -1 )
{
if ( $count == -1 )
return fread( $this->fp, min( $this->getRemainingBytes(), 1400 - 9 ) );
return fread( $this->fp, $count );
}
private function getRemainingBytes()
{
$o = socket_get_status( $this->fp );
return $o[ 'unread_bytes' ];
}
private function readPackages()
{
$this->resetStream();
$header = $this->read( 4 );
if ( $header == "\xff\xff\xff\xff" )
$this->stream = $this->read();
else if ( ( $header == "\xff\xff\xff\xfe" ) || ( $header == "\xfe\xff\xff\xff" ) )
{
$pid = $this->getRaw( self::D_LONG );
$num = $this->getRaw( self::D_BYTE );
$cur = ( $num & 0xf0 ) >> 4;
$tot = ( $num & 0x0f );
//-- kick header away
$this->read( 4 );
//-- read package
$this->stream = $this->read();
for ( $i = 1; $i < $tot; $i++ )
{
usleep( 128 );
$header = $this->read( 4 );
if ( ( $header != "\xfe\xff\xff\xff" ) && ( $header != "\xff\xff\xff\xfe" ) )
throw new Exception( 'Illegal header.' );
if ( ( $id = $this->getRaw( self::D_LONG ) ) != $pid )
throw new Exception( 'Invalid package flow. ('.$id.' != '.$pid.')' );
$num = $this->getRaw( self::D_BYTE );
$cur = ( $num & 0xf0 ) >> 4;
if ( $cur != $i )
throw new Exception( 'Wrong positioned package.' );
$this->stream .= $this->read();
}
}
else
throw new Exception( 'Unknown header.' );
if ( strlen( $this->stream ) > 0 )
$this->eof = FALSE;
}
public function addListener( $listener )
{
if ( $listener instanceof IHLStreamListener )
return parent::addListener( $listener );
else
throw new Exception( 'Listener must implement IHLStreamListener' );
}
}
class HLServer implements IHLStreamListener
{
const A2S_INFO = "\xff\xff\xff\xff\x54\x53\x6f\x75\x72\x63\x65\x20\x45\x6e\x67\x69\x6e\x65\x20\x51\x75\x65\x72\x79\x00";
const A2S_PLAYER = "\xff\xff\xff\xff\x55";
const A2S_RULES = "\xff\xff\xff\xff\x56";
const A2S_SERVERQUERY_GETCHALLENGE = "\xff\xff\xff\xff\x57";
const R_INFO = "\x6d";
const R_PLAYER = "\x44";
const R_RULES = "\x45";
const R_SERVERQUERY_GETCHALLENGE = "\x41";
private $sock;
private $challenge;
public $rules;
public $players;
public $server;
public function __construct( $ip, $port )
{
HLServerLog::write( 'HLServer constructed.' );
$this->rules = array();
$this->players = array();
$this->server = array();
$this->challenge = -1;
$this->sock = new HLStream( $ip, $port );
$this->sock->addListener( $this );
try
{
$this->sock->connect();
}
catch( Exception $e )
{
var_dump ( $e );
}
}
private function iif( $cond, $a, $b )
{
return ( $cond ) ? $a : $b;
}
public function onConnect()
{
HLServerLog::write( 'Successfully connected. Now asking for challenge ID.' );
$this->sock->query( self::A2S_SERVERQUERY_GETCHALLENGE );
}
public function onData()
{
$type = $this->sock->get( HLStream::D_CHAR );
switch( $type )
{
case self::R_INFO:
HLServerLog::write( 'Received A2S_INFO.' );
$this->server = array();
$this->server[ 'gameip' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'hostname' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'map' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'gamedir' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'gamedesc' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'players' ] = $this->sock->get( HLStream::D_BYTE );
$this->server[ 'maxplayers' ] = $this->sock->get( HLStream::D_BYTE );
$this->server[ 'version' ] = $this->sock->get( HLStream::D_BYTE );
$this->server[ 'dedicated' ] = $this->iif( strtolower( $this->sock->get( HLStream::D_CHAR ) ) == 'd', TRUE, FALSE );
$this->server[ 'os' ] = $this->iif( strtolower( $this->sock->get( HLStream::D_CHAR ) ) == 'l', 'Linux', 'Windows' );
$this->server[ 'password' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
$this->server[ 'ismod' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
if ( $this->server[ 'ismod' ] )
{
$this->server[ 'urlinfo' ] = $this->sock->get( HLStream::D_STRING );
$this->server[ 'urldl' ] = $this->sock->get( HLStream::D_STRING );
$this->sock->get( HLStream::D_STRING );
$this->server[ 'modversion' ] = $this->sock->get( HLStream::D_LONG );
$this->server[ 'modsize' ] = $this->sock->get( HLStream::D_LONG );
$this->server[ 'svonly' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
$this->server[ 'cldll' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
}
$this->server[ 'secure' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
$this->server[ 'numbots' ] = $this->sock->get( HLStream::D_BYTE );
break;
case self::R_PLAYER:
HLServerLog::write( 'Received A2S_PLAYER.' );
$this->players = array();
$n = $this->sock->get( HLStream::D_BYTE );
for ( $i = 0; $i < $n; $i++ )
{
$uid = $this->sock->get( HLStream::D_BYTE );
$name = $this->sock->get( HLStream::D_STRING );
$kills = $this->sock->get( HLStream::D_LONG );
$time = date( 'H:i:s', round( $this->sock->get( HLStream::D_FLOAT ), 0x00 ) + 0x14370 );
$this->players[ $uid ] = array(
'id' => $uid,
'name' => $name,
'kills' => $kills,
'time' => $time
);
}
break;
case self::R_RULES:
HLServerLog::write( 'Received A2S_RULES.' );
$this->rules = array();
$n = $this->sock->get( HLStream::D_BYTE );
//-- drop leading \0
$this->sock->get( HLStream::D_CHAR );
for ( $i = 0; $i < $n; $i++ )
$this->rules[ $this->sock->get( HLStream::D_STRING ) ] = $this->sock->get( HLStream::D_STRING );
break;
case self::R_SERVERQUERY_GETCHALLENGE:
HLServerLog::write( 'Received A2S_SERVERQUERY_GETCHALLENGE.' );
$this->challenge = pack( 'i', $this->sock->get( HLStream::D_LONG ) );
break;
default:
HLServerLog::write( 'Unknown server response "'.$type.'"' );
//-- has to be handled outside.
throw new Exception( 'Unknown server response. <b>'.$type.'</b>' );
break;
}
}
public function getAllInformation()
{
$this->getInfo();
$this->getPlayers();
$this->getRules();
}
public function getInfo()
{
$this->sock->query( self::A2S_INFO );
return $this->server;
}
public function getPlayers()
{
$this->sock->query( self::A2S_PLAYER.$this->challenge );
return $this->players;
}
public function getRules()
{
$this->sock->query( self::A2S_RULES.$this->challenge );
return $this->rules;
}
}
//-- test it
try
{
$test = new HLServer( '81.169.142.250', 27015 );
$test->getAllInformation();
echo '<h1>log:</h1><br />';
echo HLServerLog::getHtml();
echo '<h1>var_dump:</h1>';
echo '<pre>';
var_dump ( $test->server );
echo "\r\n";
var_dump ( $test->rules );
echo "\r\n";
var_dump ( $test->players );
echo '</pre>';
}
catch( Exception $e )
{
var_dump ( $e );
}
?>