session = $session; $this->server = $server; $this->playerInfoConsumer = $playerInfoConsumer; $this->authCallback = $authCallback; } private static function dummy() : void{ echo PublicKeyInterface::class; //this prevents the import getting removed by tools that don't understand phpstan } public function handleLogin(LoginPacket $packet) : bool{ if(!$this->isCompatibleProtocol($packet->protocol)){ $this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : 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->session->disconnect( $this->server->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]), false ); return true; } $extraData = $this->fetchAuthData($packet->chainDataJwt); if(!Player::isValidUserName($extraData->displayName)){ $this->session->disconnect("disconnectionScreen.invalidName"); return true; } $clientData = $this->parseClientData($packet->clientDataJwt); $safeB64Decode = static function(string $base64, string $context) : string{ $result = base64_decode($base64, true); if($result === false){ throw new \InvalidArgumentException("$context: Malformed base64, cannot be decoded"); } return $result; }; try{ /** @var SkinAnimation[] $animations */ $animations = []; foreach($clientData->AnimatedImageData as $k => $animation){ $animations[] = new SkinAnimation( new SkinImage( $animation->ImageHeight, $animation->ImageWidth, $safeB64Decode($animation->Image, "AnimatedImageData.$k.Image") ), $animation->Type, $animation->Frames ); } $skinData = new SkinData( $clientData->SkinId, $safeB64Decode($clientData->SkinResourcePatch, "SkinResourcePatch"), new SkinImage($clientData->SkinImageHeight, $clientData->SkinImageWidth, $safeB64Decode($clientData->SkinData, "SkinData")), $animations, new SkinImage($clientData->CapeImageHeight, $clientData->CapeImageWidth, $safeB64Decode($clientData->CapeData, "CapeData")), $safeB64Decode($clientData->SkinGeometryData, "SkinGeometryData"), $safeB64Decode($clientData->SkinAnimationData, "SkinAnimationData"), $clientData->PremiumSkin, $clientData->PersonaSkin, $clientData->CapeOnClassicSkin, $clientData->CapeId, null, $clientData->ArmSize, $clientData->SkinColor, array_map(function(ClientDataPersonaSkinPiece $piece) : PersonaSkinPiece{ return new PersonaSkinPiece($piece->PieceId, $piece->PieceType, $piece->PackId, $piece->IsDefault, $piece->ProductId); }, $clientData->PersonaPieces), array_map(function(ClientDataPersonaPieceTintColor $tint) : PersonaPieceTintColor{ return new PersonaPieceTintColor($tint->PieceType, $tint->Colors); }, $clientData->PieceTintColors) ); $skin = SkinAdapterSingleton::get()->fromSkinData($skinData); }catch(\InvalidArgumentException $e){ $this->session->getLogger()->debug("Invalid skin: " . $e->getMessage()); $this->session->disconnect("disconnectionScreen.invalidSkin"); return true; } try{ $uuid = UUID::fromString($extraData->identity); }catch(\InvalidArgumentException $e){ throw BadPacketException::wrap($e, "Failed to parse login UUID"); } ($this->playerInfoConsumer)(new PlayerInfo( $extraData->displayName, $uuid, $skin, $clientData->LanguageCode, $extraData->XUID, (array) $clientData )); $ev = new PlayerPreLoginEvent( $this->session->getPlayerInfo(), $this->session->getIp(), $this->session->getPort(), $this->server->requiresAuthentication() ); if($this->server->getNetwork()->getConnectionCount() > $this->server->getMaxPlayers()){ $ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_FULL, "disconnectionScreen.serverFull"); } if(!$this->server->isWhitelisted($this->session->getPlayerInfo()->getUsername())){ $ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_WHITELISTED, "Server is whitelisted"); } if($this->server->getNameBans()->isBanned($this->session->getPlayerInfo()->getUsername()) or $this->server->getIPBans()->isBanned($this->session->getIp())){ $ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_BANNED, "You are banned"); } $ev->call(); if(!$ev->isAllowed()){ $this->session->disconnect($ev->getFinalKickMessage()); return true; } $this->processLogin($packet, $ev->isAuthRequired()); return true; } /** * @throws BadPacketException */ protected function fetchAuthData(JwtChain $chain) : AuthenticationData{ /** @var AuthenticationData|null $extraData */ $extraData = null; foreach($chain->chain as $k => $chain){ //validate every chain element try{ [, $claims, ] = JwtUtils::parse($chain); }catch(JwtException $e){ throw BadPacketException::wrap($e); } if(isset($claims["extraData"])){ if($extraData !== null){ throw new BadPacketException("Found 'extraData' more than once in chainData"); } if(!is_array($claims["extraData"])){ throw new BadPacketException("'extraData' key should be an array"); } $mapper = new \JsonMapper; $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models $mapper->bExceptionOnMissingData = true; $mapper->bExceptionOnUndefinedProperty = true; try{ /** @var AuthenticationData $extraData */ $extraData = $mapper->map($claims['extraData'], new AuthenticationData); }catch(\JsonMapper_Exception $e){ throw BadPacketException::wrap($e); } } } if($extraData === null){ throw new BadPacketException("'extraData' not found in chain data"); } return $extraData; } /** * @throws BadPacketException */ protected function parseClientData(string $clientDataJwt) : ClientData{ try{ [, $clientDataClaims, ] = JwtUtils::parse($clientDataJwt); }catch(JwtException $e){ throw BadPacketException::wrap($e); } $mapper = new \JsonMapper; $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models $mapper->bExceptionOnMissingData = true; $mapper->bExceptionOnUndefinedProperty = true; try{ $clientData = $mapper->map($clientDataClaims, new ClientData); }catch(\JsonMapper_Exception $e){ throw BadPacketException::wrap($e); } return $clientData; } /** * TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication. * In the future this won't be necessary. * * @throws \InvalidArgumentException */ protected function processLogin(LoginPacket $packet, bool $authRequired) : void{ $this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet, $authRequired, $this->authCallback)); $this->session->setHandler(null); //drop packets received during login verification } protected function isCompatibleProtocol(int $protocolVersion) : bool{ return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL; } }