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.
This commit is contained in:
Dylan K. Taylor
2018-07-20 17:09:04 +01:00
parent 97a1483f75
commit f626b9e8a0
8 changed files with 409 additions and 213 deletions

View File

@ -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));
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\Player;
/**
* Handles the initial login phase of the session. This handler is used as the initial state.
*/
class LoginSessionHandler extends SessionHandler{
/** @var Player */
private $player;
/** @var NetworkSession */
private $session;
public function __construct(Player $player, NetworkSession $session){
$this->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;
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
/**
* Handler which simply ignores all packets received.
*/
class NullSessionHandler extends SessionHandler{
}

View File

@ -0,0 +1,99 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\Player;
use pocketmine\Server;
/**
* Handler used for the pre-spawn phase of the session.
*/
class PreSpawnSessionHandler extends SessionHandler{
/** @var Server */
private $server;
/** @var Player */
private $player;
/** @var NetworkSession */
private $session;
public function __construct(Server $server, Player $player, NetworkSession $session){
$this->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;
}
}

View File

@ -0,0 +1,128 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\network\mcpe\NetworkSession;
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\Player;
use pocketmine\resourcepacks\ResourcePack;
use pocketmine\Server;
/**
* Handler used for the resource packs sequence phase of the session. This handler takes care of downloading resource
* packs to the client.
*/
class ResourcePacksSessionHandler extends SessionHandler{
/** @var Server */
private $server;
/** @var Player */
private $player;
/** @var NetworkSession */
private $session;
public function __construct(Server $server, Player $player, NetworkSession $session){
$this->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;
}
}

View File

@ -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);
}

View File

@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h>
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{