Download this file · Not for commercial use
  1. <?php
  2.  
  3. error_reporting( E_ALL );
  4.  
  5. interface IEventDispatcher
  6. {
  7. public function broadcastMessage( $event );
  8. public function addListener( $listener );
  9. public function removeListener( $listener );
  10. }
  11.  
  12. interface IHLStreamListener
  13. {
  14. public function onData();
  15. public function onConnect();
  16. }
  17.  
  18. class EventDispatcher implements IEventDispatcher
  19. {
  20. protected $listeners;
  21.  
  22. public function __construct()
  23. {
  24. $this->listeners = array();
  25. }
  26.  
  27. public function addListener( $listener )
  28. {
  29. if ( !in_array( $listener, $this->listeners ) )
  30. {
  31. $this->listeners[] = $listener;
  32. return TRUE;
  33. }
  34. return FALSE;
  35. }
  36.  
  37. public function removeListener( $listener )
  38. {
  39. if ( in_array( $listener, $this->listeners ) )
  40. {
  41. $n = count( $this->listeners );
  42. for ( $i = 0; $i < $n; $i++ )
  43. if ( $this->listeners[ $i ] == $listener )
  44. {
  45. array_splice( $this->listeners, $i, 1 );
  46. return TRUE;
  47. }
  48. }
  49. return FALSE;
  50. }
  51.  
  52. public function broadcastMessage( $event )
  53. {
  54. $n = count( $this->listeners );
  55. for ( $i = 0; $i < $n; $i++ )
  56. call_user_func( array( &$this->listeners[ $i ], $event ) );
  57. }
  58. }
  59.  
  60. final class HLServerLog
  61. {
  62. private static $log;
  63.  
  64. public static function write( $message )
  65. {
  66. if ( !is_array( self::$log ) )
  67. self::$log = array();
  68. self::$log[] = array( time(), debug_backtrace(), $message );
  69. }
  70.  
  71. public static function getHtml()
  72. {
  73. $html = '';
  74.  
  75. $n = count( self::$log );
  76. for ( $i = 0; $i < $n; $i++ )
  77. {
  78. $file = basename(self::$log[ $i ][ 1 ][ 1 ][ 'file' ]);
  79. $line = self::$log[ $i ][ 1 ][ 1 ][ 'line' ];
  80. $call = self::$log[ $i ][ 1 ][ 1 ][ 'class' ].self::$log[ $i ][ 1 ][ 1 ][ 'type' ].self::$log[ $i ][ 1 ][ 1 ][ 'function' ];
  81.  
  82. $args = '';
  83. foreach ( self::$log[ $i ][ 1 ][ 1 ][ 'args' ] as $arg )
  84. $args .= $arg.', ';
  85. $args = substr( $args, 0, -2 );
  86.  
  87. $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 />';
  88. }
  89. return $html;
  90. }
  91. }
  92.  
  93. class HLStream extends EventDispatcher
  94. {
  95. protected $fp;
  96. protected $ip;
  97. protected $port;
  98. protected $timeout;
  99.  
  100. protected $stream;
  101. protected $pointer;
  102. protected $eof;
  103.  
  104. const D_BYTE = 0;
  105. const D_CHAR = 1;
  106. const D_SHORT = 2;
  107. const D_LONG = 3;
  108. const D_FLOAT = 4;
  109. const D_STRING = 5;
  110.  
  111. const M_DATA = 'onData';
  112. const M_CONNECT = 'onConnect';
  113.  
  114. public function __construct( $ip, $port, $timeout = 10 )
  115. {
  116.  
  117. HLServerLog::write( 'HLStream constructed.' );
  118.  
  119. $this->ip = $ip;
  120. $this->port = $port;
  121. $this->timeout = $timeout;
  122.  
  123. $this->pointer = 0;
  124. $this->stream = '';
  125. $this->eof = TRUE;
  126.  
  127. $this->listeners = array();
  128. }
  129.  
  130. public function connect()
  131. {
  132. $this->fp = fsockopen( 'udp://'.$this->ip, $this->port );
  133.  
  134. if ( !$this->fp )
  135. throw new Exception( 'Could not connect to server.' );
  136.  
  137. $this->broadcastMessage( self::M_CONNECT );
  138. }
  139.  
  140. public function get( $type = 0 )
  141. {
  142. if ( $this->eof )
  143. return FALSE;
  144.  
  145. $result = -1;
  146.  
  147. switch ( $type )
  148. {
  149. case self::D_BYTE:
  150. $result = ord ( $this->stream{ $this->pointer++ } );
  151. break;
  152.  
  153. case self::D_CHAR:
  154. $result = $this->stream{ $this->pointer++ };
  155. break;
  156.  
  157. case self::D_SHORT:
  158. $short = unpack( 'sint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } );
  159. $result = $short[ 'int' ];
  160. $this->pointer += 2;
  161. break;
  162.  
  163. case self::D_LONG:
  164. $long = unpack( 'iint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } . $this->stream{ $this->pointer + 2 } . $this->stream{ $this->pointer + 3 } );
  165. $result = $long[ 'int' ];
  166. $this->pointer += 4;
  167. break;
  168.  
  169. case self::D_FLOAT:
  170. $float = unpack( 'fint', $this->stream{ $this->pointer } . $this->stream{ $this->pointer + 1 } . $this->stream{ $this->pointer + 2 } . $this->stream{ $this->pointer + 3 } );
  171. $result = $float[ 'int' ];
  172. $this->pointer += 4;
  173. break;
  174.  
  175. case self::D_STRING:
  176. $string = '';
  177. while ( ( $char = $this->stream{ $this->pointer++ } ) != "\x00" )
  178. $string .= $char;
  179. $result = $string;
  180. break;
  181.  
  182. default:
  183. throw new Exception( 'Illegal data type.' );
  184. }
  185.  
  186. if ( $this->pointer > strlen( $this->stream ) )
  187. $this->eof = TRUE;
  188. else
  189. $this->eof = FALSE;
  190.  
  191. return $result;
  192. }
  193.  
  194. private function getRaw( $type = 0 )
  195. {
  196. switch ( $type )
  197. {
  198. case self::D_BYTE:
  199. return ord( fread( $this->fp, 1 ) );
  200. case self::D_LONG:
  201. $long = unpack( 'iint', fread( $this->fp, 4 ) );
  202. return $long[ 'int' ];
  203. default:
  204. throw new Exception( 'Illegal data type.' );
  205. }
  206. }
  207.  
  208. public function query( $query )
  209. {
  210. $recall = TRUE;
  211.  
  212. while ( $recall )
  213. {
  214. try
  215. {
  216. fwrite( $this->fp , $query );
  217. $this->readPackages();
  218.  
  219. HLServerLog::write( 'Recieved UDP packets on first try.' );
  220.  
  221. $recall = FALSE;
  222. }
  223. catch( Exception $e )
  224. {
  225. $recall = TRUE;
  226. HLServerLog::write( 'Try again. UDP Package error: '.$e->getMessage() );
  227. }
  228. }
  229.  
  230. $this->broadcastMessage( self::M_DATA );
  231. }
  232.  
  233. private function resetStream()
  234. {
  235. $this->pointer = 0;
  236. $this->stream = '';
  237. $this->eof = TRUE;
  238. }
  239.  
  240. private function read( $count = -1 )
  241. {
  242. if ( $count == -1 )
  243. return fread( $this->fp, min( $this->getRemainingBytes(), 1400 - 9 ) );
  244. return fread( $this->fp, $count );
  245. }
  246.  
  247. private function getRemainingBytes()
  248. {
  249. $o = socket_get_status( $this->fp );
  250. return $o[ 'unread_bytes' ];
  251. }
  252.  
  253. private function readPackages()
  254. {
  255. $this->resetStream();
  256.  
  257. $header = $this->read( 4 );
  258.  
  259. if ( $header == "\xff\xff\xff\xff" )
  260. $this->stream = $this->read();
  261. else if ( ( $header == "\xff\xff\xff\xfe" ) || ( $header == "\xfe\xff\xff\xff" ) )
  262. {
  263. $pid = $this->getRaw( self::D_LONG );
  264. $num = $this->getRaw( self::D_BYTE );
  265. $cur = ( $num & 0xf0 ) >> 4;
  266. $tot = ( $num & 0x0f );
  267.  
  268. //-- kick header away
  269. $this->read( 4 );
  270.  
  271. //-- read package
  272. $this->stream = $this->read();
  273.  
  274. for ( $i = 1; $i < $tot; $i++ )
  275. {
  276. usleep( 128 );
  277.  
  278. $header = $this->read( 4 );
  279.  
  280. if ( ( $header != "\xfe\xff\xff\xff" ) && ( $header != "\xff\xff\xff\xfe" ) )
  281. throw new Exception( 'Illegal header.' );
  282.  
  283. if ( ( $id = $this->getRaw( self::D_LONG ) ) != $pid )
  284. throw new Exception( 'Invalid package flow. ('.$id.' != '.$pid.')' );
  285.  
  286. $num = $this->getRaw( self::D_BYTE );
  287. $cur = ( $num & 0xf0 ) >> 4;
  288.  
  289. if ( $cur != $i )
  290. throw new Exception( 'Wrong positioned package.' );
  291.  
  292. $this->stream .= $this->read();
  293. }
  294. }
  295. else
  296. throw new Exception( 'Unknown header.' );
  297.  
  298. if ( strlen( $this->stream ) > 0 )
  299. $this->eof = FALSE;
  300. }
  301.  
  302. public function addListener( $listener )
  303. {
  304. if ( $listener instanceof IHLStreamListener )
  305. return parent::addListener( $listener );
  306. else
  307. throw new Exception( 'Listener must implement IHLStreamListener' );
  308. }
  309. }
  310.  
  311. class HLServer implements IHLStreamListener
  312. {
  313. 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";
  314. const A2S_PLAYER = "\xff\xff\xff\xff\x55";
  315. const A2S_RULES = "\xff\xff\xff\xff\x56";
  316. const A2S_SERVERQUERY_GETCHALLENGE = "\xff\xff\xff\xff\x57";
  317.  
  318. const R_INFO = "\x6d";
  319. const R_PLAYER = "\x44";
  320. const R_RULES = "\x45";
  321. const R_SERVERQUERY_GETCHALLENGE = "\x41";
  322.  
  323. private $sock;
  324. private $challenge;
  325.  
  326. public $rules;
  327. public $players;
  328. public $server;
  329.  
  330. public function __construct( $ip, $port )
  331. {
  332. HLServerLog::write( 'HLServer constructed.' );
  333.  
  334. $this->rules = array();
  335. $this->players = array();
  336. $this->server = array();
  337.  
  338. $this->challenge = -1;
  339.  
  340. $this->sock = new HLStream( $ip, $port );
  341. $this->sock->addListener( $this );
  342.  
  343. try
  344. {
  345. $this->sock->connect();
  346. }
  347. catch( Exception $e )
  348. {
  349. var_dump ( $e );
  350. }
  351. }
  352.  
  353. private function iif( $cond, $a, $b )
  354. {
  355. return ( $cond ) ? $a : $b;
  356. }
  357.  
  358. public function onConnect()
  359. {
  360. HLServerLog::write( 'Successfully connected. Now asking for challenge ID.' );
  361. $this->sock->query( self::A2S_SERVERQUERY_GETCHALLENGE );
  362. }
  363.  
  364. public function onData()
  365. {
  366. $type = $this->sock->get( HLStream::D_CHAR );
  367.  
  368. switch( $type )
  369. {
  370. case self::R_INFO:
  371. HLServerLog::write( 'Received A2S_INFO.' );
  372. $this->server = array();
  373.  
  374. $this->server[ 'gameip' ] = $this->sock->get( HLStream::D_STRING );
  375. $this->server[ 'hostname' ] = $this->sock->get( HLStream::D_STRING );
  376. $this->server[ 'map' ] = $this->sock->get( HLStream::D_STRING );
  377. $this->server[ 'gamedir' ] = $this->sock->get( HLStream::D_STRING );
  378. $this->server[ 'gamedesc' ] = $this->sock->get( HLStream::D_STRING );
  379. $this->server[ 'players' ] = $this->sock->get( HLStream::D_BYTE );
  380. $this->server[ 'maxplayers' ] = $this->sock->get( HLStream::D_BYTE );
  381. $this->server[ 'version' ] = $this->sock->get( HLStream::D_BYTE );
  382. $this->server[ 'dedicated' ] = $this->iif( strtolower( $this->sock->get( HLStream::D_CHAR ) ) == 'd', TRUE, FALSE );
  383. $this->server[ 'os' ] = $this->iif( strtolower( $this->sock->get( HLStream::D_CHAR ) ) == 'l', 'Linux', 'Windows' );
  384. $this->server[ 'password' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
  385. $this->server[ 'ismod' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
  386.  
  387. if ( $this->server[ 'ismod' ] )
  388. {
  389. $this->server[ 'urlinfo' ] = $this->sock->get( HLStream::D_STRING );
  390. $this->server[ 'urldl' ] = $this->sock->get( HLStream::D_STRING );
  391. $this->sock->get( HLStream::D_STRING );
  392. $this->server[ 'modversion' ] = $this->sock->get( HLStream::D_LONG );
  393. $this->server[ 'modsize' ] = $this->sock->get( HLStream::D_LONG );
  394. $this->server[ 'svonly' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
  395. $this->server[ 'cldll' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
  396. }
  397.  
  398. $this->server[ 'secure' ] = $this->iif( $this->sock->get( HLStream::D_BYTE ) == 1, TRUE, FALSE );
  399. $this->server[ 'numbots' ] = $this->sock->get( HLStream::D_BYTE );
  400. break;
  401.  
  402. case self::R_PLAYER:
  403. HLServerLog::write( 'Received A2S_PLAYER.' );
  404. $this->players = array();
  405. $n = $this->sock->get( HLStream::D_BYTE );
  406.  
  407. for ( $i = 0; $i < $n; $i++ )
  408. {
  409. $uid = $this->sock->get( HLStream::D_BYTE );
  410. $name = $this->sock->get( HLStream::D_STRING );
  411. $kills = $this->sock->get( HLStream::D_LONG );
  412. $time = date( 'H:i:s', round( $this->sock->get( HLStream::D_FLOAT ), 0x00 ) + 0x14370 );
  413.  
  414. $this->players[ $uid ] = array(
  415. 'id' => $uid,
  416. 'name' => $name,
  417. 'kills' => $kills,
  418. 'time' => $time
  419. );
  420. }
  421. break;
  422.  
  423. case self::R_RULES:
  424. HLServerLog::write( 'Received A2S_RULES.' );
  425. $this->rules = array();
  426. $n = $this->sock->get( HLStream::D_BYTE );
  427.  
  428. //-- drop leading \0
  429. $this->sock->get( HLStream::D_CHAR );
  430.  
  431. for ( $i = 0; $i < $n; $i++ )
  432. $this->rules[ $this->sock->get( HLStream::D_STRING ) ] = $this->sock->get( HLStream::D_STRING );
  433.  
  434. break;
  435.  
  436. case self::R_SERVERQUERY_GETCHALLENGE:
  437. HLServerLog::write( 'Received A2S_SERVERQUERY_GETCHALLENGE.' );
  438. $this->challenge = pack( 'i', $this->sock->get( HLStream::D_LONG ) );
  439. break;
  440.  
  441. default:
  442. HLServerLog::write( 'Unknown server response "'.$type.'"' );
  443.  
  444. //-- has to be handled outside.
  445. throw new Exception( 'Unknown server response. <b>'.$type.'</b>' );
  446.  
  447. break;
  448.  
  449. }
  450. }
  451.  
  452. public function getAllInformation()
  453. {
  454. $this->getInfo();
  455. $this->getPlayers();
  456. $this->getRules();
  457. }
  458.  
  459. public function getInfo()
  460. {
  461. $this->sock->query( self::A2S_INFO );
  462. return $this->server;
  463. }
  464.  
  465. public function getPlayers()
  466. {
  467. $this->sock->query( self::A2S_PLAYER.$this->challenge );
  468. return $this->players;
  469. }
  470.  
  471. public function getRules()
  472. {
  473. $this->sock->query( self::A2S_RULES.$this->challenge );
  474. return $this->rules;
  475. }
  476. }
  477.  
  478.  
  479. //-- test it
  480.  
  481. try
  482. {
  483. $test = new HLServer( '81.169.142.250', 27015 );
  484. $test->getAllInformation();
  485.  
  486. echo '<h1>log:</h1><br />';
  487. echo HLServerLog::getHtml();
  488.  
  489. echo '<h1>var_dump:</h1>';
  490.  
  491. echo '<pre>';
  492. var_dump ( $test->server );
  493. echo "\r\n";
  494. var_dump ( $test->rules );
  495. echo "\r\n";
  496. var_dump ( $test->players );
  497. echo '</pre>';
  498. }
  499. catch( Exception $e )
  500. {
  501. var_dump ( $e );
  502. }
  503.  
  504.  
  505. ?>
Unless otherwise expressly stated, all original material of whatever nature created by Joa D. Ebert and included in the je2050.de weblog and any related pages, including the weblog's archives, is licensed under a Creative Commons License.
Parsed in 0.544 seconds, using GeSHi 1.0.7.7