472 lines
11 KiB
PHP
472 lines
11 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* ManiaPlanet dedicated server Xml-RPC client
|
||
|
*
|
||
|
* @license http://www.gnu.org/licenses/lgpl.html LGPL License 3
|
||
|
*/
|
||
|
|
||
|
namespace Maniaplanet\DedicatedServer\Xmlrpc;
|
||
|
|
||
|
if (!defined('LF'))
|
||
|
{
|
||
|
define('LF', "\n");
|
||
|
}
|
||
|
|
||
|
if (!defined('SIZE_MAX'))
|
||
|
{
|
||
|
define('SIZE_MAX', 4096*1024);
|
||
|
}
|
||
|
|
||
|
class Client
|
||
|
{
|
||
|
public $socket;
|
||
|
public $message = false;
|
||
|
public $cb_message = array();
|
||
|
public $reqhandle;
|
||
|
public $protocol = 0;
|
||
|
|
||
|
static $received;
|
||
|
static $sent;
|
||
|
|
||
|
function bigEndianTest()
|
||
|
{
|
||
|
list($endiantest) = array_values(unpack('L1L', pack('V', 1)));
|
||
|
if ($endiantest != 1)
|
||
|
{
|
||
|
if(!function_exists(__NAMESPACE__.'\\unpack'))
|
||
|
{
|
||
|
/**
|
||
|
* The following code is a workaround for php's unpack function which
|
||
|
* does not have the capability of unpacking double precision floats
|
||
|
* that were packed in the opposite byte order of the current machine.
|
||
|
*/
|
||
|
function unpack($format, $data)
|
||
|
{
|
||
|
$ar = unpack($format, $data);
|
||
|
$vals = array_values($ar);
|
||
|
$f = explode('/', $format);
|
||
|
$i = 0;
|
||
|
foreach ($f as $f_k => $f_v)
|
||
|
{
|
||
|
$repeater = intval(substr($f_v, 1));
|
||
|
if ($repeater == 0)
|
||
|
{
|
||
|
$repeater = 1;
|
||
|
}
|
||
|
if ($f_v{1} == '*')
|
||
|
{
|
||
|
$repeater = count($ar) - $i;
|
||
|
}
|
||
|
if ($f_v{0} != 'd')
|
||
|
{
|
||
|
$i += $repeater;
|
||
|
continue;
|
||
|
}
|
||
|
$j = $i + $repeater;
|
||
|
for ($a = $i; $a < $j; ++$a)
|
||
|
{
|
||
|
$p = pack('d', $vals[$i]);
|
||
|
$p = strrev($p);
|
||
|
list($vals[$i]) = array_values(unpack('d1d', $p));
|
||
|
++$i;
|
||
|
}
|
||
|
}
|
||
|
$a = 0;
|
||
|
foreach ($ar as $ar_k => $ar_v)
|
||
|
{
|
||
|
$ar[$ar_k] = $vals[$a];
|
||
|
++$a;
|
||
|
}
|
||
|
return $ar;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function __construct($hostname = 'localhost', $port = 5000, $timeout)
|
||
|
{
|
||
|
$this->socket = false;
|
||
|
$this->reqhandle = 0x80000000;
|
||
|
$this->init($hostname, $port, $timeout);
|
||
|
}
|
||
|
|
||
|
function __destruct()
|
||
|
{
|
||
|
$this->terminate();
|
||
|
}
|
||
|
|
||
|
protected function init($hostname, $port, $timeout)
|
||
|
{
|
||
|
|
||
|
$this->bigEndianTest();
|
||
|
|
||
|
// open connection
|
||
|
$this->socket = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
|
||
|
if (!$this->socket)
|
||
|
{
|
||
|
throw new Exception("transport error - could not open socket (error: $errno, $errstr)", -32300);
|
||
|
}
|
||
|
// handshake
|
||
|
$array_result = unpack('Vsize', fread($this->socket, 4));
|
||
|
$size = $array_result['size'];
|
||
|
if ($size > 64)
|
||
|
{
|
||
|
throw new Exception('transport error - wrong lowlevel protocol header', -32300);
|
||
|
}
|
||
|
$handshake = fread($this->socket, $size);
|
||
|
if ($handshake == 'GBXRemote 1')
|
||
|
{
|
||
|
$this->protocol = 1;
|
||
|
}
|
||
|
elseif ($handshake == 'GBXRemote 2')
|
||
|
{
|
||
|
$this->protocol = 2;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw new Exception('transport error - wrong lowlevel protocol version', -32300);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function terminate()
|
||
|
{
|
||
|
if ($this->socket)
|
||
|
{
|
||
|
fclose($this->socket);
|
||
|
$this->socket = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected function sendRequest(Request $request)
|
||
|
{
|
||
|
$xml = $request->getXml();
|
||
|
|
||
|
@stream_set_timeout($this->socket, 20); // timeout 20 s (to write the request)
|
||
|
// send request
|
||
|
$this->reqhandle++;
|
||
|
if ($this->protocol == 1)
|
||
|
{
|
||
|
$bytes = pack('Va*', strlen($xml), $xml);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml);
|
||
|
}
|
||
|
|
||
|
$bytes_to_write = strlen($bytes);
|
||
|
|
||
|
// increase sent counter ...
|
||
|
self::$sent += $bytes_to_write;
|
||
|
|
||
|
while ($bytes_to_write > 0)
|
||
|
{
|
||
|
$r = fwrite($this->socket, $bytes);
|
||
|
if ($r === false || $r == 0)
|
||
|
{
|
||
|
throw new Exception('Connection interupted');
|
||
|
}
|
||
|
|
||
|
$bytes_to_write -= $r;
|
||
|
if ($bytes_to_write == 0)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
$bytes = substr($bytes, $r);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected function getResult()
|
||
|
{
|
||
|
$contents = '';
|
||
|
$contents_length = 0;
|
||
|
do
|
||
|
{
|
||
|
$size = 0;
|
||
|
$recvhandle = 0;
|
||
|
@stream_set_timeout($this->socket, 5); // timeout 20 s (to read the reply header)
|
||
|
// Get result
|
||
|
if ($this->protocol == 1)
|
||
|
{
|
||
|
$contents = fread($this->socket, 4);
|
||
|
if (strlen($contents) == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - connection interrupted!', -32700);
|
||
|
}
|
||
|
$array_result = unpack('Vsize', $contents);
|
||
|
$size = $array_result['size'];
|
||
|
$recvhandle = $this->reqhandle;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$contents = fread($this->socket, 8);
|
||
|
if (strlen($contents) == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - connection interrupted!', -32700);
|
||
|
}
|
||
|
$array_result = unpack('Vsize/Vhandle', $contents);
|
||
|
$size = $array_result['size'];
|
||
|
$recvhandle = $array_result['handle'];
|
||
|
// -- amd64 support --
|
||
|
$bits = sprintf('%b', $recvhandle);
|
||
|
if (strlen($bits) == 64)
|
||
|
{
|
||
|
$recvhandle = bindec(substr($bits, 32));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($recvhandle == 0 || $size == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - connection interrupted!', -32700);
|
||
|
}
|
||
|
|
||
|
if ($size > SIZE_MAX)
|
||
|
{
|
||
|
throw new Exception("transport error - answer too big ($size)", -32700);
|
||
|
}
|
||
|
|
||
|
self::$received += $size;
|
||
|
|
||
|
$contents = '';
|
||
|
$contents_length = 0;
|
||
|
@stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (for successive reads until end)
|
||
|
while ($contents_length < $size)
|
||
|
{
|
||
|
$contents .= fread($this->socket, $size-$contents_length);
|
||
|
$contents_length = strlen($contents);
|
||
|
}
|
||
|
|
||
|
if (($recvhandle & 0x80000000) == 0)
|
||
|
{
|
||
|
// this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents
|
||
|
// just add it to the message list for the user to read
|
||
|
$new_cb_message = new Message($contents);
|
||
|
if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault')
|
||
|
{
|
||
|
array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
while ((int)$recvhandle != (int)$this->reqhandle);
|
||
|
|
||
|
$this->message = new Message($contents);
|
||
|
if (!$this->message->parse())
|
||
|
{
|
||
|
// XML error
|
||
|
throw new Exception('parse error. not well formed', -32700);
|
||
|
}
|
||
|
// Is the message a fault?
|
||
|
if ($this->message->messageType == 'fault')
|
||
|
{
|
||
|
throw new Exception($this->message->faultString, $this->message->faultCode);
|
||
|
}
|
||
|
|
||
|
return $this->message;
|
||
|
}
|
||
|
|
||
|
|
||
|
function query()
|
||
|
{
|
||
|
$args = func_get_args();
|
||
|
$method = array_shift($args);
|
||
|
|
||
|
if (!$this->socket || $this->protocol == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - Client not initialized', -32300);
|
||
|
}
|
||
|
|
||
|
$request = new Request($method, $args);
|
||
|
|
||
|
// Check if request is larger than 512 Kbytes
|
||
|
if ($request->getLength() > 512*1024-8)
|
||
|
{
|
||
|
throw new Exception('transport error - request too large!', -32700);
|
||
|
}
|
||
|
|
||
|
$this->sendRequest($request);
|
||
|
return $this->getResult();
|
||
|
}
|
||
|
|
||
|
// Non-blocking query method: doesn't read the response
|
||
|
function queryIgnoreResult()
|
||
|
{
|
||
|
$args = func_get_args();
|
||
|
$method = array_shift($args);
|
||
|
|
||
|
if (!$this->socket || $this->protocol == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - Client not initialized', -32300);
|
||
|
}
|
||
|
|
||
|
$request = new Request($method, $args);
|
||
|
|
||
|
// Check if the request is greater than 512 Kbytes to avoid errors
|
||
|
// If the method is system.multicall, make two calls (possibly recursively)
|
||
|
if ($request->getLength() > 512*1024-8)
|
||
|
{
|
||
|
if ($method == 'system.multicall' && isset($args[0]))
|
||
|
{
|
||
|
$count = count($args[0]);
|
||
|
// If count is 1, query cannot be reduced
|
||
|
if ($count < 2)
|
||
|
{
|
||
|
throw new Exception('transport error - request too large!', -32700);
|
||
|
}
|
||
|
$length = floor($count/2);
|
||
|
|
||
|
$args1 = array_slice($args[0], 0, $length);
|
||
|
$args2 = array_slice($args[0], $length, ($count-$length));
|
||
|
|
||
|
$res1 = $this->queryIgnoreResult('system.multicall', $args1);
|
||
|
$res2 = $this->queryIgnoreResult('system.multicall', $args2);
|
||
|
return ($res1 && $res2);
|
||
|
}
|
||
|
// If the method is not a multicall, just stop
|
||
|
else
|
||
|
{
|
||
|
throw new Exception('transport error - request too large!', -32700);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->sendRequest($request);
|
||
|
}
|
||
|
|
||
|
function getResponse()
|
||
|
{
|
||
|
// methodResponses can only have one param - return that
|
||
|
return $this->message->params[0];
|
||
|
}
|
||
|
|
||
|
function readCallbacks($timeout = 2000)
|
||
|
{
|
||
|
if (!$this->socket || $this->protocol == 0)
|
||
|
throw new Exception('transport error - Client not initialized', -32300);
|
||
|
if ($this->protocol == 1)
|
||
|
return false;
|
||
|
|
||
|
// flo: moved to end
|
||
|
//$something_received = count($this->cb_message)>0;
|
||
|
$contents = '';
|
||
|
$contents_length = 0;
|
||
|
|
||
|
@stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (to read available data)
|
||
|
// (assignment in arguments is forbidden since php 5.1.1)
|
||
|
$read = array($this->socket);
|
||
|
$write = NULL;
|
||
|
$except = NULL;
|
||
|
$nb = false;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
$nb = @stream_select($read, $write, $except, 0, $timeout);
|
||
|
}
|
||
|
catch (\Exception $e)
|
||
|
{
|
||
|
if (strpos($e->getMessage(), 'Invalid CRT') !== false)
|
||
|
{
|
||
|
$nb = true;
|
||
|
}
|
||
|
elseif (strpos($e->getMessage(), 'Interrupted system call') !== false)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// workaround for stream_select bug with amd64
|
||
|
if ($nb !== false)
|
||
|
{
|
||
|
$nb = count($read);
|
||
|
}
|
||
|
|
||
|
while ($nb !== false && $nb > 0)
|
||
|
{
|
||
|
$timeout = 0; // we don't want to wait for the full time again, just flush the available data
|
||
|
|
||
|
$size = 0;
|
||
|
$recvhandle = 0;
|
||
|
// Get result
|
||
|
$contents = fread($this->socket, 8);
|
||
|
if (strlen($contents) == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - connection interrupted!', -32700);
|
||
|
}
|
||
|
$array_result = unpack('Vsize/Vhandle', $contents);
|
||
|
$size = $array_result['size'];
|
||
|
$recvhandle = $array_result['handle'];
|
||
|
|
||
|
if ($recvhandle == 0 || $size == 0)
|
||
|
{
|
||
|
throw new Exception('transport error - connection interrupted!', -32700);
|
||
|
}
|
||
|
if ($size > SIZE_MAX)
|
||
|
{
|
||
|
throw new Exception("transport error - answer too big ($size)", -32700);
|
||
|
}
|
||
|
|
||
|
self::$received += $size;
|
||
|
|
||
|
$contents = '';
|
||
|
$contents_length = 0;
|
||
|
while ($contents_length < $size)
|
||
|
{
|
||
|
$contents .= fread($this->socket, $size-$contents_length);
|
||
|
$contents_length = strlen($contents);
|
||
|
}
|
||
|
|
||
|
if (($recvhandle & 0x80000000) == 0)
|
||
|
{
|
||
|
// this is a callback. handle= $recvhandle, xml-rpc= $contents
|
||
|
//echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF;
|
||
|
$new_cb_message = new Message($contents);
|
||
|
if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault')
|
||
|
{
|
||
|
array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params));
|
||
|
}
|
||
|
// flo: moved to end ...
|
||
|
// $something_received = true;
|
||
|
}
|
||
|
|
||
|
// (assignment in arguments is forbidden since php 5.1.1)
|
||
|
$read = array($this->socket);
|
||
|
$write = NULL;
|
||
|
$except = NULL;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
$nb = @stream_select($read, $write, $except, 0, $timeout);
|
||
|
}
|
||
|
catch (\Exception $e)
|
||
|
{
|
||
|
if (strpos($e->getMessage(), 'Invalid CRT') !== false)
|
||
|
{
|
||
|
$nb = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// workaround for stream_select bug with amd64
|
||
|
if ($nb !== false)
|
||
|
{
|
||
|
$nb = count($read);
|
||
|
}
|
||
|
}
|
||
|
return !empty($this->cb_message);
|
||
|
}
|
||
|
|
||
|
function getCallbackResponses()
|
||
|
{
|
||
|
// (look at the end of basic.php for an example)
|
||
|
$messages = $this->cb_message;
|
||
|
$this->cb_message = array();
|
||
|
return $messages;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
?>
|