robinp7720/Blue-Stats-Minecraft

View on GitHub
plugins/query/minecraftQuery.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

namespace xPaw;
class MinecraftQueryException extends \Exception {
    // Exception thrown by MinecraftQuery class
}

class MinecraftQuery {
    /*
     * Class written by xPaw
     *
     * Website: http://xpaw.me
     * GitHub: https://github.com/xPaw/PHP-Minecraft-Query
     */
    const STATISTIC = 0x00;
    const HANDSHAKE = 0x09;
    private $Socket;
    private $Players;
    private $Info;

    public function Connect ($Ip, $Port = 25565, $Timeout = 3) {
        if (!is_int($Timeout) || $Timeout < 0) {
            throw new \InvalidArgumentException('Timeout must be an integer.');
        }
        $this->Socket = @FSockOpen('udp://' . $Ip, (int) $Port, $ErrNo, $ErrStr, $Timeout);
        if ($ErrNo || $this->Socket === FALSE) {
            throw new MinecraftQueryException('Could not create socket: ' . $ErrStr);
        }
        Stream_Set_Timeout($this->Socket, $Timeout);
        Stream_Set_Blocking($this->Socket, TRUE);
        try {
            $Challenge = $this->GetChallenge();
            $this->GetStatus($Challenge);
        } // We catch this because we want to close the socket, not very elegant
        catch (MinecraftQueryException $e) {
            FClose($this->Socket);
            throw new MinecraftQueryException($e->getMessage());
        }
        FClose($this->Socket);
    }

    private function GetChallenge () {
        $Data = $this->WriteData(self :: HANDSHAKE);
        if ($Data === FALSE) {
            throw new MinecraftQueryException('Failed to receive challenge.');
        }

        return Pack('N', $Data);
    }

    private function WriteData ($Command, $Append = "") {
        $Command = Pack('c*', 0xFE, 0xFD, $Command, 0x01, 0x02, 0x03, 0x04) . $Append;
        $Length  = StrLen($Command);
        if ($Length !== FWrite($this->Socket, $Command, $Length)) {
            throw new MinecraftQueryException("Failed to write on socket.");
        }
        $Data = FRead($this->Socket, 4096);
        if ($Data === FALSE) {
            throw new MinecraftQueryException("Failed to read from socket.");
        }
        if (StrLen($Data) < 5 || $Data[0] != $Command[2]) {
            return FALSE;
        }

        return SubStr($Data, 5);
    }

    private function GetStatus ($Challenge) {
        $Data = $this->WriteData(self :: STATISTIC, $Challenge . Pack('c*', 0x00, 0x00, 0x00, 0x00));
        if (!$Data) {
            throw new MinecraftQueryException('Failed to receive status.');
        }
        $Last = '';
        $Info = [];
        $Data = SubStr($Data, 11); // splitnum + 2 int
        $Data = Explode("\x00\x00\x01player_\x00\x00", $Data);
        if (Count($Data) !== 2) {
            throw new MinecraftQueryException('Failed to parse server\'s response.');
        }
        $Players = SubStr($Data[1], 0, -2);
        $Data    = Explode("\x00", $Data[0]);
        // Array with known keys in order to validate the result
        // It can happen that server sends custom strings containing bad things (who can know!)
        $Keys = [
            'hostname'   => 'HostName',
            'gametype'   => 'GameType',
            'version'    => 'Version',
            'plugins'    => 'Plugins',
            'map'        => 'Map',
            'numplayers' => 'Players',
            'maxplayers' => 'MaxPlayers',
            'hostport'   => 'HostPort',
            'hostip'     => 'HostIp',
            'game_id'    => 'GameName',
        ];
        foreach ($Data as $Key => $Value) {
            if (~$Key & 1) {
                if (!Array_Key_Exists($Value, $Keys)) {
                    $Last = FALSE;
                    continue;
                }
                $Last        = $Keys[$Value];
                $Info[$Last] = '';
            }
            elseif ($Last != FALSE) {
                $Info[$Last] = mb_convert_encoding($Value, 'UTF-8');
            }
        }
        // Ints
        $Info['Players']    = IntVal($Info['Players']);
        $Info['MaxPlayers'] = IntVal($Info['MaxPlayers']);
        $Info['HostPort']   = IntVal($Info['HostPort']);
        // Parse "plugins", if any
        if ($Info['Plugins']) {
            $Data               = Explode(": ", $Info['Plugins'], 2);
            $Info['RawPlugins'] = $Info['Plugins'];
            $Info['Software']   = $Data[0];
            if (Count($Data) == 2) {
                $Info['Plugins'] = Explode("; ", $Data[1]);
            }
        }
        else {
            $Info['Software'] = 'Vanilla';
        }
        $this->Info = $Info;
        if (empty($Players)) {
            $this->Players = NULL;
        }
        else {
            $this->Players = Explode("\x00", $Players);
        }
    }

    public function GetInfo () {
        return isset($this->Info) ? $this->Info : FALSE;
    }

    public function GetPlayers () {
        return isset($this->Players) ? $this->Players : FALSE;
    }
}