<?php
class ServerQueryException extends Exception {}
class ServerQuery {
const TYPE_CHALLENGE = 0x09;
const TYPE_QUERY = 0x00;
private $socket;
private $players;
private $info;
public function connect($addr, $port = 25565, $timeout = 3) {
if(!is_int($timeout) || $timeout < 0) {
throw new InvalidArgumentException('Timeout must be a positive integer.');
}
if(!is_int($port) || $port < 1 || $port > 65535) {
throw new InvalidArgumentException('Port must be an integer between 1 and 65535.');
}
$this -> socket = @fsockopen('udp://' . $addr, (int)$port, $errno, $errstr, $timeout);
if($errno || $this -> socket === false) {
throw new ServerQueryException('Could not create socket: ' . $errstr);
}
stream_set_timeout($this -> socket, $timeout);
stream_set_blocking($this -> socket, true);
try {
$challenge = $this -> getChallenge();
$this -> getStatus($challenge);
} catch(ServerQueryException $ex) {
// Dispose of socket and close the connection
fclose($this -> socket);
throw new ServerQueryException($ex -> getMessage());
}
fclose($this -> socket);
}
public function getServerInfo() {
return isset($this -> info) ? $this -> info : false;
}
public function getPlayerList() {
return isset($this -> players) ? $this -> players : false;
}
private function getChallenge() {
$data = $this -> query(self::TYPE_CHALLENGE);
if($data === false) {
throw new ServerQueryException('Failed to receive challenge token.');
}
return pack('N', $data);
}
private function getStatus($challenge) {
$data = $this -> query(self::TYPE_QUERY, $challenge . pack('c*', 0x00, 0x00, 0x00, 0x00));
if(!$data) {
throw new ServerQueryException('Failed to get server status.');
}
$lastKey = '';
$info = array();
$data = substr($data, 11);
$data = explode("\x00\x00\x01player_\x00\x00", $data);
if(count($data) !== 2) {
throw new ServerQueryException('Failed to parse server status.');
}
$players = substr($data[1], 0, -2);
$data = explode("\x00", $data[0]);
$validKeys = array(
'game_id' => 'Game',
'hostname' => 'Server Name',
'gametype' => 'Game Type',
'version' => 'Version',
'plugins' => 'Plugins', //This will never actually display anything; it's hardcoded to ""
'map' => 'Default World',
'numplayers' => 'Players',
'maxplayers' => 'Max Players',
'hostport' => 'Server Port',
'hostip' => 'Server Address'
);
foreach($data as $key => $value) {
if(~$key & 1) {
if(!array_key_exists($value, $validKeys)) {
$lastKey = false;
continue;
}
$lastKey = $validKeys[$value];
$info[$lastKey] = '';
} else if($lastKey != false) {
$info[$lastKey] = $value;
}
}
// Check integers
$info['Players'] = intval($info['Players']);
$info['Max Players'] = intval($info['Max Players']);
$info['Server Port'] = intval($info['Server Port']);
// Parse installed plugins, if any
if($info['Plugins']) {
$data = explode(": ", $info['Plugins'], 2);
$info['Raw_Plugins'] = $info['Plugins'];
$info['Software'] = $data[0];
if(count($data) == 2) {
$info['Plugins'] = explode(", ", $data[1]);
}
} else {
$info['Software'] = 'Vanilla Server';
}
$this -> info = $info;
if($players) {
$this -> players = explode("\x00", $players);
}
}
private function query($command, $append = "") {
$command = pack('c*', 0xFE, 0xFD, $command, 0x01, 0x02, 0x03, 0x04) . $append;
$length = strlen($command);
// Compare number of bytes sent with number of bytes returned by fwrite
// If there is a mismatch, then we have a problem
if($length !== fwrite($this -> socket, $command, $length)) {
throw new ServerQueryException('Failed to write to socket.');
}
$data = fread($this -> socket, 2048);
if($data === false) {
throw new ServerQueryException('Failed to read from socket.');
}
if(strlen($data) < 5 || $data[0] != $command[2]) {
return false;
}
return substr($data, 5);
}
}
?>
Comments