Merge branch 'minor-next' into major-next

This commit is contained in:
Dylan K. Taylor
2025-09-26 15:55:55 +01:00
34 changed files with 1301 additions and 488 deletions

104
changelogs/5.34.md Normal file
View File

@@ -0,0 +1,104 @@
# 5.34.0
Released 26th September 2025.
This is a minor feature release containing performance improvements, new gameplay features, new API additions and network changes.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- PocketMine-MP now requires and uses [`pmmp/ext-encoding`](https://github.com/pmmp/ext-encoding) version `1.0.0`, a PHP extension designed to significantly improve performance of data encoding and decoding (@dktapps).
- This first pass only implements support in low-risk areas, such as network protocol and read-only data. Further integration will follow in future minor versions with additional performance improvements.
- While ext-encoding has been heavily tested, we can't be sure there won't be major issues once it reaches production. Please report any bugs you find on the GitHub issue tracker.
- New Bedrock OpenID authentication support has been implemented (@dries-c, @dktapps).
- This system fetches keys from an API provided by Microsoft. Therefore, your server must now have internet access to authenticate players.
- `/timings paste` now creates private reports by default on `timings.pmmp.io` (@dktapps).
- Private reports require an access token to view, so your timings reports can no longer be viewed by others just by guessing the ID.
- If you're using a custom timings host, be sure to update it to get support for this feature.
- The command will generate a warning in the console if the target timings host doesn't support private reports.
## Performance
- Significantly improved performance of packet encoding and decoding using `ext-encoding` (@dktapps).
- Unnecessary NBT is now stripped from items before sending them over the network. This significantly improves performance when working with writable books, shulker boxes, etc. (@dktapps).
- Improved performance of item saving in `ItemSerializer` by avoiding slow `hasNamedTag()` call followed by `getNamedTag()` (both will rebuild the NBT) (@dktapps).
## Gameplay
- Implemented basic Trident functionality (@IvanCraft623).
- Implemented Firework and Firework Star (@IvanCraft623).
- Editing the rear side of signs is now supported (@dktapps).
- Sneaking hitbox height has been adjusted to match vanilla (@Dasciam).
## API
### General
- `pocketmine/nbt` version `1.2.0` is now used ([changelog](https://github.com/pmmp/NBT/releases/tag/1.2.0)).
- `pmmp/ext-encoding` version `1.0.0` is now required and used.
- This can be used as a faster alternative to `BinaryStream` and `Binary` in most use cases. However, please note that its API is very different, and it hasn't been battle-tested yet.
- A recent JetBrains IDE stub can be found in our [custom stubs repository](https://github.com/pmmp/phpstorm-stubs/blob/fork/encoding/encoding.php).
### `pocketmine\block`
- The following API methods have been added:
- `public BaseSign->getFaceText(bool $frontFace) : SignText`
- `public BaseSign->setFaceText(bool $frontFace, SignText $text) : $this`
- `public BaseSign->updateFaceText(Player $author, bool $frontFace, SignText $text) : bool` - called by the network system when a player edits a sign, triggers `SignChangeEvent` etc.
- `protected BaseSign->getHitboxCenter() : Vector3` - returns the center of the sign's hitbox, used to decide which face of the sign the player is editing
- `protected BaseSign->getFacingDegrees() : float` (to become abstract in PM6) - returns the horizontal facing of the sign in degrees, used to decide which face of the sign the player is editing
- The following API methods have been deprecated:
- `public BaseSign->getText() : SignText` - use `getFaceText()` instead
- `public BaseSign->setText(SignText $text) : $this` - use `setFaceText()` instead
- `public BaseSign->updateText(Player $author, SignText $text) : bool` - use `updateFaceText()` instead
### `pocketmine\entity`
- The following API classes have been added:
- `NeverSavedWithChunkEntity` - implement this instead of overriding `canSaveWithChunk()` if your entity will never need a save ID
- Used currently by `Player` and `FireworkRocket`.
- `animation\FireworkParticlesAnimation`
- `object\FireworkRocket`
- `projectile\Trident`
- The following API methods have been added:
- `public Living->getSneakOffset() : float` - returns how much the entity's hitbox is shortened and eye height lowered when sneaking (0 by default)
- `protected Projectile->despawnsOnEntityHit() : bool` - returns `true` by default, overridden by tridents (to be removed in a future major version in favour of cleaner BC-breaking methods)
### `pocketmine\event\block`
- The following API methods have been added:
- `public SignChangeEvent->isFrontFace() : bool` - returns `true` if the front face of the sign is being edited, `false` for the rear face
### `pocketmine\inventory\transaction`
- `InventoryTransaction` no longer shuffles actions before executing a transaction.
- This was intended to prevent dependency on weird client behaviour, but it is no longer necessary, as the order is now consistent since the introduction of the `ItemStackRequest` system.
### `pocketmine\item`
- The following API classes have been added:
- `FireworkRocket`
- `FireworkRocketExplosion`
- `FireworkRocketType` (enum)
- `FireworkStar`
- `Trident`
- The following API methods have been added:
- `VanillaItems::FIREWORK_ROCKET() : FireworkRocket`
- `VanillaItems::FIREWORK_STAR() : FireworkStar`
- `VanillaItems::TRIDENT() : Trident`
### `pocketmine\player`
- The following API methods have signature changes:
- `Player->openSignEditor()` now accepts an optional `bool $frontFace = true` parameter
### `pocketmine\world\sound`
- The following API classes have been added:
- `FireworkCrackleSound`
- `FireworkExplosionSound`
- `FireworkLargeExplosionSound`
- `FireworkLaunchSound`
- `TridentHitEntitySound`
- `TridentHitBlockSound`
- `TridentThrowSound`
## Internals
- Many low-risk data handling areas have been switched to use `ext-encoding`, including:
- Bedrock packets
- Bedrock chunk serialization
- `FastChunkSerializer` (used for transmitting chunks between threads)
- GS4 Query
- Auxiliary read-only data loading in the `pocketmine\data\bedrock` package

View File

@@ -12,6 +12,7 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-date": "*", "ext-date": "*",
"ext-encoding": "~1.0.0",
"ext-gmp": "*", "ext-gmp": "*",
"ext-hash": "*", "ext-hash": "*",
"ext-igbinary": "^3.0.1", "ext-igbinary": "^3.0.1",
@@ -36,7 +37,7 @@
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100", "pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100",
"pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100", "pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~41.0.0+bedrock-1.21.100", "pocketmine/bedrock-protocol": "~50.0.0+bedrock-1.21.100",
"pocketmine/binaryutils": "^0.2.1", "pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "dev-rewrite", "pocketmine/callback-validator": "dev-rewrite",
"pocketmine/color": "^0.3.0", "pocketmine/color": "^0.3.0",

16
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2b45f6c98ece3e7043727ce0717b7c4b", "content-hash": "09e49f15217a41b27bed993400e6c185",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@@ -256,19 +256,20 @@
}, },
{ {
"name": "pocketmine/bedrock-protocol", "name": "pocketmine/bedrock-protocol",
"version": "41.0.1+bedrock-1.21.100", "version": "50.0.0+bedrock-1.21.100",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git", "url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "01f54c3e86ddfd3d9354b17a57c7b9a6064f3795" "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/01f54c3e86ddfd3d9354b17a57c7b9a6064f3795", "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2d7aa27a5537ae593fb1c39158648ea462fef72a",
"reference": "01f54c3e86ddfd3d9354b17a57c7b9a6064f3795", "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-encoding": "~1.0.0",
"ext-json": "*", "ext-json": "*",
"php": "^8.1", "php": "^8.1",
"pocketmine/binaryutils": "^0.2.0", "pocketmine/binaryutils": "^0.2.0",
@@ -296,9 +297,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": { "support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues", "issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/41.0.1+bedrock-1.21.100" "source": "https://github.com/pmmp/BedrockProtocol/tree/50.0.0+bedrock-1.21.100"
}, },
"time": "2025-09-14T19:29:22+00:00" "time": "2025-09-20T23:09:19+00:00"
}, },
{ {
"name": "pocketmine/binaryutils", "name": "pocketmine/binaryutils",
@@ -2812,6 +2813,7 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-date": "*", "ext-date": "*",
"ext-encoding": "~1.0.0",
"ext-gmp": "*", "ext-gmp": "*",
"ext-hash": "*", "ext-hash": "*",
"ext-igbinary": "^3.0.1", "ext-igbinary": "^3.0.1",

View File

@@ -98,6 +98,7 @@ namespace pocketmine {
"crypto" => "php-crypto", "crypto" => "php-crypto",
"ctype" => "ctype", "ctype" => "ctype",
"date" => "Date", "date" => "Date",
"encoding" => "pmmp/ext-encoding",
"gmp" => "GMP", "gmp" => "GMP",
"hash" => "Hash", "hash" => "Hash",
"igbinary" => "igbinary", "igbinary" => "igbinary",
@@ -155,6 +156,12 @@ namespace pocketmine {
} }
} }
if(($encoding_version = phpversion("encoding")) !== false){
if(version_compare($encoding_version, "1.0.0") < 0 || version_compare($encoding_version, "2.0.0") >= 0){
$messages[] = "pmmp/ext-encoding ^1.0.0 is required, while you have $encoding_version.";
}
}
if(extension_loaded("pocketmine")){ if(extension_loaded("pocketmine")){
$messages[] = "The native PocketMine extension is no longer supported."; $messages[] = "The native PocketMine extension is no longer supported.";
} }

View File

@@ -50,6 +50,7 @@ use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException; use pocketmine\lang\LanguageNotFoundException;
use pocketmine\lang\Translatable; use pocketmine\lang\Translatable;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\auth\AuthKeyProvider;
use pocketmine\network\mcpe\compression\CompressBatchPromise; use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\CompressBatchTask; use pocketmine\network\mcpe\compression\CompressBatchTask;
use pocketmine\network\mcpe\compression\Compressor; use pocketmine\network\mcpe\compression\Compressor;
@@ -271,6 +272,7 @@ class Server{
private int $maxPlayers; private int $maxPlayers;
private bool $onlineMode = true; private bool $onlineMode = true;
private AuthKeyProvider $authKeyProvider;
private Network $network; private Network $network;
private bool $networkCompressionAsync = true; private bool $networkCompressionAsync = true;
@@ -987,6 +989,8 @@ class Server{
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled())); $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
} }
$this->authKeyProvider = new AuthKeyProvider(new \PrefixedLogger($this->logger, "Minecraft Auth Key Provider"), $this->asyncPool);
if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){ if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
$this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD); $this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
} }
@@ -1806,6 +1810,13 @@ class Server{
return $this->forceLanguage; return $this->forceLanguage;
} }
/**
* @internal
*/
public function getAuthKeyProvider() : AuthKeyProvider{
return $this->authKeyProvider;
}
public function getNetwork() : Network{ public function getNetwork() : Network{
return $this->network; return $this->network;
} }

View File

@@ -31,8 +31,8 @@ use function str_repeat;
final class VersionInfo{ final class VersionInfo{
public const NAME = "PocketMine-MP"; public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.33.3"; public const BASE_VERSION = "5.34.0";
public const IS_DEVELOPMENT_BUILD = true; public const IS_DEVELOPMENT_BUILD = false;
public const BUILD_CHANNEL = "stable"; public const BUILD_CHANNEL = "stable";
/** /**

View File

@@ -26,6 +26,8 @@ declare(strict_types=1);
*/ */
namespace pocketmine\block; namespace pocketmine\block;
use pmmp\encoding\BE;
use pmmp\encoding\LE;
use pocketmine\block\tile\Spawnable; use pocketmine\block\tile\Spawnable;
use pocketmine\block\tile\Tile; use pocketmine\block\tile\Tile;
use pocketmine\block\utils\SupportType; use pocketmine\block\utils\SupportType;
@@ -49,7 +51,6 @@ use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\world\BlockTransaction; use pocketmine\world\BlockTransaction;
use pocketmine\world\format\Chunk; use pocketmine\world\format\Chunk;
use pocketmine\world\Position; use pocketmine\world\Position;
@@ -98,9 +99,10 @@ class Block{
* of operations required to compute the state ID (micro optimization). * of operations required to compute the state ID (micro optimization).
*/ */
private static function computeStateIdXorMask(int $typeId) : int{ private static function computeStateIdXorMask(int $typeId) : int{
//TODO: the mixed byte order here is probably a mistake, but it doesn't break anything for now
return return
$typeId << self::INTERNAL_STATE_DATA_BITS | $typeId << self::INTERNAL_STATE_DATA_BITS |
(Binary::readLong(hash('xxh3', Binary::writeLLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK); (BE::unpackSignedLong(hash('xxh3', LE::packSignedLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK);
} }
/** /**

View File

@@ -23,10 +23,11 @@ declare(strict_types=1);
namespace pocketmine\crafting; namespace pocketmine\crafting;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\VarInt;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\TreeRoot; use pocketmine\nbt\TreeRoot;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\DestructorCallbackTrait; use pocketmine\utils\DestructorCallbackTrait;
use pocketmine\utils\ObjectSet; use pocketmine\utils\ObjectSet;
use function array_shift; use function array_shift;
@@ -103,11 +104,13 @@ class CraftingManager{
public function getRecipeRegisteredCallbacks() : ObjectSet{ return $this->recipeRegisteredCallbacks; } public function getRecipeRegisteredCallbacks() : ObjectSet{ return $this->recipeRegisteredCallbacks; }
private static function hashOutput(Item $output) : string{ private static function hashOutput(Item $output) : string{
$write = new BinaryStream(); $write = new ByteBufferWriter();
$write->putVarInt($output->getStateId()); VarInt::writeSignedInt($write, $output->getStateId());
$write->put((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag()))); //TODO: the NBT serializer allocates its own ByteBufferWriter, we should change the API in the future to
//allow passing our own to avoid this extra allocation
$write->writeByteArray((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
return $write->getBuffer(); return $write->getData();
} }
/** /**

View File

@@ -23,11 +23,12 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock\block\upgrade; namespace pocketmine\data\bedrock\block\upgrade;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\DataDecodeException;
use pmmp\encoding\VarInt;
use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\data\bedrock\block\BlockStateDeserializeException;
use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
/** /**
* Handles translating legacy 1.12 block ID/meta into modern blockstates. * Handles translating legacy 1.12 block ID/meta into modern blockstates.
@@ -84,25 +85,25 @@ final class BlockIdMetaUpgrader{
public static function loadFromString(string $data, LegacyBlockIdToStringIdMap $idMap, BlockStateUpgrader $blockStateUpgrader) : self{ public static function loadFromString(string $data, LegacyBlockIdToStringIdMap $idMap, BlockStateUpgrader $blockStateUpgrader) : self{
$mappingTable = []; $mappingTable = [];
$legacyStateMapReader = new BinaryStream($data); $legacyStateMapReader = new ByteBufferReader($data);
$nbtReader = new LittleEndianNbtSerializer(); $nbtReader = new LittleEndianNbtSerializer();
$idCount = $legacyStateMapReader->getUnsignedVarInt(); $idCount = VarInt::readUnsignedInt($legacyStateMapReader);
for($idIndex = 0; $idIndex < $idCount; $idIndex++){ for($idIndex = 0; $idIndex < $idCount; $idIndex++){
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt()); $id = $legacyStateMapReader->readByteArray(VarInt::readUnsignedInt($legacyStateMapReader));
$metaCount = $legacyStateMapReader->getUnsignedVarInt(); $metaCount = VarInt::readUnsignedInt($legacyStateMapReader);
for($metaIndex = 0; $metaIndex < $metaCount; $metaIndex++){ for($metaIndex = 0; $metaIndex < $metaCount; $metaIndex++){
$meta = $legacyStateMapReader->getUnsignedVarInt(); $meta = VarInt::readUnsignedInt($legacyStateMapReader);
$offset = $legacyStateMapReader->getOffset(); $offset = $legacyStateMapReader->getOffset();
$state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag(); $state = $nbtReader->read($legacyStateMapReader->getData(), $offset)->mustGetCompoundTag();
$legacyStateMapReader->setOffset($offset); $legacyStateMapReader->setOffset($offset);
$mappingTable[$id][$meta] = $blockStateUpgrader->upgrade(BlockStateData::fromNbt($state)); $mappingTable[$id][$meta] = $blockStateUpgrader->upgrade(BlockStateData::fromNbt($state));
} }
} }
if(!$legacyStateMapReader->feof()){ if($legacyStateMapReader->getUnreadLength() > 0){
throw new BinaryDataException("Unexpected trailing data in legacy state map data"); throw new DataDecodeException("Unexpected trailing data in legacy state map data");
} }
return new self($mappingTable, $idMap); return new self($mappingTable, $idMap);

View File

@@ -38,8 +38,8 @@ use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\world\sound\TridentHitBlockSound;
use pocketmine\world\sound\TridentHitEntitySound; use pocketmine\world\sound\TridentHitEntitySound;
use pocketmine\world\sound\TridentHitGroundSound;
class Trident extends Projectile{ class Trident extends Projectile{
@@ -119,7 +119,7 @@ class Trident extends Projectile{
protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{
parent::onHitBlock($blockHit, $hitResult); parent::onHitBlock($blockHit, $hitResult);
$this->canCollide = true; $this->canCollide = true;
$this->broadcastSound(new TridentHitGroundSound()); $this->broadcastSound(new TridentHitBlockSound());
} }
public function getItem() : Item{ public function getItem() : Item{

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\network\mcpe\compression\CompressBatchPromise; use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\Compressor; use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\network\mcpe\convert\TypeConverter; use pocketmine\network\mcpe\convert\TypeConverter;
@@ -33,7 +34,6 @@ use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\network\mcpe\serializer\ChunkSerializer; use pocketmine\network\mcpe\serializer\ChunkSerializer;
use pocketmine\scheduler\AsyncTask; use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue; use pocketmine\thread\NonThreadSafeValue;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk; use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer; use pocketmine\world\format\io\FastChunkSerializer;
use function chr; use function chr;
@@ -73,11 +73,11 @@ class ChunkRequestTask extends AsyncTask{
$converter = TypeConverter::getInstance(); $converter = TypeConverter::getInstance();
$payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $converter->getBlockTranslator(), $this->tiles); $payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $converter->getBlockTranslator(), $this->tiles);
$stream = new BinaryStream(); $stream = new ByteBufferWriter();
PacketBatch::encodePackets($stream, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $dimensionId, $subCount, false, null, $payload)]); PacketBatch::encodePackets($stream, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $dimensionId, $subCount, false, null, $payload)]);
$compressor = $this->compressor->deserialize(); $compressor = $this->compressor->deserialize();
$this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getBuffer())); $this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getData()));
} }
public function onCompletion() : void{ public function onCompletion() : void{

View File

@@ -23,8 +23,10 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function base64_decode; use function base64_decode;
use function base64_encode; use function base64_encode;
@@ -32,6 +34,7 @@ use function bin2hex;
use function chr; use function chr;
use function count; use function count;
use function explode; use function explode;
use function hex2bin;
use function is_array; use function is_array;
use function json_decode; use function json_decode;
use function json_encode; use function json_encode;
@@ -54,6 +57,7 @@ use function strlen;
use function strtr; use function strtr;
use function substr; use function substr;
use const JSON_THROW_ON_ERROR; use const JSON_THROW_ON_ERROR;
use const OPENSSL_ALGO_SHA256;
use const OPENSSL_ALGO_SHA384; use const OPENSSL_ALGO_SHA384;
use const STR_PAD_LEFT; use const STR_PAD_LEFT;
@@ -130,17 +134,17 @@ final class JwtUtils{
return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence; return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence;
} }
private static function signaturePartFromAsn1(BinaryStream $stream) : string{ private static function signaturePartFromAsn1(ByteBufferReader $stream) : string{
$prefix = $stream->get(1); $prefix = $stream->readByteArray(1);
if($prefix !== self::ASN1_INTEGER_TAG){ if($prefix !== self::ASN1_INTEGER_TAG){
throw new \InvalidArgumentException("Expected an ASN.1 INTEGER tag, got " . bin2hex($prefix)); throw new \InvalidArgumentException("Expected an ASN.1 INTEGER tag, got " . bin2hex($prefix));
} }
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed //we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
$length = $stream->getByte(); $length = Byte::readUnsigned($stream);
if($length > self::SIGNATURE_PART_LENGTH + 1){ //each part may have an extra leading 0 byte to prevent it being interpreted as a negative number if($length > self::SIGNATURE_PART_LENGTH + 1){ //each part may have an extra leading 0 byte to prevent it being interpreted as a negative number
throw new \InvalidArgumentException("Expected at most 49 bytes for signature R or S, got $length"); throw new \InvalidArgumentException("Expected at most 49 bytes for signature R or S, got $length");
} }
$part = $stream->get($length); $part = $stream->readByteArray($length);
return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT); return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT);
} }
@@ -156,11 +160,11 @@ final class JwtUtils{
throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts)); throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts));
} }
$stream = new BinaryStream($parts); $stream = new ByteBufferReader($parts);
$rRaw = self::signaturePartFromAsn1($stream); $rRaw = self::signaturePartFromAsn1($stream);
$sRaw = self::signaturePartFromAsn1($stream); $sRaw = self::signaturePartFromAsn1($stream);
if(!$stream->feof()){ if($stream->getUnreadLength() > 0){
throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data"); throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data");
} }
@@ -170,17 +174,17 @@ final class JwtUtils{
/** /**
* @throws JwtException * @throws JwtException
*/ */
public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{ public static function verify(string $jwt, string $signingKeyDer, bool $ec) : bool{
[$header, $body, $signature] = self::split($jwt); [$header, $body, $signature] = self::split($jwt);
$rawSignature = self::b64UrlDecode($signature); $rawSignature = self::b64UrlDecode($signature);
$derSignature = self::rawSignatureToDer($rawSignature); $derSignature = $ec ? self::rawSignatureToDer($rawSignature) : $rawSignature;
$v = openssl_verify( $v = openssl_verify(
$header . '.' . $body, $header . '.' . $body,
$derSignature, $derSignature,
$signingKey, self::derPublicKeyToPem($signingKeyDer),
self::SIGNATURE_ALGORITHM $ec ? self::SIGNATURE_ALGORITHM : OPENSSL_ALGO_SHA256
); );
switch($v){ switch($v){
case 0: return false; case 0: return false;
@@ -238,22 +242,56 @@ final class JwtUtils{
throw new AssumptionFailedError("OpenSSL resource contains invalid public key"); throw new AssumptionFailedError("OpenSSL resource contains invalid public key");
} }
/**
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
*/
private static function encodeDerLength(int $length) : string{
if ($length <= 0x7F) {
return chr($length);
}
$lengthBytes = ltrim(BE::packUnsignedInt($length), "\x00");
return chr(0x80 | strlen($lengthBytes)) . $lengthBytes;
}
private static function encodeDerBytes(int $tag, string $data) : string{
return chr($tag) . self::encodeDerLength(strlen($data)) . $data;
}
public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{ public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{
$signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey))); $signingKeyOpenSSL = openssl_pkey_get_public(self::derPublicKeyToPem($derKey));
if($signingKeyOpenSSL === false){ if($signingKeyOpenSSL === false){
throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string()); throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string());
} }
$details = openssl_pkey_get_details($signingKeyOpenSSL);
if($details === false){
throw new JwtException("OpenSSL failed to get details from key: " . openssl_error_string());
}
if(!isset($details['ec']['curve_name'])){
throw new JwtException("Expected an EC key");
}
$curve = $details['ec']['curve_name'];
if($curve !== self::BEDROCK_SIGNING_KEY_CURVE_NAME){
throw new JwtException("Key must belong to curve " . self::BEDROCK_SIGNING_KEY_CURVE_NAME . ", got $curve");
}
return $signingKeyOpenSSL; return $signingKeyOpenSSL;
} }
public static function derPublicKeyToPem(string $derKey) : string{
return sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey));
}
/**
* Create a public key represented in DER format from RSA modulus and exponent information
*
* @param string $nBase64 The RSA modulus encoded in Base64
* @param string $eBase64 The RSA exponent encoded in Base64
*/
public static function rsaPublicKeyModExpToDer(string $nBase64, string $eBase64) : string{
$mod = self::b64UrlDecode($nBase64);
$exp = self::b64UrlDecode($eBase64);
$modulus = self::encodeDerBytes(2, $mod);
$publicExponent = self::encodeDerBytes(2, $exp);
$rsaPublicKey = self::encodeDerBytes(48, $modulus . $publicExponent);
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
$rsaOID = hex2bin('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
$rsaPublicKey = chr(0) . $rsaPublicKey;
$rsaPublicKey = self::encodeDerBytes(3, $rsaPublicKey);
return self::encodeDerBytes(48, $rsaOID . $rsaPublicKey);
}
} }

View File

@@ -23,6 +23,9 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\DataDecodeException;
use pocketmine\entity\effect\EffectInstance; use pocketmine\entity\effect\EffectInstance;
use pocketmine\event\player\PlayerDuplicateLoginEvent; use pocketmine\event\player\PlayerDuplicateLoginEvent;
use pocketmine\event\player\PlayerResourcePackOfferEvent; use pocketmine\event\player\PlayerResourcePackOfferEvent;
@@ -70,7 +73,6 @@ use pocketmine\network\mcpe\protocol\PlayerStartItemCooldownPacket;
use pocketmine\network\mcpe\protocol\PlayStatusPacket; use pocketmine\network\mcpe\protocol\PlayStatusPacket;
use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch; use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\ServerboundPacket; use pocketmine\network\mcpe\protocol\ServerboundPacket;
use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket; use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket;
use pocketmine\network\mcpe\protocol\SetDifficultyPacket; use pocketmine\network\mcpe\protocol\SetDifficultyPacket;
@@ -109,8 +111,6 @@ use pocketmine\promise\PromiseResolver;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\ObjectSet; use pocketmine\utils\ObjectSet;
use pocketmine\utils\TextFormat; use pocketmine\utils\TextFormat;
use pocketmine\world\format\io\GlobalItemDataHandlers; use pocketmine\world\format\io\GlobalItemDataHandlers;
@@ -401,7 +401,7 @@ class NetworkSession{
} }
try{ try{
$stream = new BinaryStream($decompressed); $stream = new ByteBufferReader($decompressed);
foreach(PacketBatch::decodeRaw($stream) as $buffer){ foreach(PacketBatch::decodeRaw($stream) as $buffer){
$this->gamePacketLimiter->decrement(); $this->gamePacketLimiter->decrement();
$packet = $this->packetPool->getPacket($buffer); $packet = $this->packetPool->getPacket($buffer);
@@ -421,7 +421,7 @@ class NetworkSession{
break; break;
} }
} }
}catch(PacketDecodeException|BinaryDataException $e){ }catch(PacketDecodeException|DataDecodeException $e){
$this->logger->logException($e); $this->logger->logException($e);
throw PacketHandlingException::wrap($e, "Packet batch decode error"); throw PacketHandlingException::wrap($e, "Packet batch decode error");
} }
@@ -453,14 +453,14 @@ class NetworkSession{
$decodeTimings = Timings::getDecodeDataPacketTimings($packet); $decodeTimings = Timings::getDecodeDataPacketTimings($packet);
$decodeTimings->startTiming(); $decodeTimings->startTiming();
try{ try{
$stream = PacketSerializer::decoder($buffer, 0); $stream = new ByteBufferReader($buffer);
try{ try{
$packet->decode($stream); $packet->decode($stream);
}catch(PacketDecodeException $e){ }catch(PacketDecodeException $e){
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }
if(!$stream->feof()){ if($stream->getUnreadLength() > 0){
$remains = substr($stream->getBuffer(), $stream->getOffset()); $remains = substr($stream->getData(), $stream->getOffset());
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains)); $this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
} }
}finally{ }finally{
@@ -478,7 +478,7 @@ class NetworkSession{
$handlerTimings->startTiming(); $handlerTimings->startTiming();
try{ try{
if($this->handler === null || !$packet->handle($this->handler)){ if($this->handler === null || !$packet->handle($this->handler)){
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer())); $this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getData()));
} }
}finally{ }finally{
$handlerTimings->stopTiming(); $handlerTimings->stopTiming();
@@ -530,8 +530,10 @@ class NetworkSession{
if($ackReceiptResolver !== null){ if($ackReceiptResolver !== null){
$this->sendBufferAckPromises[] = $ackReceiptResolver; $this->sendBufferAckPromises[] = $ackReceiptResolver;
} }
$writer = new ByteBufferWriter();
foreach($packets as $evPacket){ foreach($packets as $evPacket){
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket)); $writer->clear(); //memory reuse let's gooooo
$this->addToSendBuffer(self::encodePacketTimed($writer, $evPacket));
} }
if($immediate){ if($immediate){
$this->flushGamePacketQueue(); $this->flushGamePacketQueue();
@@ -564,12 +566,12 @@ class NetworkSession{
/** /**
* @internal * @internal
*/ */
public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{ public static function encodePacketTimed(ByteBufferWriter $serializer, ClientboundPacket $packet) : string{
$timings = Timings::getEncodeDataPacketTimings($packet); $timings = Timings::getEncodeDataPacketTimings($packet);
$timings->startTiming(); $timings->startTiming();
try{ try{
$packet->encode($serializer); $packet->encode($serializer);
return $serializer->getBuffer(); return $serializer->getData();
}finally{ }finally{
$timings->stopTiming(); $timings->stopTiming();
} }
@@ -591,13 +593,13 @@ class NetworkSession{
$syncMode = false; $syncMode = false;
} }
$stream = new BinaryStream(); $stream = new ByteBufferWriter();
PacketBatch::encodeRaw($stream, $this->sendBuffer); PacketBatch::encodeRaw($stream, $this->sendBuffer);
if($this->enableCompression){ if($this->enableCompression){
$batch = $this->server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer); $batch = $this->server->prepareBatch($stream->getData(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
}else{ }else{
$batch = $stream->getBuffer(); $batch = $stream->getData();
} }
$this->sendBuffer = []; $this->sendBuffer = [];
$ackPromises = $this->sendBufferAckPromises; $ackPromises = $this->sendBufferAckPromises;

View File

@@ -23,12 +23,11 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\event\server\DataPacketSendEvent; use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch; use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\BinaryStream;
use function count; use function count;
use function log; use function log;
use function spl_object_id; use function spl_object_id;
@@ -64,8 +63,10 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
$totalLength = 0; $totalLength = 0;
$packetBuffers = []; $packetBuffers = [];
$writer = new ByteBufferWriter();
foreach($packets as $packet){ foreach($packets as $packet){
$buffer = NetworkSession::encodePacketTimed(PacketSerializer::encoder(), $packet); $writer->clear(); //memory reuse let's gooooo
$buffer = NetworkSession::encodePacketTimed($writer, $packet);
//varint length prefix + packet buffer //varint length prefix + packet buffer
$totalLength += (((int) log(strlen($buffer), 128)) + 1) + strlen($buffer); $totalLength += (((int) log(strlen($buffer), 128)) + 1) + strlen($buffer);
$packetBuffers[] = $buffer; $packetBuffers[] = $buffer;
@@ -77,9 +78,9 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
$threshold = $compressor->getCompressionThreshold(); $threshold = $compressor->getCompressionThreshold();
if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){ if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){
//do not prepare shared batch unless we're sure it will be compressed //do not prepare shared batch unless we're sure it will be compressed
$stream = new BinaryStream(); $stream = new ByteBufferWriter();
PacketBatch::encodeRaw($stream, $packetBuffers); PacketBatch::encodeRaw($stream, $packetBuffers);
$batchBuffer = $stream->getBuffer(); $batchBuffer = $stream->getData();
$batch = $this->server->prepareBatch($batchBuffer, $compressor, timings: Timings::$playerNetworkSendCompressBroadcast); $batch = $this->server->prepareBatch($batchBuffer, $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
foreach($compressorTargets as $target){ foreach($compressorTargets as $target){

View File

@@ -0,0 +1,165 @@
<?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\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\JwtBodyRfc7519;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\SelfSignedJwtHeader;
use function base64_decode;
use function time;
final class AuthJwtHelper{
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
private const CLOCK_DRIFT_MAX = 60;
/**
* @throws VerifyLoginException if the token is expired or not yet valid
*/
private static function checkExpiry(JwtBodyRfc7519 $claims) : void{
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
}
/**
* @throws VerifyLoginException if errors are encountered
*/
public static function validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience) : XboxAuthJwtBody{
try{
if(!JwtUtils::verify($jwt, $signingKeyDer, ec: false)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
try{
[, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
//nasty dynamic new for JsonMapper
$claims = $mapper->map($claimsArray, new XboxAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
if(!isset($claims->iss) || $claims->iss !== $issuer){
throw new VerifyLoginException("Invalid JWT issuer");
}
if(!isset($claims->aud) || $claims->aud !== $audience){
throw new VerifyLoginException("Invalid JWT audience");
}
self::checkExpiry($claims);
return $claims;
}
/**
* @throws VerifyLoginException if errors are encountered
*/
public static function validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer) : LegacyAuthJwtBody{
self::validateSelfSignedToken($jwt, $expectedKeyDer);
//TODO: this parses the JWT twice and throws away a bunch of parts, optimize this
[, $claimsArray, ] = JwtUtils::parse($jwt);
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var LegacyAuthJwtBody $claims */
$claims = $mapper->map($claimsArray, new LegacyAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
self::checkExpiry($claims);
return $claims;
}
public static function validateSelfSignedToken(string $jwt, ?string $expectedKeyDer) : void{
try{
[$headersArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
try{
/** @var SelfSignedJwtHeader $headers */
$headers = $mapper->map($headersArray, new SelfSignedJwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($expectedKeyDer !== null && $headerDerKey !== $expectedKeyDer){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
if(!JwtUtils::verify($jwt, $headerDerKey, ec: true)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
}
}

View File

@@ -0,0 +1,164 @@
<?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\auth;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use pocketmine\scheduler\AsyncPool;
use pocketmine\utils\AssumptionFailedError;
use function array_keys;
use function count;
use function implode;
use function time;
class AuthKeyProvider{
private const ALLOWED_REFRESH_INTERVAL = 30 * 60; // 30 minutes
private ?AuthKeyring $keyring = null;
/** @phpstan-var PromiseResolver<AuthKeyring> */
private ?PromiseResolver $resolver = null;
private int $lastFetch = 0;
public function __construct(
private readonly \Logger $logger,
private readonly AsyncPool $asyncPool,
private readonly int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL
){}
/**
* Fetches the key for the given key ID.
* The promise will be resolved with an array of [issuer, pemPublicKey].
*
* @phpstan-return Promise<array{string, string}>
*/
public function getKey(string $keyId) : Promise{
/** @phpstan-var PromiseResolver<array{string, string}> $resolver */
$resolver = new PromiseResolver();
if(
$this->keyring === null || //we haven't fetched keys yet
($this->keyring->getKey($keyId) === null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds) //we don't recognise this one & keys might be outdated
){
//only refresh keys when we see one we don't recognise
$this->fetchKeys()->onCompletion(
onSuccess: fn(AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId),
onFailure: $resolver->reject(...)
);
}else{
$this->resolveKey($resolver, $this->keyring, $keyId);
}
return $resolver->getPromise();
}
/**
* @phpstan-param PromiseResolver<array{string, string}> $resolver
*/
private function resolveKey(PromiseResolver $resolver, AuthKeyring $keyring, string $keyId) : void{
$key = $keyring->getKey($keyId);
if($key === null){
$this->logger->debug("Key $keyId not recognised!");
$resolver->reject();
return;
}
$this->logger->debug("Key $keyId found in keychain");
$resolver->resolve([$keyring->getIssuer(), $key]);
}
/**
* @phpstan-param array<string, AuthServiceKey>|null $keys
* @phpstan-param string[]|null $errors
*/
private function onKeysFetched(?array $keys, string $issuer, ?array $errors) : void{
$resolver = $this->resolver;
if($resolver === null){
throw new AssumptionFailedError("Not expecting this to be called without a resolver present");
}
if($errors !== null){
$this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors));
//we might've still succeeded in fetching keys even if there were errors, so don't return
}
if($keys === null){
$this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
$resolver->reject();
}else{
$pemKeys = [];
foreach($keys as $keyModel){
if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){
$this->logger->error("Key ID $keyModel->kid doesn't have the expected properties: expected use=sig, kty=RSA, got use=$keyModel->use, kty=$keyModel->kty");
continue;
}
$derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
//make sure the key is valid
try{
JwtUtils::parseDerPublicKey($derKey);
}catch(JwtException $e){
$this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
$this->logger->logException($e);
continue;
}
//retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads
$pemKeys[$keyModel->kid] = $derKey;
}
if(count($keys) === 0){
$this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
$resolver->reject();
}else{
$this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys)));
$this->keyring = new AuthKeyring($issuer, $pemKeys);
$this->lastFetch = time();
$resolver->resolve($this->keyring);
}
}
}
/**
* @phpstan-return Promise<AuthKeyring>
*/
private function fetchKeys() : Promise{
if($this->resolver !== null){
$this->logger->debug("Key refresh was requested, but it's already in progress");
return $this->resolver->getPromise();
}
$this->logger->notice("Fetching new authentication keys");
/** @phpstan-var PromiseResolver<AuthKeyring> $resolver */
$resolver = new PromiseResolver();
$this->resolver = $resolver;
//TODO: extract this so it can be polyfilled for unit testing
$this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...)));
return $this->resolver->getPromise();
}
}

View File

@@ -0,0 +1,45 @@
<?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\auth;
final class AuthKeyring{
/**
* @param string[] $keys
* @phpstan-param array<string, string> $keys
*/
public function __construct(
private string $issuer,
private array $keys
){}
public function getIssuer() : string{ return $this->issuer; }
/**
* Returns a (raw) DER public key associated with the given key ID
*/
public function getKey(string $keyId) : ?string{
return $this->keys[$keyId] ?? null;
}
}

View File

@@ -0,0 +1,209 @@
<?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\auth;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceOpenIdConfiguration;
use pocketmine\network\mcpe\protocol\types\login\openid\api\MinecraftServicesDiscovery;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use pocketmine\utils\Internet;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use const JSON_THROW_ON_ERROR;
class FetchAuthKeysTask extends AsyncTask{
private const KEYS_ON_COMPLETION = "completion";
private const MINECRAFT_SERVICES_DISCOVERY_URL = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
private const AUTHORIZATION_SERVICE_URI_FALLBACK = "https://authorization.franchise.minecraft-services.net";
private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
private const AUTHORIZATION_SERVICE_KEYS_PATH = "/.well-known/keys";
/** @phpstan-var ?NonThreadSafeValue<array<string, AuthServiceKey>> */
private ?NonThreadSafeValue $keys = null;
private string $issuer;
/** @phpstan-var ?NonThreadSafeValue<non-empty-array<string>> */
private ?NonThreadSafeValue $errors = null;
/**
* @phpstan-param \Closure(?array<string, AuthServiceKey> $keys, string $issuer, ?string[] $errors) : void $onCompletion
*/
public function __construct(
\Closure $onCompletion
){
$this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
/** @var string[] $errors */
$errors = [];
try{
$authServiceUri = $this->getAuthServiceURI();
}catch(\RuntimeException $e){
$errors[] = $e->getMessage();
$authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
}
try {
$openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
$jwksUri = $openIdConfig->jwks_uri;
$this->issuer = $openIdConfig->issuer;
} catch (\RuntimeException $e) {
$errors[] = $e->getMessage();
$jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
$this->issuer = $authServiceUri;
}
try{
$this->keys = new NonThreadSafeValue($this->getKeys($jwksUri));
}catch(\RuntimeException $e){
$errors[] = $e->getMessage();
}
$this->errors = $errors === [] ? null : new NonThreadSafeValue($errors);
}
private function getAuthServiceURI() : string{
$result = Internet::getURL(self::MINECRAFT_SERVICES_DISCOVERY_URL);
if($result === null || $result->getCode() !== 200){
throw new \RuntimeException("Failed to fetch Minecraft services discovery document");
}
try{
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_object($json)){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var MinecraftServicesDiscovery $discovery */
$discovery = $mapper->map($json, new MinecraftServicesDiscovery());
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
}
private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{
$result = Internet::getURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH);
if($result === null || $result->getCode() !== 200){
throw new \RuntimeException("Failed to fetch OpenID configuration from authorization service");
}
try{
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_object($json)){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var AuthServiceOpenIdConfiguration $configuration */
$configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration());
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
return $configuration;
}
/**
* @return array<string, AuthServiceKey> keys indexed by key ID
*/
private function getKeys(string $jwksUri) : array{
$result = Internet::getURL($jwksUri);
if($result === null || $result->getCode() !== 200){
return throw new \RuntimeException("Failed to fetch keys from authorization service");
}
try{
$json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
$keys = [];
foreach($keysArray as $keyJson){
if(!is_array($keyJson)){
throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object");
}
try{
/** @var AuthServiceKey $key */
$key = $mapper->map($keyJson, new AuthServiceKey());
$keys[$key->kid] = $key;
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
}
return $keys;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(?AuthServiceKey[] $keys, string $issuer, ?string[] $errors) : void $callback
*/
$callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
$callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());
}
}

View File

@@ -0,0 +1,121 @@
<?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\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use pocketmine\utils\AssumptionFailedError;
use function base64_decode;
use function igbinary_serialize;
use function igbinary_unserialize;
class ProcessLegacyLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
/**
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
* it has not yet been deployed.
*/
public const LEGACY_MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
private string $chain;
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/** Whether the player has a certificate chain link signed by the given root public key. */
private bool $authenticated = false;
private ?string $clientPublicKeyDer = null;
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
private string $clientDataJwt,
private ?string $rootAuthKeyDer,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
$this->chain = igbinary_serialize($chainJwts) ?? throw new AssumptionFailedError("This should never fail");
}
public function onRun() : void{
try{
$this->clientPublicKeyDer = $this->validateChain();
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $this->clientPublicKeyDer);
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
/** @var string[] $chain */
$chain = igbinary_unserialize($this->chain);
$identityPublicKeyDer = null;
foreach($chain as $jwt){
$claims = AuthJwtHelper::validateLegacyAuthToken($jwt, $identityPublicKeyDer);
if($this->rootAuthKeyDer !== null && $identityPublicKeyDer === $this->rootAuthKeyDer){
$this->authenticated = true; //we're signed into xbox live, according to this root key
}
if(!isset($claims->identityPublicKey)){
throw new VerifyLoginException("Missing identityPublicKey in chain link", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
if($identityPublicKey === false){
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
}
$identityPublicKeyDer = $identityPublicKey;
}
if($identityPublicKeyDer === null){
throw new VerifyLoginException("No authentication chain links provided");
}
return $identityPublicKeyDer;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
}
}

View File

@@ -1,213 +0,0 @@
<?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\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\SelfSignedJwtHeader;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use function base64_decode;
use function igbinary_serialize;
use function igbinary_unserialize;
use function time;
class ProcessLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
/**
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
* it has not yet been deployed.
*/
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
private const CLOCK_DRIFT_MAX = 60;
private string $chain;
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
*/
private bool $authenticated = false;
private ?string $clientPublicKey = null;
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
private string $clientDataJwt,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
$this->chain = igbinary_serialize($chainJwts);
}
public function onRun() : void{
try{
$this->clientPublicKey = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
/** @var string[] $chain */
$chain = igbinary_unserialize($this->chain);
$currentKey = null;
$first = true;
foreach($chain as $jwt){
$this->validateToken($jwt, $currentKey, $first);
if($first){
$first = false;
}
}
/** @var string $clientKey */
$clientKey = $currentKey;
$this->validateToken($this->clientDataJwt, $currentKey);
return $clientKey;
}
/**
* @throws VerifyLoginException if errors are encountered
*/
private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{
try{
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
try{
/** @var SelfSignedJwtHeader $headers */
$headers = $mapper->map($headersArray, new SelfSignedJwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){
if(!$first){
throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
}elseif($headerDerKey !== $currentPublicKey){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
$signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey);
}catch(JwtException $e){
throw new VerifyLoginException("Invalid JWT public key: " . $e->getMessage(), null, 0, $e);
}
try{
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
$this->authenticated = true; //we're signed into xbox live
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var LegacyAuthJwtBody $claims */
$claims = $mapper->map($claimsArray, new LegacyAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
if(isset($claims->identityPublicKey)){
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
if($identityPublicKey === false){
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
}
try{
//verify key format and parameters
JwtUtils::parseDerPublicKey($identityPublicKey);
}catch(JwtException $e){
throw new VerifyLoginException("Invalid identityPublicKey: " . $e->getMessage(), null, 0, $e);
}
$currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this
}
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey);
}
}

View File

@@ -0,0 +1,98 @@
<?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\auth;
use pocketmine\lang\Translatable;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use function base64_decode;
class ProcessOpenIdLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
*/
private bool $authenticated = false;
private ?string $clientPublicKeyDer = null;
/**
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
private string $jwt,
private string $issuer,
private string $mojangPublicKeyDer,
private string $clientDataJwt,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
try{
$this->clientPublicKeyDer = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
$claims = AuthJwtHelper::validateOpenIdAuthToken($this->jwt, $this->mojangPublicKeyDer, issuer: $this->issuer, audience: self::MOJANG_AUDIENCE);
//validateToken will throw if the JWT is not valid
$this->authenticated = true;
$clientDerKey = base64_decode($claims->cpk, strict: true);
if($clientDerKey === false){
throw new VerifyLoginException("Invalid client public key: base64 error decoding");
}
//no further validation needed - OpenSSL will bail if the key is invalid
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $clientDerKey);
return $clientDerKey;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
}
}

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\cache; namespace pocketmine\network\mcpe\cache;
use pmmp\encoding\BE;
use pocketmine\crafting\CraftingManager; use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\FurnaceType; use pocketmine\crafting\FurnaceType;
use pocketmine\crafting\ShapedRecipe; use pocketmine\crafting\ShapedRecipe;
@@ -41,7 +42,6 @@ use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShaped
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe; use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\SingletonTrait; use pocketmine\utils\SingletonTrait;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use function array_map; use function array_map;
@@ -99,7 +99,7 @@ final class CraftingDataCache{
}; };
$recipesWithTypeIds[] = new ProtocolShapelessRecipe( $recipesWithTypeIds[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS, CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt($recipeNetId), BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable
array_map($converter->coreRecipeIngredientToNet(...), $recipe->getIngredientList()), array_map($converter->coreRecipeIngredientToNet(...), $recipe->getIngredientList()),
array_map($converter->coreItemStackToNet(...), $recipe->getResults()), array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
$nullUUID, $nullUUID,
@@ -118,7 +118,7 @@ final class CraftingDataCache{
} }
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe( $recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
CraftingDataPacket::ENTRY_SHAPED, CraftingDataPacket::ENTRY_SHAPED,
Binary::writeInt($recipeNetId), BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable
$inputs, $inputs,
array_map($converter->coreItemStackToNet(...), $recipe->getResults()), array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
$nullUUID, $nullUUID,

View File

@@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert; namespace pocketmine\network\mcpe\convert;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\block\tile\ContainerTile; use pocketmine\block\tile\ContainerTile;
use pocketmine\block\VanillaBlocks; use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient; use pocketmine\crafting\ExactRecipeIngredient;
@@ -44,7 +46,6 @@ use pocketmine\nbt\tag\Tag;
use pocketmine\nbt\TreeRoot; use pocketmine\nbt\TreeRoot;
use pocketmine\nbt\UnexpectedTagTypeException; use pocketmine\nbt\UnexpectedTagTypeException;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode; use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData;
@@ -312,7 +313,7 @@ class TypeConverter{
$extraData = $id === $this->shieldRuntimeId ? $extraData = $id === $this->shieldRuntimeId ?
new ItemStackExtraDataShield($nbt, canPlaceOn: [], canDestroy: [], blockingTick: 0) : new ItemStackExtraDataShield($nbt, canPlaceOn: [], canDestroy: [], blockingTick: 0) :
new ItemStackExtraData($nbt, canPlaceOn: [], canDestroy: []); new ItemStackExtraData($nbt, canPlaceOn: [], canDestroy: []);
$extraDataSerializer = PacketSerializer::encoder(); $extraDataSerializer = new ByteBufferWriter();
$extraData->write($extraDataSerializer); $extraData->write($extraDataSerializer);
return new ItemStack( return new ItemStack(
@@ -320,7 +321,7 @@ class TypeConverter{
$meta, $meta,
$itemStack->getCount(), $itemStack->getCount(),
$blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID, $blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID,
$extraDataSerializer->getBuffer(), $extraDataSerializer->getData(),
); );
} }
@@ -359,7 +360,7 @@ class TypeConverter{
} }
public function deserializeItemStackExtraData(string $extraData, int $id) : ItemStackExtraData{ public function deserializeItemStackExtraData(string $extraData, int $id) : ItemStackExtraData{
$extraDataDeserializer = PacketSerializer::decoder($extraData, 0); $extraDataDeserializer = new ByteBufferReader($extraData);
return $id === $this->shieldRuntimeId ? return $id === $this->shieldRuntimeId ?
ItemStackExtraDataShield::read($extraDataDeserializer) : ItemStackExtraDataShield::read($extraDataDeserializer) :
ItemStackExtraData::read($extraDataDeserializer); ItemStackExtraData::read($extraDataDeserializer);

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\encryption; namespace pocketmine\network\mcpe\encryption;
use Crypto\Cipher; use Crypto\Cipher;
use pocketmine\utils\Binary; use pmmp\encoding\LE;
use function bin2hex; use function bin2hex;
use function openssl_digest; use function openssl_digest;
use function openssl_error_string; use function openssl_error_string;
@@ -104,7 +104,7 @@ class EncryptionContext{
} }
private function calculateChecksum(int $counter, string $payload) : string{ private function calculateChecksum(int $counter, string $payload) : string{
$hash = openssl_digest(Binary::writeLLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true); $hash = openssl_digest(LE::packUnsignedLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true);
if($hash === false){ if($hash === false){
throw new \RuntimeException("openssl_digest() error: " . openssl_error_string()); throw new \RuntimeException("openssl_digest() error: " . openssl_error_string());
} }

View File

@@ -27,7 +27,8 @@ use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent; use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable; use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\auth\ProcessLoginTask; use pocketmine\network\mcpe\auth\ProcessLegacyLoginTask;
use pocketmine\network\mcpe\auth\ProcessOpenIdLoginTask;
use pocketmine\network\mcpe\JwtException; use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\NetworkSession;
@@ -38,16 +39,23 @@ use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientData;
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper; use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain; use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData; use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtHeader;
use pocketmine\network\PacketHandlingException; use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\player\PlayerInfo; use pocketmine\player\PlayerInfo;
use pocketmine\player\XboxLivePlayerInfo; use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server; use pocketmine\Server;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function chr;
use function count;
use function gettype; use function gettype;
use function is_array; use function is_array;
use function is_object; use function is_object;
use function json_decode; use function json_decode;
use function md5;
use function ord;
use const JSON_THROW_ON_ERROR; use const JSON_THROW_ON_ERROR;
/** /**
@@ -65,15 +73,95 @@ class LoginPacketHandler extends PacketHandler{
private \Closure $authCallback private \Closure $authCallback
){} ){}
private static function calculateUuidFromXuid(string $xuid) : UuidInterface{
$hash = md5("pocket-auth-1-xuid:" . $xuid, binary: true);
$hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); // set version to 3
$hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); // set variant to RFC 4122
return Uuid::fromBytes($hash);
}
public function handleLogin(LoginPacket $packet) : bool{ public function handleLogin(LoginPacket $packet) : bool{
$authInfo = $this->parseAuthInfo($packet->authInfoJson); $authInfo = $this->parseAuthInfo($packet->authInfoJson);
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
$extraData = $this->fetchAuthData($jwtChain);
if(!Player::isValidUserName($extraData->displayName)){ if($authInfo->AuthenticationType === AuthenticationType::FULL->value){
try{
[$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e, "Error parsing authentication token");
}
$header = $this->mapXboxTokenHeader($headerArray);
$claims = $this->mapXboxTokenBody($claimsArray);
$legacyUuid = self::calculateUuidFromXuid($claims->xid);
$username = $claims->xname;
$xuid = $claims->xid;
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
if($authRequired === null){
//plugin cancelled
return true;
}
$this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired);
}elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){
try{
$chainData = json_decode($authInfo->Certificate, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate chain");
}
if(!is_object($chainData)){
throw new PacketHandlingException("Unexpected type for self-signed certificate chain: " . gettype($chainData) . ", expected object");
}
try{
$chain = $this->defaultJsonMapper()->map($chainData, new LegacyAuthChain());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate chain");
}
if(count($chain->chain) > 1 || !isset($chain->chain[0])){
throw new PacketHandlingException("Expected exactly one certificate in self-signed certificate chain, got " . count($chain->chain));
}
try{
[, $claimsArray, ] = JwtUtils::parse($chain->chain[0]);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate");
}
if(!isset($claimsArray["extraData"]) || !is_array($claimsArray["extraData"])){
throw new PacketHandlingException("Expected \"extraData\" to be present in self-signed certificate");
}
try{
$claims = $this->defaultJsonMapper()->map($claimsArray["extraData"], new LegacyAuthIdentityData());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate extraData");
}
if(!Uuid::isValid($claims->identity)){
throw new PacketHandlingException("Invalid UUID string in self-signed certificate: " . $claims->identity);
}
$legacyUuid = Uuid::fromString($claims->identity);
$username = $claims->displayName;
$xuid = "";
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
if($authRequired === null){
//plugin cancelled
return true;
}
$this->processSelfSignedLogin($chain->chain, $packet->clientDataJwt, $authRequired);
}else{
throw new PacketHandlingException("Unsupported authentication type: $authInfo->AuthenticationType");
}
return true;
}
private function processLoginCommon(LoginPacket $packet, string $username, UuidInterface $legacyUuid, string $xuid) : ?bool{
if(!Player::isValidUserName($username)){
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName()); $this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
return true; return null;
} }
$clientData = $this->parseClientData($packet->clientDataJwt); $clientData = $this->parseClientData($packet->clientDataJwt);
@@ -86,32 +174,25 @@ class LoginPacketHandler extends PacketHandler{
disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin() disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
); );
return true; return null;
} }
if(!Uuid::isValid($extraData->identity)){ if($xuid !== ""){
throw new PacketHandlingException("Invalid login UUID");
}
$uuid = Uuid::fromString($extraData->identity);
$arrClientData = (array) $clientData;
$arrClientData["TitleID"] = $extraData->titleId;
if($extraData->XUID !== ""){
$playerInfo = new XboxLivePlayerInfo( $playerInfo = new XboxLivePlayerInfo(
$extraData->XUID, $xuid,
$extraData->displayName, $username,
$uuid, $legacyUuid,
$skin, $skin,
$clientData->LanguageCode, $clientData->LanguageCode,
$arrClientData (array) $clientData
); );
}else{ }else{
$playerInfo = new PlayerInfo( $playerInfo = new PlayerInfo(
$extraData->displayName, $username,
$uuid, $legacyUuid,
$skin, $skin,
$clientData->LanguageCode, $clientData->LanguageCode,
$arrClientData (array) $clientData
); );
} }
($this->playerInfoConsumer)($playerInfo); ($this->playerInfoConsumer)($playerInfo);
@@ -144,12 +225,10 @@ class LoginPacketHandler extends PacketHandler{
$ev->call(); $ev->call();
if(!$ev->isAllowed()){ if(!$ev->isAllowed()){
$this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage()); $this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
return true; return null;
} }
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired()); return $ev->isAuthRequired();
return true;
} }
/** /**
@@ -162,13 +241,10 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }
if(!is_object($authInfoJson)){ if(!is_object($authInfoJson)){
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object"); throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
} }
$mapper = new \JsonMapper(); $mapper = $this->defaultJsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{ try{
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo()); $clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
@@ -178,68 +254,31 @@ class LoginPacketHandler extends PacketHandler{
} }
/** /**
* @param array<string, mixed> $headerArray
* @throws PacketHandlingException * @throws PacketHandlingException
*/ */
protected function parseJwtChain(string $chainDataJwt) : LegacyAuthChain{ protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{
$mapper = $this->defaultJsonMapper();
try{ try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR); $header = $mapper->map($headerArray, new XboxAuthJwtHeader());
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($jwtChainJson)){
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($jwtChainJson, new LegacyAuthChain());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }
return $clientData; return $header;
} }
/** /**
* @param array<string, mixed> $bodyArray
* @throws PacketHandlingException * @throws PacketHandlingException
*/ */
protected function fetchAuthData(LegacyAuthChain $chain) : LegacyAuthIdentityData{ protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{
/** @var LegacyAuthIdentityData|null $extraData */ $mapper = $this->defaultJsonMapper();
$extraData = null; try{
foreach($chain->chain as $jwt){ $header = $mapper->map($bodyArray, new XboxAuthJwtBody());
//validate every chain element }catch(\JsonMapper_Exception $e){
try{ throw PacketHandlingException::wrap($e);
[, $claims, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e);
}
if(isset($claims["extraData"])){
if($extraData !== null){
throw new PacketHandlingException("Found 'extraData' more than once in chainData");
}
if(!is_array($claims["extraData"])){
throw new PacketHandlingException("'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;
$mapper->bStrictObjectTypeChecking = true;
try{
/** @var LegacyAuthIdentityData $extraData */
$extraData = $mapper->map($claims["extraData"], new LegacyAuthIdentityData());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
}
} }
if($extraData === null){ return $header;
throw new PacketHandlingException("'extraData' not found in chain data");
}
return $extraData;
} }
/** /**
@@ -252,11 +291,7 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }
$mapper = new \JsonMapper(); $mapper = $this->defaultJsonMapper();
$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;
$mapper->bStrictObjectTypeChecking = true;
try{ try{
$clientData = $mapper->map($clientDataClaims, new ClientData()); $clientData = $mapper->map($clientDataClaims, new ClientData());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
@@ -269,15 +304,37 @@ class LoginPacketHandler extends PacketHandler{
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication. * 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. * In the future this won't be necessary.
* *
* @param null|string[] $legacyCertificate
*
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{ protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{
if($legacyCertificate === null){
throw new PacketHandlingException("Legacy certificate cannot be null");
}
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
$this->session->setHandler(null); //drop packets received during login verification $this->session->setHandler(null); //drop packets received during login verification
$authKeyProvider = $this->server->getAuthKeyProvider();
$authKeyProvider->getKey($keyId)->onCompletion(
function(array $issuerAndKey) use ($token, $clientData, $authRequired) : void{
[$issuer, $mojangPublicKeyPem] = $issuerAndKey;
$this->server->getAsyncPool()->submitTask(new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback));
},
fn() => ($this->authCallback)(false, $authRequired, "Unrecognized authentication key ID: $keyId", null)
);
}
/**
* @param string[] $legacyCertificate
*/
protected function processSelfSignedLogin(array $legacyCertificate, string $clientDataJwt, bool $authRequired) : void{
$this->session->setHandler(null); //drop packets received during login verification
$this->server->getAsyncPool()->submitTask(new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: null, authRequired: $authRequired, onCompletion: $this->authCallback));
}
private function defaultJsonMapper() : \JsonMapper{
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
return $mapper;
} }
} }

View File

@@ -23,16 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\serializer; namespace pocketmine\network\mcpe\serializer;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\VarInt;
use pocketmine\block\tile\Spawnable; use pocketmine\block\tile\Spawnable;
use pocketmine\data\bedrock\BiomeIds; use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap; use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap;
use pocketmine\nbt\TreeRoot; use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\convert\BlockTranslator; use pocketmine\network\mcpe\convert\BlockTranslator;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\types\DimensionIds; use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk; use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk; use pocketmine\world\format\SubChunk;
@@ -84,7 +84,7 @@ final class ChunkSerializer{
* @phpstan-param DimensionIds::* $dimensionId * @phpstan-param DimensionIds::* $dimensionId
*/ */
public static function serializeFullChunk(Chunk $chunk, int $dimensionId, BlockTranslator $blockTranslator, ?string $tiles = null) : string{ public static function serializeFullChunk(Chunk $chunk, int $dimensionId, BlockTranslator $blockTranslator, ?string $tiles = null) : string{
$stream = PacketSerializer::encoder(); $stream = new ByteBufferWriter();
$subChunkCount = self::getSubChunkCount($chunk, $dimensionId); $subChunkCount = self::getSubChunkCount($chunk, $dimensionId);
$writtenCount = 0; $writtenCount = 0;
@@ -100,37 +100,34 @@ final class ChunkSerializer{
self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream); self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream);
} }
$stream->putByte(0); //border block array count Byte::writeUnsigned($stream, 0); //border block array count
//Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client. //Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client.
if($tiles !== null){ if($tiles !== null){
$stream->put($tiles); $stream->writeByteArray($tiles);
}else{ }else{
$stream->put(self::serializeTiles($chunk)); $stream->writeByteArray(self::serializeTiles($chunk));
} }
return $stream->getBuffer(); return $stream->getData();
} }
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{ public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, ByteBufferWriter $stream, bool $persistentBlockStates) : void{
$layers = $subChunk->getBlockLayers(); $layers = $subChunk->getBlockLayers();
$stream->putByte(8); //version Byte::writeUnsigned($stream, 8); //version
$stream->putByte(count($layers)); Byte::writeUnsigned($stream, count($layers));
$blockStateDictionary = $blockTranslator->getBlockStateDictionary(); $blockStateDictionary = $blockTranslator->getBlockStateDictionary();
foreach($layers as $blocks){ foreach($layers as $blocks){
$bitsPerBlock = $blocks->getBitsPerBlock(); $bitsPerBlock = $blocks->getBitsPerBlock();
$words = $blocks->getWordArray(); $words = $blocks->getWordArray();
$stream->putByte(($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1)); Byte::writeUnsigned($stream, ($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1));
$stream->put($words); $stream->writeByteArray($words);
$palette = $blocks->getPalette(); $palette = $blocks->getPalette();
if($bitsPerBlock !== 0){ if($bitsPerBlock !== 0){
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here VarInt::writeSignedInt($stream, count($palette)); //yes, this is intentionally zigzag
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$stream->putUnsignedVarInt(count($palette) << 1); //yes, this is intentionally zigzag
} }
if($persistentBlockStates){ if($persistentBlockStates){
$nbtSerializer = new NetworkNbtSerializer(); $nbtSerializer = new NetworkNbtSerializer();
@@ -141,46 +138,43 @@ final class ChunkSerializer{
$state = $blockTranslator->getFallbackStateData(); $state = $blockTranslator->getFallbackStateData();
} }
$stream->put($nbtSerializer->write(new TreeRoot($state->toNbt()))); $stream->writeByteArray($nbtSerializer->write(new TreeRoot($state->toNbt())));
} }
}else{ }else{
//we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of
//allocating a temporary array for the mapped palette IDs, especially for small palettes
foreach($palette as $p){ foreach($palette as $p){
$stream->put(Binary::writeUnsignedVarInt($blockTranslator->internalIdToNetworkId($p) << 1)); VarInt::writeSignedInt($stream, $blockTranslator->internalIdToNetworkId($p));
} }
} }
} }
} }
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $stream) : void{ private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, ByteBufferWriter $stream) : void{
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock(); $biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
$stream->putByte(($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs Byte::writeUnsigned($stream, ($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
$stream->put($biomePalette->getWordArray()); $stream->writeByteArray($biomePalette->getWordArray());
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$biomePaletteArray = $biomePalette->getPalette(); $biomePaletteArray = $biomePalette->getPalette();
if($biomePaletteBitsPerBlock !== 0){ if($biomePaletteBitsPerBlock !== 0){
$stream->putUnsignedVarInt(count($biomePaletteArray) << 1); VarInt::writeSignedInt($stream, count($biomePaletteArray));
} }
foreach($biomePaletteArray as $p){ foreach($biomePaletteArray as $p){
if($biomeIdMap->legacyToString($p) === null){ //we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this //allocating a temporary array for the mapped palette IDs, especially for small palettes
$p = BiomeIds::OCEAN; VarInt::writeSignedInt($stream, $biomeIdMap->legacyToString($p) !== null ? $p : BiomeIds::OCEAN);
}
$stream->put(Binary::writeUnsignedVarInt($p << 1));
} }
} }
public static function serializeTiles(Chunk $chunk) : string{ public static function serializeTiles(Chunk $chunk) : string{
$stream = new BinaryStream(); $stream = new ByteBufferWriter();
foreach($chunk->getTiles() as $tile){ foreach($chunk->getTiles() as $tile){
if($tile instanceof Spawnable){ if($tile instanceof Spawnable){
$stream->put($tile->getSerializedSpawnCompound()->getEncodedNbt()); $stream->writeByteArray($tile->getSerializedSpawnCompound()->getEncodedNbt());
} }
} }
return $stream->getBuffer(); return $stream->getData();
} }
} }

View File

@@ -27,16 +27,16 @@ declare(strict_types=1);
*/ */
namespace pocketmine\network\query; namespace pocketmine\network\query;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\DataDecodeException;
use pocketmine\network\AdvancedNetworkInterface; use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\RawPacketHandler; use pocketmine\network\RawPacketHandler;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use function chr;
use function hash; use function hash;
use function random_bytes; use function random_bytes;
use function strlen;
use function substr; use function substr;
class QueryHandler implements RawPacketHandler{ class QueryHandler implements RawPacketHandler{
@@ -80,51 +80,53 @@ class QueryHandler implements RawPacketHandler{
} }
public static function getTokenString(string $token, string $salt) : int{ public static function getTokenString(string $token, string $salt) : int{
return Binary::readInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4)); return BE::unpackSignedInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4));
} }
public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{ public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{
try{ try{
$stream = new BinaryStream($packet); $stream = new ByteBufferReader($packet);
$header = $stream->get(2); $header = $stream->readByteArray(2);
if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above
return false; return false;
} }
$packetType = $stream->getByte(); $packetType = Byte::readUnsigned($stream);
$sessionID = $stream->getInt(); $sessionID = BE::readUnsignedInt($stream);
switch($packetType){ switch($packetType){
case self::HANDSHAKE: //Handshake case self::HANDSHAKE: //Handshake
$reply = chr(self::HANDSHAKE); $writer = new ByteBufferWriter();
$reply .= Binary::writeInt($sessionID); Byte::writeUnsigned($writer, self::HANDSHAKE);
$reply .= self::getTokenString($this->token, $address) . "\x00"; BE::writeUnsignedInt($writer, $sessionID);
$writer->writeByteArray(self::getTokenString($this->token, $address) . "\x00");
$interface->sendRawPacket($address, $port, $reply); $interface->sendRawPacket($address, $port, $writer->getData());
return true; return true;
case self::STATISTICS: //Stat case self::STATISTICS: //Stat
$token = $stream->getInt(); $token = BE::readUnsignedInt($stream);
if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){ if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){
$this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2"); $this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2");
return true; return true;
} }
$reply = chr(self::STATISTICS); $writer = new ByteBufferWriter();
$reply .= Binary::writeInt($sessionID); Byte::writeUnsigned($writer, self::STATISTICS);
BE::writeUnsignedInt($writer, $sessionID);
$remaining = $stream->getRemaining(); $remaining = $stream->getUnreadLength();
if(strlen($remaining) === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01 if($remaining === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01
$reply .= $this->server->getQueryInformation()->getLongQuery(); $writer->writeByteArray($this->server->getQueryInformation()->getLongQuery());
}else{ }else{
$reply .= $this->server->getQueryInformation()->getShortQuery(); $writer->writeByteArray($this->server->getQueryInformation()->getShortQuery());
} }
$interface->sendRawPacket($address, $port, $reply); $interface->sendRawPacket($address, $port, $writer->getData());
return true; return true;
default: default:
return false; return false;
} }
}catch(BinaryDataException $e){ }catch(DataDecodeException $e){
$this->logger->debug("Bad packet from $address $port: " . $e->getMessage()); $this->logger->debug("Bad packet from $address $port: " . $e->getMessage());
return false; return false;
} }

View File

@@ -23,11 +23,11 @@ declare(strict_types=1);
namespace pocketmine\network\query; namespace pocketmine\network\query;
use pmmp\encoding\LE;
use pocketmine\player\GameMode; use pocketmine\player\GameMode;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\plugin\Plugin; use pocketmine\plugin\Plugin;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\utils\Binary;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use pocketmine\YmlServerProperties; use pocketmine\YmlServerProperties;
use function array_map; use function array_map;
@@ -236,6 +236,6 @@ final class QueryInfo{
} }
public function getShortQuery() : string{ public function getShortQuery() : string{
return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . Binary::writeLShort($this->port) . $this->ip . "\x00"); return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . LE::packUnsignedShort($this->port) . $this->ip . "\x00");
} }
} }

View File

@@ -88,7 +88,7 @@ class SendUsageTask extends AsyncTask{
"phpVersion" => PHP_VERSION, "phpVersion" => PHP_VERSION,
"machine" => php_uname("a"), "machine" => php_uname("a"),
"release" => php_uname("r"), "release" => php_uname("r"),
"platform" => php_uname("i") "platform" => php_uname("m")
]; ];
$data["players"] = [ $data["players"] = [

View File

@@ -23,8 +23,10 @@ declare(strict_types=1);
namespace pocketmine\world\format\io; namespace pocketmine\world\format\io;
use pocketmine\utils\Binary; use pmmp\encoding\BE;
use pocketmine\utils\BinaryStream; use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\world\format\Chunk; use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk; use pocketmine\world\format\SubChunk;
@@ -45,15 +47,15 @@ final class FastChunkSerializer{
//NOOP //NOOP
} }
private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array) : void{ private static function serializePalettedArray(ByteBufferWriter $stream, PalettedBlockArray $array) : void{
$wordArray = $array->getWordArray(); $wordArray = $array->getWordArray();
$palette = $array->getPalette(); $palette = $array->getPalette();
$stream->putByte($array->getBitsPerBlock()); Byte::writeUnsigned($stream, $array->getBitsPerBlock());
$stream->put($wordArray); $stream->writeByteArray($wordArray);
$serialPalette = pack("L*", ...$palette); $serialPalette = pack("L*", ...$palette);
$stream->putInt(strlen($serialPalette)); BE::writeUnsignedInt($stream, strlen($serialPalette));
$stream->put($serialPalette); $stream->writeByteArray($serialPalette);
} }
/** /**
@@ -61,21 +63,20 @@ final class FastChunkSerializer{
* TODO: tiles and entities * TODO: tiles and entities
*/ */
public static function serializeTerrain(Chunk $chunk) : string{ public static function serializeTerrain(Chunk $chunk) : string{
$stream = new BinaryStream(); $stream = new ByteBufferWriter();
$stream->putByte( Byte::writeUnsigned($stream, ($chunk->isPopulated() ? self::FLAG_POPULATED : 0));
($chunk->isPopulated() ? self::FLAG_POPULATED : 0)
);
//subchunks //subchunks
$subChunks = $chunk->getSubChunks(); $subChunks = $chunk->getSubChunks();
$count = count($subChunks); $count = count($subChunks);
$stream->putByte($count); Byte::writeUnsigned($stream, $count);
foreach($subChunks as $y => $subChunk){ foreach($subChunks as $y => $subChunk){
$stream->putByte($y); Byte::writeSigned($stream, $y);
$stream->putInt($subChunk->getEmptyBlockId()); BE::writeUnsignedInt($stream, $subChunk->getEmptyBlockId());
$layers = $subChunk->getBlockLayers(); $layers = $subChunk->getBlockLayers();
$stream->putByte(count($layers)); Byte::writeUnsigned($stream, count($layers));
foreach($layers as $blocks){ foreach($layers as $blocks){
self::serializePalettedArray($stream, $blocks); self::serializePalettedArray($stream, $blocks);
} }
@@ -83,14 +84,15 @@ final class FastChunkSerializer{
} }
return $stream->getBuffer(); return $stream->getData();
} }
private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{ private static function deserializePalettedArray(ByteBufferReader $stream) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte(); $bitsPerBlock = Byte::readUnsigned($stream);
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); $words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
$paletteSize = BE::readUnsignedInt($stream);
/** @var int[] $unpackedPalette */ /** @var int[] $unpackedPalette */
$unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here $unpackedPalette = unpack("L*", $stream->readByteArray($paletteSize)); //unpack() will never fail here
$palette = array_values($unpackedPalette); $palette = array_values($unpackedPalette);
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
@@ -100,20 +102,21 @@ final class FastChunkSerializer{
* Deserializes a fast-serialized chunk * Deserializes a fast-serialized chunk
*/ */
public static function deserializeTerrain(string $data) : Chunk{ public static function deserializeTerrain(string $data) : Chunk{
$stream = new BinaryStream($data); $stream = new ByteBufferReader($data);
$flags = $stream->getByte(); $flags = Byte::readUnsigned($stream);
$terrainPopulated = (bool) ($flags & self::FLAG_POPULATED); $terrainPopulated = (bool) ($flags & self::FLAG_POPULATED);
$subChunks = []; $subChunks = [];
$count = $stream->getByte(); $count = Byte::readUnsigned($stream);
for($subCount = 0; $subCount < $count; ++$subCount){ for($subCount = 0; $subCount < $count; ++$subCount){
$y = Binary::signByte($stream->getByte()); $y = Byte::readSigned($stream);
$airBlockId = $stream->getInt(); //TODO: why the heck are we using big-endian here?
$airBlockId = BE::readUnsignedInt($stream);
$layers = []; $layers = [];
for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){
$layers[] = self::deserializePalettedArray($stream); $layers[] = self::deserializePalettedArray($stream);
} }
$biomeArray = self::deserializePalettedArray($stream); $biomeArray = self::deserializePalettedArray($stream);

View File

@@ -27,7 +27,7 @@ use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent; use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class TridentHitGroundSound implements Sound{ class TridentHitBlockSound implements Sound{
public function encode(Vector3 $pos) : array{ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT_GROUND, $pos, false)]; return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT_GROUND, $pos, false)];

View File

@@ -822,12 +822,6 @@ parameters:
count: 1 count: 1
path: ../../../src/network/mcpe/NetworkSession.php path: ../../../src/network/mcpe/NetworkSession.php
-
message: '#^Property pocketmine\\network\\mcpe\\auth\\ProcessLoginTask\:\:\$chain \(string\) does not accept string\|null\.$#'
identifier: assign.propertyType
count: 1
path: ../../../src/network/mcpe/auth/ProcessLoginTask.php
- -
message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#' message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#'
identifier: argument.type identifier: argument.type

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\tools\generate_bedrock_data_from_packets; namespace pocketmine\tools\generate_bedrock_data_from_packets;
use pmmp\encoding\ByteBufferReader;
use pocketmine\crafting\json\FurnaceRecipeData; use pocketmine\crafting\json\FurnaceRecipeData;
use pocketmine\crafting\json\ItemStackData; use pocketmine\crafting\json\ItemStackData;
use pocketmine\crafting\json\PotionContainerChangeRecipeData; use pocketmine\crafting\json\PotionContainerChangeRecipeData;
@@ -51,7 +52,6 @@ use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\ItemRegistryPacket; use pocketmine\network\mcpe\protocol\ItemRegistryPacket;
use pocketmine\network\mcpe\protocol\PacketPool; use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\StartGamePacket; use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
@@ -190,7 +190,7 @@ class ParserPacketHandler extends PacketHandler{
$rawExtraData = $itemStack->getRawExtraData(); $rawExtraData = $itemStack->getRawExtraData();
if($rawExtraData !== ""){ if($rawExtraData !== ""){
$decoder = PacketSerializer::decoder($rawExtraData, 0); $decoder = new ByteBufferReader($rawExtraData);
$extraData = $itemStringId === ItemTypeNames::SHIELD ? ItemStackExtraDataShield::read($decoder) : ItemStackExtraData::read($decoder); $extraData = $itemStringId === ItemTypeNames::SHIELD ? ItemStackExtraDataShield::read($decoder) : ItemStackExtraData::read($decoder);
$nbt = $extraData->getNbt(); $nbt = $extraData->getNbt();
if($nbt !== null && count($nbt) > 0){ if($nbt !== null && count($nbt) > 0){
@@ -645,12 +645,13 @@ function main(array $argv) : int{
fwrite(STDERR, "Unknown packet on line " . ($lineNum + 1) . ": " . $parts[1]); fwrite(STDERR, "Unknown packet on line " . ($lineNum + 1) . ": " . $parts[1]);
continue; continue;
} }
$serializer = PacketSerializer::decoder($raw, 0); $serializer = new ByteBufferReader($raw);
$pk->decode($serializer); $pk->decode($serializer);
$pk->handle($handler); $pk->handle($handler);
if(!$serializer->feof()){ $remaining = strlen($serializer->getData()) - $serializer->getOffset();
echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getBuffer()) . " bytes): " . bin2hex($serializer->getRemaining()) . "\n"; if($remaining > 0){
echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getData()) . " bytes): " . bin2hex($serializer->readByteArray($remaining)) . "\n";
} }
} }
return 0; return 0;