From f626b9e8a079cfa2d12f9daaeea8d59e223841e6 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 20 Jul 2018 17:09:04 +0100 Subject: [PATCH] Initial mass migration to session handlers This introduces several new session handlers, splitting up session handling into several new states: - Login: Only allows handling the LoginPacket. This is the only time LoginPacket can be sent, and it'll be discarded when sent at any other time. - Resource packs: Handles only the resource packs sequence (downloading packs and such). This is the only time ResourcePackClientResponse and ResourcePackChunkRequest will be handled. - Pre-spawn: Only chunk radius requests are accepted during this state. SimpleNetworkHandler handles all the "rest" of the logic that hasn't yet been separated out into their own dedicated handlers. There's also a NullNetworkHandler which discards all packets while it's active. This solves a large number of issues with the security of the login sequence. It solves a range of possible DoS attacks and crashes, while also allowing great code simplification and cleanup. --- src/pocketmine/Player.php | 216 ++---------------- .../network/mcpe/NetworkSession.php | 31 ++- .../mcpe/handler/LoginSessionHandler.php | 89 ++++++++ .../mcpe/handler/NullSessionHandler.php | 31 +++ .../mcpe/handler/PreSpawnSessionHandler.php | 99 ++++++++ .../handler/ResourcePacksSessionHandler.php | 128 +++++++++++ .../mcpe/handler/SimpleSessionHandler.php | 15 -- .../network/mcpe/protocol/LoginPacket.php | 13 +- 8 files changed, 409 insertions(+), 213 deletions(-) create mode 100644 src/pocketmine/network/mcpe/handler/LoginSessionHandler.php create mode 100644 src/pocketmine/network/mcpe/handler/NullSessionHandler.php create mode 100644 src/pocketmine/network/mcpe/handler/PreSpawnSessionHandler.php create mode 100644 src/pocketmine/network/mcpe/handler/ResourcePacksSessionHandler.php diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index c71033b7c..f5cf35998 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -114,26 +114,16 @@ use pocketmine\network\mcpe\protocol\MobEffectPacket; use pocketmine\network\mcpe\protocol\MobEquipmentPacket; use pocketmine\network\mcpe\protocol\MovePlayerPacket; use pocketmine\network\mcpe\protocol\PlayerActionPacket; -use pocketmine\network\mcpe\protocol\PlayStatusPacket; -use pocketmine\network\mcpe\protocol\ProtocolInfo; -use pocketmine\network\mcpe\protocol\ResourcePackChunkDataPacket; -use pocketmine\network\mcpe\protocol\ResourcePackChunkRequestPacket; -use pocketmine\network\mcpe\protocol\ResourcePackClientResponsePacket; -use pocketmine\network\mcpe\protocol\ResourcePackDataInfoPacket; -use pocketmine\network\mcpe\protocol\ResourcePacksInfoPacket; -use pocketmine\network\mcpe\protocol\ResourcePackStackPacket; use pocketmine\network\mcpe\protocol\RespawnPacket; use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket; use pocketmine\network\mcpe\protocol\SetSpawnPositionPacket; use pocketmine\network\mcpe\protocol\SetTitlePacket; -use pocketmine\network\mcpe\protocol\StartGamePacket; use pocketmine\network\mcpe\protocol\TextPacket; use pocketmine\network\mcpe\protocol\TransferPacket; use pocketmine\network\mcpe\protocol\types\CommandData; use pocketmine\network\mcpe\protocol\types\CommandEnum; use pocketmine\network\mcpe\protocol\types\CommandParameter; use pocketmine\network\mcpe\protocol\types\ContainerIds; -use pocketmine\network\mcpe\protocol\types\DimensionIds; use pocketmine\network\mcpe\protocol\types\PlayerPermissions; use pocketmine\network\mcpe\protocol\UpdateAttributesPacket; use pocketmine\network\mcpe\protocol\UpdateBlockPacket; @@ -143,7 +133,6 @@ use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissionAttachment; use pocketmine\permission\PermissionAttachmentInfo; use pocketmine\plugin\Plugin; -use pocketmine\resourcepacks\ResourcePack; use pocketmine\tile\ItemFrame; use pocketmine\tile\Spawnable; use pocketmine\tile\Tile; @@ -182,9 +171,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ /** @var NetworkSession */ protected $networkSession; - /** @var int */ - protected $protocol = -1; - /** * @var int * Last measurement of player's latency in milliseconds. @@ -993,7 +979,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ protected function doFirstSpawn(){ $this->spawned = true; - $this->sendPlayStatus(PlayStatusPacket::PLAYER_SPAWN); + $this->networkSession->onSpawn(); if($this->hasPermission(Server::BROADCAST_CHANNEL_USERS)){ $this->server->getPluginManager()->subscribeToPermission(Server::BROADCAST_CHANNEL_USERS, $this); @@ -1799,59 +1785,17 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleLogin(LoginPacket $packet) : bool{ - if($this->loggedIn){ - return false; - } - - $this->protocol = $packet->protocol; - - if($packet->protocol !== ProtocolInfo::CURRENT_PROTOCOL){ - if($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL){ - $this->sendPlayStatus(PlayStatusPacket::LOGIN_FAILED_CLIENT, true); - }else{ - $this->sendPlayStatus(PlayStatusPacket::LOGIN_FAILED_SERVER, true); - } - - //This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client) - $this->close("", $this->server->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol ?? "unknown"]), false); - - return true; - } - - if(!self::isValidUserName($packet->username)){ - $this->close("", "disconnectionScreen.invalidName"); - - return true; - } - $this->username = TextFormat::clean($packet->username); $this->displayName = $this->username; $this->iusername = strtolower($this->username); - - if($packet->locale !== null){ - $this->locale = $packet->locale; - } - + $this->locale = $packet->locale; $this->randomClientId = $packet->clientId; $this->uuid = UUID::fromString($packet->clientUUID); $this->rawUUID = $this->uuid->toBinary(); - $skin = new Skin( - $packet->clientData["SkinId"], - base64_decode($packet->clientData["SkinData"] ?? ""), - base64_decode($packet->clientData["CapeData"] ?? ""), - $packet->clientData["SkinGeometryName"] ?? "", - base64_decode($packet->clientData["SkinGeometry"] ?? "") - ); + $this->setSkin($packet->skin); - if(!$skin->isValid()){ - $this->close("", "disconnectionScreen.invalidSkin"); - - return true; - } - - $this->setSkin($skin); $this->server->getPluginManager()->callEvent($ev = new PlayerPreLoginEvent($this, "Plugin reason")); if($ev->isCancelled()){ @@ -1884,13 +1828,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return true; } - public function sendPlayStatus(int $status, bool $immediate = false){ - $pk = new PlayStatusPacket(); - $pk->status = $status; - $pk->protocol = $this->protocol; - $this->sendDataPacket($pk, $immediate); - } - public function onVerifyCompleted(LoginPacket $packet, ?string $error, bool $signedByMojang) : void{ if($this->closed){ return; @@ -1935,64 +1872,12 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } } - $this->sendPlayStatus(PlayStatusPacket::LOGIN_SUCCESS); - $this->loggedIn = true; $this->server->onPlayerLogin($this); - - $pk = new ResourcePacksInfoPacket(); - $manager = $this->server->getResourcePackManager(); - $pk->resourcePackEntries = $manager->getResourceStack(); - $pk->mustAccept = $manager->resourcePacksRequired(); - $this->dataPacket($pk); + $this->networkSession->onLoginSuccess(); } - public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ - switch($packet->status){ - case ResourcePackClientResponsePacket::STATUS_REFUSED: - //TODO: add lang strings for this - $this->close("", "You must accept resource packs to join this server.", true); - break; - case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: - $manager = $this->server->getResourcePackManager(); - foreach($packet->packIds as $uuid){ - $pack = $manager->getPackById($uuid); - if(!($pack instanceof ResourcePack)){ - //Client requested a resource pack but we don't have it available on the server - $this->close("", "disconnectionScreen.resourcePack", true); - $this->server->getLogger()->debug("Got a resource pack request for unknown pack with UUID " . $uuid . ", available packs: " . implode(", ", $manager->getPackIdList())); - - return false; - } - - $pk = new ResourcePackDataInfoPacket(); - $pk->packId = $pack->getPackId(); - $pk->maxChunkSize = 1048576; //1MB - $pk->chunkCount = (int) ceil($pack->getPackSize() / $pk->maxChunkSize); - $pk->compressedPackSize = $pack->getPackSize(); - $pk->sha256 = $pack->getSha256(); - $this->dataPacket($pk); - } - - break; - case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: - $pk = new ResourcePackStackPacket(); - $manager = $this->server->getResourcePackManager(); - $pk->resourcePackStack = $manager->getResourceStack(); - $pk->mustAccept = $manager->resourcePacksRequired(); - $this->dataPacket($pk); - break; - case ResourcePackClientResponsePacket::STATUS_COMPLETED: - $this->_actuallyConstruct(); - break; - default: - return false; - } - - return true; - } - - protected function _actuallyConstruct(){ + public function _actuallyConstruct(){ $namedtag = $this->server->getOfflinePlayerData($this->username); //TODO: make this async if(($level = $this->server->getLevelByName($namedtag->getString("Level", "", true))) === null){ @@ -2021,50 +1906,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return; } - $spawnPosition = $this->getSpawn(); - - $pk = new StartGamePacket(); - $pk->entityUniqueId = $this->id; - $pk->entityRuntimeId = $this->id; - $pk->playerGamemode = Player::getClientFriendlyGamemode($this->gamemode); - - $pk->playerPosition = $this->getOffsetPosition($this); - - $pk->pitch = $this->pitch; - $pk->yaw = $this->yaw; - $pk->seed = -1; - $pk->dimension = DimensionIds::OVERWORLD; //TODO: implement this properly - $pk->worldGamemode = Player::getClientFriendlyGamemode($this->server->getGamemode()); - $pk->difficulty = $this->level->getDifficulty(); - $pk->spawnX = $spawnPosition->getFloorX(); - $pk->spawnY = $spawnPosition->getFloorY(); - $pk->spawnZ = $spawnPosition->getFloorZ(); - $pk->hasAchievementsDisabled = true; - $pk->time = $this->level->getTime(); - $pk->eduMode = false; - $pk->rainLevel = 0; //TODO: implement these properly - $pk->lightningLevel = 0; - $pk->commandsEnabled = true; - $pk->levelId = ""; - $pk->worldName = $this->server->getMotd(); - $this->dataPacket($pk); - - $this->level->sendTime($this); - - $this->sendAttributes(true); - $this->sendCommandData(); - $this->sendSettings(); - $this->sendPotionEffects($this); - $this->sendData($this); - - $this->sendAllInventories(); - $this->inventory->sendCreativeContents(); - $this->inventory->sendHeldItem($this); - $this->networkSession->getInterface()->putPacket($this, $this->server->getCraftingManager()->getCraftingDataPacket()); - - $this->server->addOnlinePlayer($this); - $this->server->sendFullPlayerListData($this); - $this->server->getLogger()->info($this->getServer()->getLanguage()->translateString("pocketmine.player.logIn", [ TextFormat::AQUA . $this->username . TextFormat::WHITE, $this->networkSession->getIp(), @@ -2075,6 +1916,8 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ round($this->y, 4), round($this->z, 4) ])); + + $this->server->addOnlinePlayer($this); } protected function initHumanData() : void{ @@ -2128,7 +1971,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ * @return bool */ public function chat(string $message) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return false; } @@ -2172,7 +2015,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ $this->sendPosition($this, null, null, MovePlayerPacket::MODE_RESET); $this->server->getLogger()->debug("Got outdated pre-teleport movement from " . $this->getName() . ", received " . $newPos . ", expected " . $this->asVector3()); //Still getting movements from before teleport, ignore them - }elseif((!$this->isAlive() or !$this->spawned) and $newPos->distanceSquared($this) > 0.01){ + }elseif((!$this->isAlive()) and $newPos->distanceSquared($this) > 0.01){ $this->sendPosition($this, null, null, MovePlayerPacket::MODE_RESET); $this->server->getLogger()->debug("Reverted movement of " . $this->getName() . " due to not alive or not spawned, received " . $newPos . ", locked at " . $this->asVector3()); }else{ @@ -2204,7 +2047,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleEntityEvent(EntityEventPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return true; } $this->doCloseInventory(); @@ -2232,7 +2075,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ * @return bool */ public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return false; } @@ -2594,7 +2437,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleMobEquipment(MobEquipmentPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return true; } @@ -2644,7 +2487,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handlePlayerAction(PlayerActionPacket $packet) : bool{ - if(!$this->spawned or (!$this->isAlive() and $packet->action !== PlayerActionPacket::ACTION_RESPAWN and $packet->action !== PlayerActionPacket::ACTION_DIMENSION_CHANGE_REQUEST)){ + if(!$this->isAlive() and $packet->action !== PlayerActionPacket::ACTION_RESPAWN and $packet->action !== PlayerActionPacket::ACTION_DIMENSION_CHANGE_REQUEST){ return true; } @@ -2668,7 +2511,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ $this->stopSleep(); break; case PlayerActionPacket::ACTION_RESPAWN: - if(!$this->spawned or $this->isAlive() or !$this->isOnline()){ + if($this->isAlive() or !$this->isOnline()){ break; } @@ -2781,7 +2624,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleAnimate(AnimatePacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return true; } @@ -2805,7 +2648,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ * @return bool if the item was dropped or if the item was null */ public function dropItem(Item $item) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return false; } @@ -2854,7 +2697,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleBlockEntityData(BlockEntityDataPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return true; } $this->doCloseInventory(); @@ -2890,7 +2733,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } public function handleItemFrameDropItem(ItemFrameDropItemPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ + if(!$this->isAlive()){ return true; } @@ -2917,25 +2760,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return true; } - public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ - $manager = $this->server->getResourcePackManager(); - $pack = $manager->getPackById($packet->packId); - if(!($pack instanceof ResourcePack)){ - $this->close("", "disconnectionScreen.resourcePack", true); - $this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $packet->packId . ", available packs: " . implode(", ", $manager->getPackIdList())); - - return false; - } - - $pk = new ResourcePackChunkDataPacket(); - $pk->packId = $pack->getPackId(); - $pk->chunkIndex = $packet->chunkIndex; - $pk->data = $pack->getPackChunk(1048576 * $packet->chunkIndex, 1048576); - $pk->progress = (1048576 * $packet->chunkIndex); - $this->dataPacket($pk); - return true; - } - public function handleBookEdit(BookEditPacket $packet) : bool{ /** @var WritableBook $oldBook */ $oldBook = $this->inventory->getItem($packet->inventorySlot); @@ -3732,7 +3556,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ * @return bool */ public function doCloseWindow(int $windowId) : bool{ - if(!$this->spawned or $windowId === 0){ + if($windowId === 0){ return false; } @@ -3844,7 +3668,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } } - protected function sendAllInventories(){ + public function sendAllInventories(){ foreach($this->windowIndex as $id => $inventory){ $inventory->sendContents($this); } diff --git a/src/pocketmine/network/mcpe/NetworkSession.php b/src/pocketmine/network/mcpe/NetworkSession.php index 6cd32b683..a0ec1bf97 100644 --- a/src/pocketmine/network/mcpe/NetworkSession.php +++ b/src/pocketmine/network/mcpe/NetworkSession.php @@ -25,11 +25,15 @@ namespace pocketmine\network\mcpe; use pocketmine\event\server\DataPacketReceiveEvent; use pocketmine\event\server\DataPacketSendEvent; +use pocketmine\network\mcpe\handler\LoginSessionHandler; +use pocketmine\network\mcpe\handler\PreSpawnSessionHandler; +use pocketmine\network\mcpe\handler\ResourcePacksSessionHandler; use pocketmine\network\mcpe\handler\SessionHandler; use pocketmine\network\mcpe\handler\SimpleSessionHandler; use pocketmine\network\mcpe\protocol\DataPacket; use pocketmine\network\mcpe\protocol\DisconnectPacket; use pocketmine\network\mcpe\protocol\PacketPool; +use pocketmine\network\mcpe\protocol\PlayStatusPacket; use pocketmine\network\NetworkInterface; use pocketmine\Player; use pocketmine\Server; @@ -60,7 +64,7 @@ class NetworkSession{ $this->ip = $ip; $this->port = $port; - $this->setHandler(new SimpleSessionHandler($player)); + $this->setHandler(new LoginSessionHandler($player, $this)); } public function getInterface() : NetworkInterface{ @@ -144,4 +148,29 @@ class NetworkSession{ } $this->interface->close($this->player, $notify ? $reason : ""); } + + //TODO: onEnableEncryption() step + + public function onLoginSuccess() : void{ + $pk = new PlayStatusPacket(); + $pk->status = PlayStatusPacket::LOGIN_SUCCESS; + $this->sendDataPacket($pk); + + $this->setHandler(new ResourcePacksSessionHandler($this->server, $this->player, $this)); + } + + public function onResourcePacksDone() : void{ + $this->player->_actuallyConstruct(); + + $this->setHandler(new PreSpawnSessionHandler($this->server, $this->player, $this)); + } + + public function onSpawn() : void{ + $pk = new PlayStatusPacket(); + $pk->status = PlayStatusPacket::PLAYER_SPAWN; + $this->sendDataPacket($pk); + + //TODO: split this up even further + $this->setHandler(new SimpleSessionHandler($this->player)); + } } diff --git a/src/pocketmine/network/mcpe/handler/LoginSessionHandler.php b/src/pocketmine/network/mcpe/handler/LoginSessionHandler.php new file mode 100644 index 000000000..a65854f49 --- /dev/null +++ b/src/pocketmine/network/mcpe/handler/LoginSessionHandler.php @@ -0,0 +1,89 @@ +player = $player; + $this->session = $session; + } + + public function handleLogin(LoginPacket $packet) : bool{ + if(!$this->isCompatibleProtocol($packet->protocol)){ + $pk = new PlayStatusPacket(); + $pk->status = $packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? + PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER; + $pk->protocol = $packet->protocol; + $this->session->sendDataPacket($pk, true); + + //This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client) + $this->player->close( + "", + $this->player->getServer()->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]), + false + ); + + return true; + } + + if(!Player::isValidUserName($packet->username)){ + $this->player->close("", "disconnectionScreen.invalidName"); + + return true; + } + + if($packet->skin === null or !$packet->skin->isValid()){ + $this->player->close("", "disconnectionScreen.invalidSkin"); + + return true; + } + + if($this->player->handleLogin($packet)){ + if($this->session->getHandler() === $this){ //when login verification is disabled, the handler will already have been replaced + $this->session->setHandler(new NullSessionHandler()); //drop packets received during login verification + } + return true; + } + return false; + } + + protected function isCompatibleProtocol(int $protocolVersion) : bool{ + return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL; + } +} diff --git a/src/pocketmine/network/mcpe/handler/NullSessionHandler.php b/src/pocketmine/network/mcpe/handler/NullSessionHandler.php new file mode 100644 index 000000000..a36a3989d --- /dev/null +++ b/src/pocketmine/network/mcpe/handler/NullSessionHandler.php @@ -0,0 +1,31 @@ +player = $player; + $this->server = $server; + $this->session = $session; + } + + public function setUp() : void{ + $spawnPosition = $this->player->getSpawn(); + + $pk = new StartGamePacket(); + $pk->entityUniqueId = $this->player->getId(); + $pk->entityRuntimeId = $this->player->getId(); + $pk->playerGamemode = Player::getClientFriendlyGamemode($this->player->getGamemode()); + $pk->playerPosition = $this->player->getOffsetPosition($this->player); + $pk->pitch = $this->player->pitch; + $pk->yaw = $this->player->yaw; + $pk->seed = -1; + $pk->dimension = DimensionIds::OVERWORLD; //TODO: implement this properly + $pk->worldGamemode = Player::getClientFriendlyGamemode($this->server->getGamemode()); + $pk->difficulty = $this->player->getLevel()->getDifficulty(); + $pk->spawnX = $spawnPosition->getFloorX(); + $pk->spawnY = $spawnPosition->getFloorY(); + $pk->spawnZ = $spawnPosition->getFloorZ(); + $pk->hasAchievementsDisabled = true; + $pk->time = $this->player->getLevel()->getTime(); + $pk->eduMode = false; + $pk->rainLevel = 0; //TODO: implement these properly + $pk->lightningLevel = 0; + $pk->commandsEnabled = true; + $pk->levelId = ""; + $pk->worldName = $this->server->getMotd(); + $this->session->sendDataPacket($pk); + + $this->player->getLevel()->sendTime($this->player); + + $this->player->sendAttributes(true); + $this->player->sendCommandData(); + $this->player->sendSettings(); + $this->player->sendPotionEffects($this->player); + $this->player->sendData($this->player); + + $this->player->sendAllInventories(); + $this->player->getInventory()->sendCreativeContents(); + $this->player->getInventory()->sendHeldItem($this->player); + $this->session->getInterface()->putPacket($this->player, $this->server->getCraftingManager()->getCraftingDataPacket()); + + $this->server->sendFullPlayerListData($this->player); + } + + public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{ + $this->player->setViewDistance($packet->radius); + + return true; + } +} diff --git a/src/pocketmine/network/mcpe/handler/ResourcePacksSessionHandler.php b/src/pocketmine/network/mcpe/handler/ResourcePacksSessionHandler.php new file mode 100644 index 000000000..cfb50ed9d --- /dev/null +++ b/src/pocketmine/network/mcpe/handler/ResourcePacksSessionHandler.php @@ -0,0 +1,128 @@ +server = $server; + $this->player = $player; + $this->session = $session; + } + + public function setUp() : void{ + $pk = new ResourcePacksInfoPacket(); + $manager = $this->server->getResourcePackManager(); + $pk->resourcePackEntries = $manager->getResourceStack(); + $pk->mustAccept = $manager->resourcePacksRequired(); + $this->session->sendDataPacket($pk); + } + + public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ + switch($packet->status){ + case ResourcePackClientResponsePacket::STATUS_REFUSED: + //TODO: add lang strings for this + $this->player->close("", "You must accept resource packs to join this server.", true); + break; + case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: + $manager = $this->server->getResourcePackManager(); + foreach($packet->packIds as $uuid){ + $pack = $manager->getPackById($uuid); + if(!($pack instanceof ResourcePack)){ + //Client requested a resource pack but we don't have it available on the server + $this->player->close("", "disconnectionScreen.resourcePack", true); + $this->server->getLogger()->debug("Got a resource pack request for unknown pack with UUID " . $uuid . ", available packs: " . implode(", ", $manager->getPackIdList())); + + return false; + } + + $pk = new ResourcePackDataInfoPacket(); + $pk->packId = $pack->getPackId(); + $pk->maxChunkSize = 1048576; //1MB + $pk->chunkCount = (int) ceil($pack->getPackSize() / $pk->maxChunkSize); + $pk->compressedPackSize = $pack->getPackSize(); + $pk->sha256 = $pack->getSha256(); + $this->session->sendDataPacket($pk); + } + + break; + case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: + $pk = new ResourcePackStackPacket(); + $manager = $this->server->getResourcePackManager(); + $pk->resourcePackStack = $manager->getResourceStack(); + $pk->mustAccept = $manager->resourcePacksRequired(); + $this->session->sendDataPacket($pk); + break; + case ResourcePackClientResponsePacket::STATUS_COMPLETED: + $this->session->onResourcePacksDone(); + break; + default: + return false; + } + + return true; + } + + public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ + $manager = $this->server->getResourcePackManager(); + $pack = $manager->getPackById($packet->packId); + if(!($pack instanceof ResourcePack)){ + $this->player->close("", "disconnectionScreen.resourcePack", true); + $this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $packet->packId . ", available packs: " . implode(", ", $manager->getPackIdList())); + + return false; + } + + $pk = new ResourcePackChunkDataPacket(); + $pk->packId = $pack->getPackId(); + $pk->chunkIndex = $packet->chunkIndex; + $pk->data = $pack->getPackChunk(1048576 * $packet->chunkIndex, 1048576); + $pk->progress = (1048576 * $packet->chunkIndex); + $this->session->sendDataPacket($pk); + + return true; + } +} diff --git a/src/pocketmine/network/mcpe/handler/SimpleSessionHandler.php b/src/pocketmine/network/mcpe/handler/SimpleSessionHandler.php index b640192bb..7bad4b51e 100644 --- a/src/pocketmine/network/mcpe/handler/SimpleSessionHandler.php +++ b/src/pocketmine/network/mcpe/handler/SimpleSessionHandler.php @@ -42,7 +42,6 @@ use pocketmine\network\mcpe\protocol\InventoryTransactionPacket; use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket; use pocketmine\network\mcpe\protocol\LabTablePacket; use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\network\mcpe\protocol\MapInfoRequestPacket; use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket; use pocketmine\network\mcpe\protocol\MobEquipmentPacket; @@ -53,8 +52,6 @@ use pocketmine\network\mcpe\protocol\PlayerHotbarPacket; use pocketmine\network\mcpe\protocol\PlayerInputPacket; use pocketmine\network\mcpe\protocol\PlayerSkinPacket; use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket; -use pocketmine\network\mcpe\protocol\ResourcePackChunkRequestPacket; -use pocketmine\network\mcpe\protocol\ResourcePackClientResponsePacket; use pocketmine\network\mcpe\protocol\ServerSettingsRequestPacket; use pocketmine\network\mcpe\protocol\SetLocalPlayerAsInitializedPacket; use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket; @@ -77,18 +74,10 @@ class SimpleSessionHandler extends SessionHandler{ $this->player = $player; } - public function handleLogin(LoginPacket $packet) : bool{ - return $this->player->handleLogin($packet); - } - public function handleClientToServerHandshake(ClientToServerHandshakePacket $packet) : bool{ return false; //TODO } - public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ - return $this->player->handleResourcePackClientResponse($packet); - } - public function handleText(TextPacket $packet) : bool{ if($packet->type === TextPacket::TYPE_CHAT){ return $this->player->chat($packet->message); @@ -207,10 +196,6 @@ class SimpleSessionHandler extends SessionHandler{ return false; //TODO } - public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ - return $this->player->handleResourcePackChunkRequest($packet); - } - public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{ return $this->player->changeSkin($packet->skin, $packet->newSkinName, $packet->oldSkinName); } diff --git a/src/pocketmine/network/mcpe/protocol/LoginPacket.php b/src/pocketmine/network/mcpe/protocol/LoginPacket.php index af971f952..d6aaff9eb 100644 --- a/src/pocketmine/network/mcpe/protocol/LoginPacket.php +++ b/src/pocketmine/network/mcpe/protocol/LoginPacket.php @@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol; #include +use pocketmine\entity\Skin; use pocketmine\network\mcpe\handler\SessionHandler; use pocketmine\utils\BinaryStream; use pocketmine\utils\MainLogger; @@ -52,6 +53,8 @@ class LoginPacket extends DataPacket{ public $serverAddress; /** @var string */ public $locale; + /** @var Skin|null */ + public $skin; /** @var array (the "chain" index contains one or more JWTs) */ public $chainData = []; @@ -136,7 +139,15 @@ class LoginPacket extends DataPacket{ $this->clientId = $this->clientData["ClientRandomId"] ?? null; $this->serverAddress = $this->clientData["ServerAddress"] ?? null; - $this->locale = $this->clientData["LanguageCode"] ?? null; + $this->locale = $this->clientData["LanguageCode"] ?? "en_US"; + + $this->skin = new Skin( + $this->clientData["SkinId"] ?? "", + base64_decode($this->clientData["SkinData"] ?? ""), + base64_decode($this->clientData["CapeData"] ?? ""), + $this->clientData["SkinGeometryName"] ?? "", + base64_decode($this->clientData["SkinGeometry"] ?? "") + ); } protected function encodePayload() : void{