LoginPacket: use netresearch/jsonmapper for login data decoding

this makes retrieval static analysis friendly without extra steps.
This commit is contained in:
Dylan K. Taylor 2020-03-23 21:58:12 +00:00
parent 3e5d3a646b
commit 83a3adecff
11 changed files with 495 additions and 143 deletions

View File

@ -41,7 +41,8 @@
"pocketmine/classloader": "dev-master",
"pocketmine/callback-validator": "^1.0.1",
"adhocore/json-comment": "^0.1.0",
"particle/validator": "^2.3"
"particle/validator": "^2.3",
"netresearch/jsonmapper": "^2.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.14",

48
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b59b043a71525b45752770b4fd1ce2cb",
"content-hash": "8a94ad4a1822c04cb884a6dee81923df",
"packages": [
{
"name": "adhocore/json-comment",
@ -191,6 +191,52 @@
],
"time": "2018-12-03T18:17:01+00:00"
},
{
"name": "netresearch/jsonmapper",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e245890383c3ed38b6d202ee373c23ccfebc0f54",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0",
"squizlabs/php_codesniffer": "~3.5"
},
"type": "library",
"autoload": {
"psr-0": {
"JsonMapper": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"OSL-3.0"
],
"authors": [
{
"name": "Christian Weiske",
"email": "cweiske@cweiske.de",
"homepage": "http://github.com/cweiske/jsonmapper/",
"role": "Developer"
}
],
"description": "Map nested JSON structures onto PHP classes",
"time": "2020-03-04T17:23:33+00:00"
},
{
"name": "particle/validator",
"version": "v2.3.4",

View File

@ -33,6 +33,7 @@ parameters:
- pocketmine\IS_DEVELOPMENT_BUILD
- pocketmine\DEBUG
stubFiles:
- tests/phpstan/stubs/JsonMapper.stub
- tests/phpstan/stubs/pthreads.stub
reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings
ignoreErrors:

View File

@ -95,7 +95,7 @@ class ProcessLoginTask extends AsyncTask{
$currentKey = null;
$first = true;
foreach($packet->chainDataJwt as $jwt){
foreach($packet->chainDataJwt->chain as $jwt){
$this->validateToken($jwt, $currentKey, $first);
if($first){
$first = false;

View File

@ -68,38 +68,39 @@ class LoginPacketHandler extends PacketHandler{
return true;
}
if(!Player::isValidUserName($packet->extraData[LoginPacket::I_USERNAME])){
if(!Player::isValidUserName($packet->extraData->displayName)){
$this->session->disconnect("disconnectionScreen.invalidName");
return true;
}
try{
$clientData = $packet->clientData; //this serves no purpose except readability
/** @var SkinAnimation[] $animations */
$animations = [];
foreach($packet->clientData[LoginPacket::I_ANIMATION_IMAGES] as $animation){
foreach($clientData->AnimatedImageData as $animation){
$animations[] = new SkinAnimation(
new SkinImage(
$animation[LoginPacket::I_ANIMATION_IMAGE_HEIGHT],
$animation[LoginPacket::I_ANIMATION_IMAGE_WIDTH],
base64_decode($animation[LoginPacket::I_ANIMATION_IMAGE_DATA], true)
$animation->ImageHeight,
$animation->ImageWidth,
base64_decode($animation->Image, true)
),
$animation[LoginPacket::I_ANIMATION_IMAGE_TYPE],
$animation[LoginPacket::I_ANIMATION_IMAGE_FRAMES]
$animation->Type,
$animation->Frames
);
}
$skinData = new SkinData(
$packet->clientData[LoginPacket::I_SKIN_ID],
base64_decode($packet->clientData[LoginPacket::I_SKIN_RESOURCE_PATCH], true),
new SkinImage($packet->clientData[LoginPacket::I_SKIN_HEIGHT], $packet->clientData[LoginPacket::I_SKIN_WIDTH], base64_decode($packet->clientData[LoginPacket::I_SKIN_DATA], true)),
$clientData->SkinId,
base64_decode($clientData->SkinResourcePatch, true),
new SkinImage($clientData->SkinImageHeight, $clientData->SkinImageWidth, base64_decode($clientData->SkinData, true)),
$animations,
new SkinImage($packet->clientData[LoginPacket::I_CAPE_HEIGHT], $packet->clientData[LoginPacket::I_CAPE_WIDTH], base64_decode($packet->clientData[LoginPacket::I_CAPE_DATA], true)),
base64_decode($packet->clientData[LoginPacket::I_GEOMETRY_DATA], true),
base64_decode($packet->clientData[LoginPacket::I_ANIMATION_DATA], true),
$packet->clientData[LoginPacket::I_PREMIUM_SKIN],
$packet->clientData[LoginPacket::I_PERSONA_SKIN],
$packet->clientData[LoginPacket::I_PERSONA_CAPE_ON_CLASSIC_SKIN],
$packet->clientData[LoginPacket::I_CAPE_ID]
new SkinImage($clientData->CapeImageHeight, $clientData->CapeImageWidth, base64_decode($clientData->CapeData, true)),
base64_decode($clientData->SkinGeometryData, true),
base64_decode($clientData->SkinAnimationData, true),
$clientData->PremiumSkin,
$clientData->PersonaSkin,
$clientData->CapeOnClassicSkin,
$clientData->CapeId
);
$skin = SkinAdapterSingleton::get()->fromSkinData($skinData);
@ -111,12 +112,12 @@ class LoginPacketHandler extends PacketHandler{
}
$this->session->setPlayerInfo(new PlayerInfo(
$packet->extraData[LoginPacket::I_USERNAME],
UUID::fromString($packet->extraData[LoginPacket::I_UUID]),
$packet->extraData->displayName,
UUID::fromString($packet->extraData->identity),
$skin,
$packet->clientData[LoginPacket::I_LANGUAGE_CODE],
$packet->extraData[LoginPacket::I_XUID],
$packet->clientData
$packet->clientData->LanguageCode,
$packet->extraData->XUID,
(array) $packet->clientData
));
$ev = new PlayerPreLoginEvent(

View File

@ -25,77 +25,34 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h>
use Particle\Validator\Validator;
use pocketmine\network\BadPacketException;
use pocketmine\network\mcpe\handler\PacketHandler;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
use pocketmine\network\mcpe\serializer\NetworkBinaryStream;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function array_filter;
use function count;
use function implode;
use function is_array;
use function json_decode;
use function json_last_error_msg;
class LoginPacket extends DataPacket implements ServerboundPacket{
public const NETWORK_ID = ProtocolInfo::LOGIN_PACKET;
public const EDITION_POCKET = 0;
public const I_USERNAME = 'displayName';
public const I_UUID = 'identity';
public const I_XUID = 'XUID';
public const I_CLIENT_RANDOM_ID = 'ClientRandomId';
public const I_SERVER_ADDRESS = 'ServerAddress';
public const I_LANGUAGE_CODE = 'LanguageCode';
public const I_SKIN_RESOURCE_PATCH = 'SkinResourcePatch';
public const I_SKIN_ID = 'SkinId';
public const I_SKIN_HEIGHT = 'SkinImageHeight';
public const I_SKIN_WIDTH = 'SkinImageWidth';
public const I_SKIN_DATA = 'SkinData';
public const I_CAPE_ID = 'CapeId';
public const I_CAPE_HEIGHT = 'CapeImageHeight';
public const I_CAPE_WIDTH = 'CapeImageWidth';
public const I_CAPE_DATA = 'CapeData';
public const I_GEOMETRY_DATA = 'SkinGeometryData';
public const I_ANIMATION_DATA = 'SkinAnimationData';
public const I_ANIMATION_IMAGES = 'AnimatedImageData';
public const I_ANIMATION_IMAGE_HEIGHT = 'ImageHeight';
public const I_ANIMATION_IMAGE_WIDTH = 'ImageWidth';
public const I_ANIMATION_IMAGE_FRAMES = 'Frames';
public const I_ANIMATION_IMAGE_TYPE = 'Type';
public const I_ANIMATION_IMAGE_DATA = 'Image';
public const I_PREMIUM_SKIN = 'PremiumSkin';
public const I_PERSONA_SKIN = 'PersonaSkin';
public const I_PERSONA_CAPE_ON_CLASSIC_SKIN = 'CapeOnClassicSkin';
/** @var int */
public $protocol;
/** @var string[] array of encoded JWT */
public $chainDataJwt = [];
/**
* @var mixed[]|null extraData index of whichever JWT has it
* @phpstan-var array<string, mixed>
*/
/** @var JwtChain */
public $chainDataJwt;
/** @var AuthenticationData|null extraData index of whichever JWT has it */
public $extraData = null;
/** @var string */
public $clientDataJwt;
/**
* @var mixed[] decoded payload of the clientData JWT
* @phpstan-var array<string, mixed>
*/
public $clientData = [];
/** @var ClientData decoded payload of the clientData JWT */
public $clientData;
/**
* This field may be used by plugins to bypass keychain verification. It should only be used for plugins such as
@ -114,22 +71,6 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
$this->decodeConnectionRequest($in);
}
/**
* @param mixed $data
*
* @throws BadPacketException
*/
private static function validate(Validator $v, string $name, $data) : void{
$result = $v->validate($data);
if($result->isNotValid()){
$messages = [];
foreach($result->getFailures() as $f){
$messages[] = $f->format();
}
throw new BadPacketException("Failed to validate '$name': " . implode(", ", $messages));
}
}
/**
* @throws BadPacketException
* @throws BinaryDataException
@ -137,19 +78,19 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
protected function decodeConnectionRequest(NetworkBinaryStream $in) : void{
$buffer = new BinaryStream($in->getString());
$chainData = json_decode($buffer->get($buffer->getLInt()), true);
if(!is_array($chainData)){
throw new BadPacketException("Failed to decode chainData JSON: " . json_last_error_msg());
$chainDataJson = json_decode($buffer->get($buffer->getLInt()));
$mapper = new \JsonMapper;
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
try{
$chainData = $mapper->map($chainDataJson, new JwtChain);
}catch(\JsonMapper_Exception $e){
throw BadPacketException::wrap($e);
}
$vd = new Validator();
$vd->required('chain')->isArray()->callback(function(array $data) : bool{
return count($data) <= 3 and count(array_filter($data, '\is_string')) === count($data);
});
self::validate($vd, "chainData", $chainData);
$this->chainDataJwt = $chainData;
$this->chainDataJwt = $chainData['chain'];
foreach($this->chainDataJwt as $k => $chain){
foreach($this->chainDataJwt->chain as $k => $chain){
//validate every chain element
try{
$claims = Utils::getJwtClaims($chain);
@ -164,13 +105,15 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
throw new BadPacketException("Found 'extraData' more than once in chainData");
}
$extraV = new Validator();
$extraV->required(self::I_USERNAME)->string();
$extraV->required(self::I_UUID)->uuid();
$extraV->required(self::I_XUID)->string()->digits()->allowEmpty(true);
self::validate($extraV, "chain.$k.extraData", $claims['extraData']);
$this->extraData = $claims['extraData'];
$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{
$this->extraData = $mapper->map($claims['extraData'], new AuthenticationData);
}catch(\JsonMapper_Exception $e){
throw BadPacketException::wrap($e);
}
}
}
if($this->extraData === null){
@ -184,40 +127,15 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
throw new BadPacketException($e->getMessage(), 0, $e);
}
$v = new Validator();
$v->required(self::I_CLIENT_RANDOM_ID)->integer();
$v->required(self::I_SERVER_ADDRESS)->string();
$v->required(self::I_LANGUAGE_CODE)->string();
$v->required(self::I_SKIN_RESOURCE_PATCH)->string();
$v->required(self::I_SKIN_ID)->string();
$v->required(self::I_SKIN_DATA)->string();
$v->required(self::I_SKIN_HEIGHT)->integer(true);
$v->required(self::I_SKIN_WIDTH)->integer(true);
$v->required(self::I_CAPE_ID, null, true)->string();
$v->required(self::I_CAPE_DATA, null, true)->string();
$v->required(self::I_CAPE_HEIGHT)->integer(true);
$v->required(self::I_CAPE_WIDTH)->integer(true);
$v->required(self::I_GEOMETRY_DATA, null, true)->string();
$v->required(self::I_ANIMATION_DATA, null, true)->string();
$v->required(self::I_ANIMATION_IMAGES, null, true)->isArray()->each(function(Validator $vSub) : void{
$vSub->required(self::I_ANIMATION_IMAGE_HEIGHT)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_WIDTH)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_FRAMES)->numeric(); //float() doesn't accept ints ???
$vSub->required(self::I_ANIMATION_IMAGE_TYPE)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_DATA)->string();
});
$v->required(self::I_PREMIUM_SKIN)->bool();
$v->required(self::I_PERSONA_SKIN)->bool();
$v->required(self::I_PERSONA_CAPE_ON_CLASSIC_SKIN)->bool();
self::validate($v, 'clientData', $clientData);
$this->clientData = $clientData;
$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{
$this->clientData = $mapper->map($clientData, new ClientData);
}catch(\JsonMapper_Exception $e){
throw BadPacketException::wrap($e);
}
}
protected function encodePayload(NetworkBinaryStream $out) : void{

View File

@ -0,0 +1,51 @@
<?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\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class AuthenticationData{
/**
* @var string
* @required
*/
public $displayName;
/**
* @var string
* @required
*/
public $identity;
/** @var string */
public $titleId = ""; //TODO: find out what this is for
/**
* @var string
* @required
*/
public $XUID;
}

View File

@ -0,0 +1,219 @@
<?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\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class ClientData{
/**
* @var ClientDataAnimationFrame[]
* @required
*/
public $AnimatedImageData;
/**
* @var string
* @required
*/
public $CapeData;
/**
* @var string
* @required
*/
public $CapeId;
/**
* @var int
* @required
*/
public $CapeImageHeight;
/**
* @var int
* @required
*/
public $CapeImageWidth;
/**
* @var bool
* @required
*/
public $CapeOnClassicSkin;
/**
* @var int
* @required
*/
public $ClientRandomId;
/**
* @var int
* @required
*/
public $CurrentInputMode;
/**
* @var int
* @required
*/
public $DefaultInputMode;
/**
* @var string
* @required
*/
public $DeviceId;
/**
* @var string
* @required
*/
public $DeviceModel;
/**
* @var int
* @required
*/
public $DeviceOS;
/**
* @var string
* @required
*/
public $GameVersion;
/**
* @var int
* @required
*/
public $GuiScale;
/**
* @var string
* @required
*/
public $LanguageCode;
/**
* @var bool
* @required
*/
public $PersonaSkin;
/**
* @var string
* @required
*/
public $PlatformOfflineId;
/**
* @var string
* @required
*/
public $PlatformOnlineId;
/** @var string */
public $PlatformUserId = ""; //xbox-only, apparently
/**
* @var bool
* @required
*/
public $PremiumSkin = false;
/**
* @var string
* @required
*/
public $SelfSignedId;
/**
* @var string
* @required
*/
public $ServerAddress;
/**
* @var string
* @required
*/
public $SkinAnimationData;
/**
* @var string
* @required
*/
public $SkinData;
/**
* @var string
* @required
*/
public $SkinGeometryData;
/**
* @var string
* @required
*/
public $SkinId;
/**
* @var int
* @required
*/
public $SkinImageHeight;
/**
* @var int
* @required
*/
public $SkinImageWidth;
/**
* @var string
* @required
*/
public $SkinResourcePatch;
/**
* @var string
* @required
*/
public $ThirdPartyName;
/**
* @var bool
* @required
*/
public $ThirdPartyNameOnly;
/**
* @var int
* @required
*/
public $UIProfile;
}

View File

@ -0,0 +1,60 @@
<?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\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class ClientDataAnimationFrame{
/**
* @var int
* @required
*/
public $ImageHeight;
/**
* @var int
* @required
*/
public $ImageWidth;
/**
* @var float
* @required
*/
public $Frames;
/**
* @var int
* @required
*/
public $Type;
/**
* @var string
* @required
*/
public $Image;
}

View File

@ -0,0 +1,36 @@
<?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\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class JwtChain{
/**
* @var string[]
* @required
*/
public $chain;
}

View File

@ -0,0 +1,19 @@
<?php
//possible bug in phpstan requires this to be defined here
class JsonMapper_Exception extends \Exception{}
class JsonMapper{
/**
* @template TModel of object
*
* @param mixed[]|object $json
* @param TModel $object
*
* @return TModel
*
* @throws JsonMapper_Exception
*/
public function map($json, object $object) : object{}
}