Skip to content

Instantly share code, notes, and snippets.

@segg21
Last active January 27, 2021 01:14
Show Gist options
  • Save segg21/a3c79d69877c27fafabccf427e55e226 to your computer and use it in GitHub Desktop.
Save segg21/a3c79d69877c27fafabccf427e55e226 to your computer and use it in GitHub Desktop.
Steam Source UDP PHP Query Script
<?php
namespace Query {
use Query\Buffer\BinaryReader;
class Socket {
private $socket;
private static $timeout = array('sec'=>4, 'usec' => 0);
function __construct($host, $port)
{
if(!($this->socket = @\socket_create(AF_INET, SOCK_DGRAM, 0)))
throw new \Exception('Could not create socket.');
if(!@\socket_connect($this->socket, $host, intval($port)))
throw new \Exception('Could not connect to socket.');
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, self::$timeout);
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, self::$timeout);
}
function send($data) {
$size = ((is_array($data)) ? count($data) : strlen($data));
if(is_array($data)) $data = implode('', $data);
return ((\socket_send($this->socket, $data, $size, 0)) ? true : false);
}
function read($l = 1024 * 4) {
return array_map('ord', str_split(@\socket_read($this->socket, $l, PHP_BINARY_READ)));
}
function dispose(){
if($this->socket instanceof \Socket) {
socket_close($this->socket);
}
unset($this->socket);
}
}
class Utils {
public static function Ord2Chr($a) {
foreach($a as $k => &$v) {
$a[$k] = chr($v);
}
return $a;
}
}
class Steam {
// Todo, support split protocols
public static function A2S_INFO($host, $port){
$socket = new Socket($host, $port);
$socket->send(Utils::Ord2Chr(array(0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x46, 0x4b, 0x0)));
$reader = new Buffer\BinaryReader($socket->read());
$socket->dispose();
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x49) {
$query = (object) array(
'Protocol' => $reader->readByte(),
'Name' => $reader->readString(),
'Map' => $reader->readString(),
'Folder' => $reader->readString(),
'Game' => $reader->readString(),
'ID' => $reader->readShort(),
'Players' => $reader->readByte(),
'Max Players' => $reader->readByte(),
'Bots' => $reader->readByte(),
'Server Type' => chr($reader->readByte()),
'Environment' => chr($reader->readByte()),
'Visibility' => $reader->readByte(),
'Vac' => $reader->readByte(),
'Version' => $reader->readString(),
'EDF' => $reader->readByte(),
);
unset($socket, $reader);
return $query;
}
unset($socket, $reader);
return null;
}
public static function A2S_PLAYER($host, $port) {
$socket = new Socket($host, $port);
$packet = array(0xFF, 0xFF, 0xFF, 0xFF, 0x55, 0xFF, 0xFF, 0xFF, 0xFF);
$socket->send(Utils::Ord2Chr($packet));
$reader = new Buffer\BinaryReader($socket->read(9));
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x41) {
$packet[5] = $reader->readByte();
$packet[6] = $reader->readByte();
$packet[7] = $reader->readByte();
$packet[8] = $reader->readByte();
$socket->send(Utils::Ord2Chr($packet));
$reader->set($socket->read());
$socket->dispose();
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x44) {
$players = $reader->readByte();
$data = array();
while(--$players > 0) {
array_push($data, array(
'Id' => $reader->readByte(),
'Name' => $reader->readString(),
'Score' => $reader->readInt64(),
//'Duration' => $reader->readFloat()
));
}
unset($packet, $socket, $reader);
return $data;
}
}
unset($packet, $socket, $reader);
return null;
}
public static function A2S_RULES($host, $port) {
$socket = new Socket($host, $port);
$packet = array(0xFF, 0xFF, 0xFF, 0xFF, 0x56, 0xFF, 0xFF, 0xFF, 0xFF);
$socket->send(Utils::Ord2Chr($packet));
$reader = new Buffer\BinaryReader($socket->read(9));
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x41) {
$packet[5] = $reader->readByte();
$packet[6] = $reader->readByte();
$packet[7] = $reader->readByte();
$packet[8] = $reader->readByte();
$socket->send(Utils::Ord2Chr($packet));
$reader->set($socket->read());
$socket->dispose();
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x45) {
$rules = $reader->readShort();
$data = array();
while(--$rules > 0) {
$data[$reader->readString()] = $reader->readString();
}
unset($packet, $socket, $reader);
return $data;
}
}
unset($packet, $socket, $reader);
return null;
}
public static function A2S_PING($host, $port) {
$socket = new Socket($host, $port);
$socket->send(Utils::Ord2Chr(array(0xFF, 0xFF, 0xFF, 0xFF, 0x69)));
$reader = new Buffer\BinaryReader($socket->read());
$socket->dispose();
if($reader->Length() > 5 && $reader->readInt() == -1 && $reader->readByte() == 0x6A) {
$payload = $reader->readString();
unset($socket, $reader);
return $payload;
}
unset($socket, $reader);
return null;
}
}
$ip = null;
$port = null;
$method = 'A2S_INFO';
$class = '\Query\Steam';
if(isset($_GET['ip']) && isset($_GET['port'])) {
$ip = $_GET['ip'];
$port = $_GET['port'];
if(isset($_GET['method'])) {
$method = $_GET['method'];
}
}else {
$path = explode('/', substr($_SERVER['PATH_INFO'], 1));
switch(count($path)) {
case 1:
$path = explode(':', $path[0]);
if(count($path) == 2) {
$ip = $path[0];
$port = intval($path[1]);
}
break;
case 2:
$method = $path[0];
$path = explode(':', $path[1]);
if(count($path) == 2) {
$ip = $path[0];
$port = intval($path[1]);
}
break;
default: throw new \Exception('Invalid Response');
}
}
if($ip == null || $port == null) {
throw new \Exception('IP or PORT not set');
}
if(!method_exists($class, $method)){
throw new \Exception('Method `'.$method.'` does not exists!');
}
header('Content-Type: application/json');
die(json_encode($class::$method($ip, intval($port)) ?? []));
}
namespace Query\Buffer {
class BinaryReader {
private $buffer, $length, $index;
function __construct($buffer)
{
$this->buffer = $buffer;
$this->length = count($this->buffer);
$this->index = 0;
}
private function _read($bytes = 1) {
if(($this->index + $bytes) <= $this->length) {
$this->index += $bytes;
return array_splice($this->buffer, 0, $bytes);
}
throw new \Exception('Buffer overflow');
}
private static function unpack($format, ...$data) {
array_unshift($data, 'C*');
return unpack($format, call_user_func_array('pack', $data))[1];
}
function set($array) {
if(is_array($array)){
$this->buffer = $array;
$this->index = 0;
$this->length = count($this->buffer);
}
}
function Size(){
return $this->length;
}
function Length(){
return $this->length - $this->index;
}
function next(){
$this->_read();
return $this;
}
function readByte(){
return self::unpack('C', $this->_read()[0]);
}
function readShort(){
return self::unpack('s', $this->readByte(), $this->readByte());
}
function readUShort(){
return self::unpack('S', $this->readByte(), $this->readByte());
}
function readInt(){
return self::unpack('i', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte());
}
function readUInt(){
return self::unpack('I', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte());
}
function readInt64(){
return self::unpack('l', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(),$this->readByte(), $this->readByte(),$this->readByte());
}
function readUInt64(){
return self::unpack('L', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(),$this->readByte(), $this->readByte(),$this->readByte());
}
function readFloat(){
return self::unpack('f', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(),$this->readByte(), $this->readByte(),$this->readByte());
}
function readDouble(){
return self::unpack('d', $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(), $this->readByte(),$this->readByte(), $this->readByte(),$this->readByte());
}
function readString(){
$string = "";
while(($char = $this->readByte()) > 0) {
$string .= chr($char);
}
return $string;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment