LoginPacket: Cater for more error cases

This now doesn't crash unexpectedly at the first sign of broken data.
This commit is contained in:
Dylan K. Taylor
2019-01-07 14:45:44 +00:00
parent 8663be8504
commit 4f50119b74
5 changed files with 178 additions and 46 deletions

View File

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

View File

@ -26,19 +26,34 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h>
use Particle\Validator\Validator;
use pocketmine\entity\Skin;
use pocketmine\network\mcpe\handler\SessionHandler;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function array_filter;
use function base64_decode;
use function get_class;
use function count;
use function implode;
use function is_array;
use function json_decode;
use function json_last_error_msg;
class LoginPacket extends DataPacket{
public const NETWORK_ID = ProtocolInfo::LOGIN_PACKET;
public const EDITION_POCKET = 0;
public const I_CLIENT_RANDOM_ID = 'ClientRandomId';
public const I_SERVER_ADDRESS = 'ServerAddress';
public const I_LANGUAGE_CODE = 'LanguageCode';
public const I_SKIN_ID = 'SkinId';
public const I_SKIN_DATA = 'SkinData';
public const I_CAPE_DATA = 'CapeData';
public const I_GEOMETRY_NAME = 'SkinGeometryName';
public const I_GEOMETRY_DATA = 'SkinGeometry';
/** @var string */
public $username;
/** @var int */
@ -58,8 +73,8 @@ class LoginPacket extends DataPacket{
/** @var Skin|null */
public $skin;
/** @var array (the "chain" index contains one or more JWTs) */
public $chainData = [];
/** @var string[] array of encoded JWT */
public $chainDataJwt = [];
/** @var string */
public $clientDataJwt;
/** @var array decoded payload of the clientData JWT */
@ -83,65 +98,99 @@ class LoginPacket extends DataPacket{
protected function decodePayload() : void{
$this->protocol = $this->getInt();
$this->decodeConnectionRequest();
}
try{
$this->decodeConnectionRequest();
}catch(\Throwable $e){
if($this->protocol === ProtocolInfo::CURRENT_PROTOCOL){
throw $e;
}
$logger = \GlobalLogger::get();
$logger->debug(get_class($e) . " was thrown while decoding connection request in login (protocol version " . ($this->protocol ?? "unknown") . "): " . $e->getMessage());
foreach(Utils::printableTrace($e->getTrace()) as $line){
$logger->debug($line);
/**
* @param Validator $v
* @param string $name
* @param $data
*
* @throws \UnexpectedValueException
*/
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 \UnexpectedValueException("Failed to validate '$name': " . implode(", ", $messages));
}
}
/**
* @throws \OutOfBoundsException
* @throws \UnexpectedValueException
*/
protected function decodeConnectionRequest() : void{
$buffer = new BinaryStream($this->getString());
$this->chainData = json_decode($buffer->get($buffer->getLInt()), true);
$chainData = json_decode($buffer->get($buffer->getLInt()), true);
if(!is_array($chainData)){
throw new \UnexpectedValueException("Failed to decode chainData JSON: " . json_last_error_msg());
}
$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['chain'];
$hasExtraData = false;
foreach($this->chainData["chain"] as $chain){
$webtoken = Utils::decodeJWT($chain);
if(isset($webtoken["extraData"])){
foreach($this->chainDataJwt as $k => $chain){
//validate every chain element
$claims = Utils::getJwtClaims($chain);
if(isset($claims["extraData"])){
if(!is_array($claims["extraData"])){
throw new \UnexpectedValueException("'extraData' key should be an array");
}
if($hasExtraData){
throw new \RuntimeException("Found 'extraData' multiple times in key chain");
throw new \UnexpectedValueException("Found 'extraData' more than once in chainData");
}
$hasExtraData = true;
if(isset($webtoken["extraData"]["displayName"])){
$this->username = $webtoken["extraData"]["displayName"];
}
if(isset($webtoken["extraData"]["identity"])){
$this->clientUUID = $webtoken["extraData"]["identity"];
}
if(isset($webtoken["extraData"]["XUID"])){
$this->xuid = $webtoken["extraData"]["XUID"];
}
}
if(isset($webtoken["identityPublicKey"])){
$this->identityPublicKey = $webtoken["identityPublicKey"];
$extraV = new Validator();
$extraV->required('displayName')->string();
$extraV->required('identity')->uuid();
$extraV->required('XUID')->string()->digits();
self::validate($extraV, "chain.$k.extraData", $claims['extraData']);
$this->username = $claims["extraData"]["displayName"];
$this->clientUUID = $claims["extraData"]["identity"];
$this->xuid = $claims["extraData"]["XUID"];
}
}
$this->clientDataJwt = $buffer->get($buffer->getLInt());
$this->clientData = Utils::decodeJWT($this->clientDataJwt);
$clientData = Utils::getJwtClaims($this->clientDataJwt);
$this->clientId = $this->clientData["ClientRandomId"] ?? null;
$this->serverAddress = $this->clientData["ServerAddress"] ?? null;
$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();
$this->locale = $this->clientData["LanguageCode"] ?? "en_US";
$v->required(self::I_SKIN_ID)->string();
$v->required(self::I_SKIN_DATA)->string();
$v->required(self::I_CAPE_DATA, null, true)->string();
$v->required(self::I_GEOMETRY_NAME)->string();
$v->required(self::I_GEOMETRY_DATA, null, true)->string();
self::validate($v, 'clientData', $clientData);
$this->clientData = $clientData;
$this->clientId = $this->clientData[self::I_CLIENT_RANDOM_ID];
$this->serverAddress = $this->clientData[self::I_SERVER_ADDRESS];
$this->locale = $this->clientData[self::I_LANGUAGE_CODE];
$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"] ?? "")
$this->clientData[self::I_SKIN_ID],
base64_decode($this->clientData[self::I_SKIN_DATA]),
base64_decode($this->clientData[self::I_CAPE_DATA]),
$this->clientData[self::I_GEOMETRY_NAME],
base64_decode($this->clientData[self::I_GEOMETRY_DATA])
);
}