From 1ce23ab8d757ea0382e46bb2eebc32fea73107ff Mon Sep 17 00:00:00 2001 From: Shoghi Cervantes Date: Sat, 8 Jun 2013 00:41:34 +0200 Subject: [PATCH] Added DEFLATEPacket protocol support https://gist.github.com/shoghicp/5729825 --- src/PocketMinecraftServer.php | 2 +- src/config.php | 2 +- src/network/MinecraftInterface.php | 183 ++++++++++++++++++-- src/network/Packet.php | 22 +++ src/{constants => network}/ProtocolInfo.php | 7 + src/world/LevelImport.php | 2 +- 6 files changed, 198 insertions(+), 20 deletions(-) rename src/{constants => network}/ProtocolInfo.php (98%) diff --git a/src/PocketMinecraftServer.php b/src/PocketMinecraftServer.php index 6aedbbdfc8..61635a30f7 100644 --- a/src/PocketMinecraftServer.php +++ b/src/PocketMinecraftServer.php @@ -68,7 +68,7 @@ class PocketMinecraftServer{ $this->saveEnabled = true; $this->tickMeasure = array_fill(0, 40, 0); $this->setType("normal"); - $this->interface = new MinecraftInterface("255.255.255.255", $this->port, true, false, $this->serverip); + $this->interface = new MinecraftInterface($this, "255.255.255.255", $this->port, true, false, $this->serverip); $this->reloadConfig(); $this->stop = false; $this->ticks = 0; diff --git a/src/config.php b/src/config.php index cf71e67163..6a4da3ccbc 100644 --- a/src/config.php +++ b/src/config.php @@ -66,7 +66,7 @@ ini_set("memory_limit", "128M"); //Default define("LOG", true); define("START_TIME", microtime(true)); define("MAJOR_VERSION", "Alpha_1.3.1dev"); -define("CURRENT_MINECRAFT_VERSION", "0.7.0 alpha"); +define("CURRENT_MINECRAFT_VERSION", "0.7.1 alpha"); define("CURRENT_API_VERSION", 8); define("CURRENT_PHP_VERSION", "5.5"); $gitsha1 = false; diff --git a/src/network/MinecraftInterface.php b/src/network/MinecraftInterface.php index c33668ac71..84574ad0c7 100644 --- a/src/network/MinecraftInterface.php +++ b/src/network/MinecraftInterface.php @@ -29,7 +29,10 @@ class MinecraftInterface{ public $client; private $socket; private $data; - function __construct($server, $port = 25565, $listen = false, $client = false, $serverip = "0.0.0.0"){ + private $chunked; + private $toChunk; + private $needCheck; + function __construct($object, $server, $port = 25565, $listen = false, $client = false, $serverip = "0.0.0.0"){ $this->socket = new UDPSocket($server, $port, (bool) $listen, $serverip); if($this->socket->connected === false){ console("[ERROR] Couldn't bind to $serverip:".$port, true, true, 0); @@ -37,6 +40,10 @@ class MinecraftInterface{ } $this->client = (bool) $client; $this->start = microtime(true); + $this->chunked = array(); + $this->toChunk = array(); + $this->needCheck = array(); + $object->schedule(1, array($this, "checkChunked"), array(), true); } public function close(){ @@ -71,20 +78,22 @@ class MinecraftInterface{ } public function readPacket(){ - $p = $this->popPacket(); - if($p !== false){ - return $p; - } + $pk = $this->popPacket(); if($this->socket->connected === false){ - return false; + return $pk; } $buf = ""; $source = false; $port = 1; $len = $this->socket->read($buf, $source, $port); if($len === false){ - return false; + return $pk; } + $this->parsePacket($buf, $source, $port); + return ($pk !== false ? $pk : $this->popPacket()); + } + + private function parsePacket($buf, $source, $port){ $pid = ord($buf{0}); $struct = $this->getStruct($pid); if($struct === false){ @@ -96,7 +105,7 @@ class MinecraftInterface{ "port" => $port )) !== true){ if(LOG === true and DEBUG >= 3){ - console("[ERROR] Unknown Packet ID 0x".Utils::strToHex(chr($pid)), true, true, 0); + console("[ERROR] Unknown Packet ID 0x".Utils::strToHex(chr($pid)), true, true, 2); $p = "[".(microtime(true) - $this->start)."] [CLIENT->SERVER ".$source.":".$port."]: Error, bad packet id 0x".Utils::strToHex(chr($pid))." [length ".strlen($buf)."]".PHP_EOL; $p .= Utils::hexdump($buf); $p .= PHP_EOL; @@ -108,8 +117,132 @@ class MinecraftInterface{ $packet = new Packet($pid, $struct, $buf); @$packet->parse(); + if($pid === 0x99){ + $CID = PocketMinecraftServer::clientID($source, $port); + if(!isset($this->chunked[$CID]) and $packet->data[0] !== 0){ //Drop packet + return $this->popPacket(); + } + switch($packet->data[0]){ + case 0: + $this->initChunked($CID, $source, $port); + return false; + case 1: + $this->stopChunked($CID); + return false; + case 3: + $this->ackChunked($CID, $data[1]["id"], $data[1]["index"]); + return false; + case 4: + $this->receiveChunked($CID, $data[1]["id"], $data[1]["index"], $data[1]["count"], $data[1]["data"]); + return true; + } + } $this->data[] = array($pid, $packet->data, $buf, $source, $port); - return $this->popPacket(); + return true; + } + + public function checkChunked($CID){ + $time = microtime(true); + foreach($this->needCheck as $CID => $packets){ + if($packets[-1] < $time){ + $d = $this->chunked[$CID]; + unset($packets[-1]); + foreach($packets as $packet){ + $this->writePacket(0x99, $packet, true, $d[1], $d[2], true); + } + $this->needCheck[$CID][-1] = $time + 5; + } + } + foreach($this->toChunk as $CID => $packets){ + $d = $this->chunked[$CID]; + $raw = ""; + $MTU = 0; + foreach($packets as $packet){ + $raw .= $packet; + if(($len = strlen($packet)) > $MTU){ + $MTU = $len; + } + } + if($MTU > $d[0][2]){ + $this->chunked[$CID][0][2] = $MTU; + }else{ + $MTU = $d[0][2]; + } + $raw = str_split(gzdeflate($raw, DEFLATEPACKET_LEVEL), $MTU - 9); // - 1 - 2 - 2 - 2 - 2 + $count = count($raw); + $messageID = $this->chunked[$CID][0][0]++; + if(!isset($this->needCheck[$CID])){ + $this->needCheck[$CID] = array(); + } + $this->needCheck[$CID][$messageID] = array(-1 => $time + 1); + foreach($raw as $index => $r){ + $p = "\x99\x02".Utils::writeShort($messageID).Utils::writeShort($index).Utils::writeShort($count).Utils::writeShort(strlen($r)).$r; + $this->needCheck[$CID][$messageID][$index] = $p; + $this->writePacket(0x99, $p, true, $d[1], $d[2], true); + } + unset($this->toChunk[$CID]); + } + } + + public function isChunked($CID){ + return isset($this->chunked[$CID]); + } + + private function initChunked($CID, $source, $port){ + console("[DEBUG] Starting DEFLATEPacket for $source:$port", true, true, 2); + $this->chunked[$CID] = array( + 0 => array(0, 0, 0), //index, sent/received; MTU + 1 => $source, + 2 => $port, + 3 => array(), //Received packets + ); + $this->writePacket(0x99, array( + 0 => 0, //start + ), false, $source, $port); + } + + public function stopChunked($CID){ + if(!isset($this->chunked[$CID])){ + return false; + } + $this->writePacket(0x99, array( + 0 => 1, //stop + ), false, $this->chunked[$CID][1], $this->chunked[$CID][2]); + console("[DEBUG] Stopping DEFLATEPacket for ".$this->chunked[$CID][1].":".$this->chunked[$CID][2], true, true, 2); + $this->chunked[$CID][3] = null; + $this->chunked[$CID][4] = null; + unset($this->chunked[$CID]); + unset($this->toChunk[$CID]); + unset($this->needCheck[$CID]); + } + + private function ackChunked($CID, $ID, $index){ + unset($this->needCheck[$CID][$ID][$index]); + if(count($this->needCheck[$CID][$ID]) <= 1){ + unset($this->needCheck[$CID][$ID]); + } + } + + private function receiveChunked($CID, $ID, $index, $count, $data){ + if(!isset($this->chunked[$CID][3][$ID])){ + $this->chunked[$CID][3][$ID] = array(); + } + $this->chunked[$CID][3][$ID][$index] = $data; + + if(count($this->chunked[$CID][3][$ID]) === $count){ + ksort($this->chunked[$CID][3][$ID]); + $data = gzinflate(implode($this->chunked[$CID][3][$ID])); + unset($this->chunked[$CID][3][$ID]); + if($data === false or strlen($data) === 0){ + console("[ERROR] Invalid DEFLATEPacket for ".$this->chunked[$CID][1].":".$this->chunked[$CID][2], true, true, 2); + } + $offset = 0; + while(($plen = Utils::readShort(substr($data, $offset, 2), false)) !== 0xFFFF or $offset >= $len){ + $offset += 2; + $packet = substr($data, $offset, $plen); + $this->parsePacket($packet, $this->chunked[$CID][1], $this->chunked[$CID][2]); + } + } } public function popPacket(){ @@ -131,17 +264,33 @@ class MinecraftInterface{ return false; } - public function writePacket($pid, $data = array(), $raw = false, $dest = false, $port = false){ - $struct = $this->getStruct($pid); + public function writePacket($pid, $data = array(), $raw = false, $dest = false, $port = false, $force = false){ + $CID = PocketMinecraftServer::clientID($dest, $port); if($raw === false){ - $packet = new Packet($pid, $struct); + $packet = new Packet($pid, $this->getStruct($pid)); $packet->data = $data; - @$packet->create(); - $write = $this->socket->write($packet->raw, $dest, $port); - $this->writeDump($pid, $packet->raw, $data, "client", $dest, $port); + @$packet->create(); + if($force === false and $this->isChunked($CID)){ + if(!isset($this->toChunk[$CID])){ + $this->toChunk[$CID] = array(); + } + $this->toChunk[$CID][] = $packet->raw; + $write = strlen($packet->raw); + }else{ + $write = $this->socket->write($packet->raw, $dest, $port); + $this->writeDump($pid, $packet->raw, $data, "client", $dest, $port); + } }else{ - $write = $this->socket->write($data, $dest, $port); - $this->writeDump($pid, $data, false, "client", $dest, $port); + if($force === false and $this->isChunked($CID)){ + if(!isset($this->toChunk[$CID])){ + $this->toChunk[$CID] = array(); + } + $this->toChunk[$CID][] = $dest; + $write = strlen($packet->raw); + }else{ + $write = $this->socket->write($data, $dest, $port); + $this->writeDump($pid, $data, false, "client", $dest, $port); + } } return $write; } diff --git a/src/network/Packet.php b/src/network/Packet.php index 35f9945b8d..3423c38194 100644 --- a/src/network/Packet.php +++ b/src/network/Packet.php @@ -58,6 +58,16 @@ class Packet{ switch($type){ case "special1": switch($this->pid){ + case 0x99: + if($this->data[0] >= 2){ + $this->addRaw(Utils::writeShort($this->data[1]["id"])); + $this->addRaw(Utils::writeShort($this->data[1]["index"])); + if($this->data[0] === 2){ + $this->addRaw(Utils::writeShort($this->data[1]["count"])); + $this->addRaw(Utils::writeShort(strlen($this->data[1]["data"])).$this->data[1]["data"]); + } + } + break; case 0xc0: case 0xa0: $payload = ""; @@ -188,6 +198,18 @@ class Packet{ switch($type){ case "special1": switch($this->pid){ + case 0x99: + if($this->data[0] >= 2){ // + $messageID = Utils::readShort($this->get(2), false); + $messageIndex = Utils::readShort($this->get(2), false); + $this->data[1] = array("id" => $messageID, "index" => $messageIndex); + if($this->data[0] === 2){ + $this->data[1]["count"] = Utils::readShort($this->get(2), false); + $dataLength = Utils::readShort($this->get(2), false); + $this->data[1]["data"] = $this->get($dataLength); + } + } + break; case 0xc0: case 0xa0: $cnt = Utils::readShort($this->get(2), false); diff --git a/src/constants/ProtocolInfo.php b/src/network/ProtocolInfo.php similarity index 98% rename from src/constants/ProtocolInfo.php rename to src/network/ProtocolInfo.php index 12e225ce20..a32ebdd5b4 100644 --- a/src/constants/ProtocolInfo.php +++ b/src/network/ProtocolInfo.php @@ -26,6 +26,8 @@ the Free Software Foundation, either version 3 of the License, or */ +define("DEFLATEPACKET_LEVEL", 1); + define("CURRENT_STRUCTURE", 5); define("CURRENT_PROTOCOL", 11); @@ -321,6 +323,11 @@ class Protocol{ "ubyte", "customData", ), + + 0x99 => array( + "byte", + "special1", + ), 0xa0 => array( "special1", diff --git a/src/world/LevelImport.php b/src/world/LevelImport.php index 1e1c109b75..69aed2d9e2 100644 --- a/src/world/LevelImport.php +++ b/src/world/LevelImport.php @@ -70,7 +70,7 @@ class LevelImport{ "spawnZ" => $level["SpawnZ"], "extra" => "", "width" => 16, - "height" => 8 + "height" => 8 )); $chunks = new PocketChunkParser(); $chunks->loadFile($this->path."chunks.dat");