Compare commits

..

31 Commits

Author SHA1 Message Date
95bbca7c33 Remove more unnecessary changes 2025-09-09 22:06:56 +01:00
1bdd1c380d Fix confusing variable names 2025-09-09 22:05:19 +01:00
2d7d2223c0 Undo unnecessary change 2025-09-09 22:05:02 +01:00
fecdbe6bd2 stable protocol package 2025-09-09 21:54:29 +01:00
f913f79da1 Issuer and audience are now required to be set 2025-09-09 21:48:15 +01:00
b867e21be2 Restore self-signed login capability, and refactor a bunch of code 2025-09-09 21:42:20 +01:00
87ffb9ac89 stfu 2025-09-08 21:23:14 +01:00
82dceaaf38 Reworked a bunch of auth code 2025-09-08 21:20:55 +01:00
094196cbf8 Fix PHPStan 2025-09-07 00:23:54 +02:00
c5d017208d Rename variables from manager to provider 2025-09-06 23:46:11 +02:00
b3170913f7 Initial attempt to support new authentication 2025-09-06 23:44:25 +02:00
dc04992ba9 Merge branch 'stable' of github.com:pmmp/PocketMine-MP into stable 2025-09-05 19:25:31 +01:00
bddab47ee8 Bump build/php from ce1b095 to b839e52 (#6795) 2025-09-05 10:32:17 +00:00
3411103e11 Remove dead PHPStan ignores 2025-09-04 23:29:55 +01:00
1868536916 Add PHP 8.4 to test matrix 2025-09-04 21:58:12 +01:00
9a0a8a55b1 Bump shivammathur/setup-php in the github-actions group (#6787) 2025-09-02 13:17:41 +00:00
09cc76ae2b 5.33.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/17351431906
2025-08-31 02:28:17 +00:00
a540de1e3c Prepare 5.33.1 patch release (#6784) 2025-08-31 03:27:21 +01:00
9eee1a9a6e Banner: don't bail on missing type tags
we didn't set these prior to 5.33.0, so these won't be present on older worlds.
2025-08-31 03:22:58 +01:00
f673159471 5.33.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/17346780638
2025-08-30 17:46:07 +00:00
831c5a0464 Merge pull request #6783 from pmmp/r5.33.0
Release 5.33.0
2025-08-30 18:45:11 +01:00
5c363965f0 Fix build date
we really need a better way to deal with this
2025-08-30 18:43:18 +01:00
95679b5a29 Update BedrockData and some transient deps 2025-08-30 18:36:42 +01:00
f1b1e1977e Harden validation for server auth block breaking 2025-08-29 20:37:29 +01:00
23d612f1af Suggested additions 2025-08-29 18:49:08 +01:00
8f7e16a9ad Prepare 5.33.0 release 2025-08-29 14:11:50 +01:00
beaedc3627 Tidy up in block properties aisle 2025-08-29 13:07:09 +01:00
48ba334218 CS again :< 2025-08-29 12:33:50 +01:00
0be15a7403 Rename MultiFacing -> MultiAnyFacing
to match the trait name
2025-08-29 12:33:04 +01:00
2404d63b1f Ageable: added getMaxAge()
we'll probably need this...
2025-08-29 12:24:24 +01:00
dd9030f1f5 tools/generate-bedrock-data-from-packets: generate less noise for items
if we have only a name (the majority case), we can just return the name directly instead of an object.
this massively reduces the amount of noise in the files as seen in pmmp/BedrockData@f814036229
2025-08-28 21:15:09 +01:00
53 changed files with 1392 additions and 463 deletions

View File

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v5
- name: Setup PHP and tools
uses: shivammathur/setup-php@2.35.3
uses: shivammathur/setup-php@2.35.4
with:
php-version: 8.2

View File

@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@v5
- name: Setup PHP
uses: shivammathur/setup-php@2.35.3
uses: shivammathur/setup-php@2.35.4
with:
php-version: 8.2

View File

@ -87,7 +87,7 @@ jobs:
submodules: true
- name: Setup PHP
uses: shivammathur/setup-php@2.35.3
uses: shivammathur/setup-php@2.35.4
with:
php-version: ${{ env.PHP_VERSION }}

View File

@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ["8.1", "8.2", "8.3"]
php: ["8.1", "8.2", "8.3", "8.4"]
uses: ./.github/workflows/main-php-matrix.yml
with:
@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v5
- name: Setup PHP and tools
uses: shivammathur/setup-php@2.35.3
uses: shivammathur/setup-php@2.35.4
with:
php-version: 8.3
tools: php-cs-fixer:3.75

135
changelogs/5.33.md Normal file
View File

@ -0,0 +1,135 @@
# 5.33.0
Released 30th August 2025.
This is a minor feature release containing internals improvements, API improvements and new gameplay features.
**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.
## Performance
- Worlds now remember when a chunk isn't generated. This reduces world I/O during world generation.
- `BlockObjectToStateSerializer` now creates fewer objects in certain cases.
## Gameplay
- The following blocks have been added and/or are now properly supported:
- Hanging signs
- Illager banners
## Tools
- `generate-bedrock-data-from-packets.php` now represents items as strings directly when only an ID is present. This significantly improves readability in `BedrockData` and reduces file sizes.
## API
### `pocketmine\block`
- Added (and implemented) interfaces for many common block properties, to allow `instanceof` to be used:
- `Ageable`: for blocks with age, such as crops
- `AnyFacing`: for blocks which can face up, down, and horizontal directions (not the same as `HorizontalFacing`!)
- `Colored`: for blocks with 16 `DyeColor` variants
- `CoralMaterial`: for coral blocks, provides access to coral type and dead/alive
- `HorizontalFacing`: for blocks which can **only** face horizontal directions (not the same as `AnyFacing`!)
- `Lightable`: for light-source blocks which can be turned on and off, e.g. redstone lamp
- `MultiAnyFacing`: for blocks which can appear in multiple faces of the same block (including up, down, and horizontal faces), e.g. glow lichen
- `PillarRotation`: for blocks which can be oriented on an axis, e.g. logs
- `PoweredByRedstone`: for blocks which receive power from a redstone component, e.g. redstone lamp
- `SignLikeRotation`: for blocks which can be rotated 16 ways, e.g. signs, banners
- `WoodMaterial`: for blocks made from wood
- These interfaces have been implemented on many blocks. For the sake of brevity, they are not listed here, but you can expect to see them wherever the corresponding traits were used.
- The following classes have been added:
- `BaseOminousBanner`
- `CeilingCenterHangingSign` - both chains connected to the same point on the block above, can face 16 directions
- `CeilingEdgesHangingSign` - each chain connected to separate edges of the block above, can face 4 directions
- `OminousFloorBanner` - floor version of illager banner, can face 16 directions
- `OminousWallBanner` - wall version of illager banner, can face 4 directions
- `WallHangingSign` - hangs from a horizontal beam, can face 4 directions
- The following API methods have been added:
- `public ChiseledBookshelf->setSlots(list<ChiseledBookshelfSlot> $slots) : $this`
- `public static VanillaBlocks` methods:
- `ACACIA_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `ACACIA_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `ACACIA_WALL_HANGING_SIGN() : WallHangingSign`
- `BIRCH_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `BIRCH_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `BIRCH_WALL_HANGING_SIGN() : WallHangingSign`
- `CHERRY_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `CHERRY_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `CHERRY_WALL_HANGING_SIGN() : WallHangingSign`
- `CRIMSON_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `CRIMSON_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `CRIMSON_WALL_HANGING_SIGN() : WallHangingSign`
- `DARK_OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `DARK_OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `DARK_OAK_WALL_HANGING_SIGN() : WallHangingSign`
- `JUNGLE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `JUNGLE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `JUNGLE_WALL_HANGING_SIGN() : WallHangingSign`
- `MANGROVE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `MANGROVE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `MANGROVE_WALL_HANGING_SIGN() : WallHangingSign`
- `OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `OAK_WALL_HANGING_SIGN() : WallHangingSign`
- `OMINOUS_FLOOR_BANNER() : OminousFloorBanner`
- `OMINOUS_WALL_BANNER() : OminousWallBanner`
- `PALE_OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `PALE_OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `PALE_OAK_WALL_HANGING_SIGN() : WallHangingSign`
- `SPRUCE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `SPRUCE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `SPRUCE_WALL_HANGING_SIGN() : WallHangingSign`
- `WARPED_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign`
- `WARPED_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign`
- `WARPED_WALL_HANGING_SIGN() : WallHangingSign`
- `public AgeableTrait->getMaxAge() : int` (included by all growable plant-like blocks, e.g. crops)
### `pocketmine\data\bedrock\block\convert`
- A new system for symmetric block serializers and deserializers has been introduced.
- This allows registering both a serializer and a deserializer with the same code, meaning way less code
- It also eliminates information duplication and potential inconsistencies, improving maintainability.
- A proper way to deal with flattened IDs (e.g. color blocks) has been introduced which _doesn't_ require hardcoding a giant mess of IDs
- This symmetric system covers 99% of blocks which have a 1:1 association between PM and vanilla blocks, or 1:N where IDs are flattened
- However, there are still some special cases which require registering separate serializers and deserializers (usually in cases where the PM implementation deviates from Mojang where Mojang's implementation sucks, such as hanging signs or big dripleaf).
- No backwards compatibility breaks are expected as a result of this change. However, it's recommended to migrate old code to this new system for maintainability.
- The following new classes have been added:
- `BlockSerializerDeserializerRegistrar` - handles unified registration of block serializers and deserializers, based on a provided block model
- `FlattenedIdModel` - represents a block with some properties baked into its Minecraft ID, e.g. coral or color blocks
- `Model` - represents a regular block with all properties in its `states` NBT
- `property\BoolFromStringProperty<TBlock>` - property mapping a bool value from a string NBT state
- `property\BoolProperty<TBlock>`
- `property\CommonProperties` - singleton containing commonly-used block property definitions and groups, e.g. facing, stair properties
- `property\EnumFromRawStateMap<TEnum, TRaw>` - maps a raw NBT value to a PHP `enum` and vice versa
- `property\IntFromRawStateMap<TRaw>` - maps a raw NBT value to PM integer constants and vice versa
- `property\IntProperty<TBlock>` - an integer range property with a min, max, and optional offset
- `property\Property<TBlock>` - interface implemented by all property definitions accepted by a `Model` or `FlattenedIdModel`
- `property\StateMap<TValue, TRaw>` - interface implemented by classes accepted by mapping properties, e.g. `BoolFromStringProperty`
- `property\StringProperty<TBlock>` - interface implemented by properties whose raw outputs are strings - these can be used as ID components in `FlattenedIdModel`
- `property\ValueFromIntProperty<TBlock, TValue>` - property mapping a generic PM value from an int NBT state
- `property\ValueFromStringProperty<TBlock, TValue>` - same as above, but for a string NBT state
- `property\ValueSetFromIntProperty<TBlock, TOption>` - a property mapping an `int[]` or `enum[]` from a set of flags in NBT states
- `property\ValueMappings` - singleton containing commonly-needed `StateMap`s
- The following classes have been deprecated:
- `BlockStateDeserializerHelper`
- `BlockStateSerializerHelper`
- The following methods have been deprecated:
- All methods for decoding mapped property types in `BlockStateReader`, e.g. `readFacingDirection()`
- All methods for encoding mapped property types in `BlockStateWriter`, e.g. `writeFacingDirection()`
- All specific blocktype mapping functions in `BlockStateToObjectDeserializer`, e.g. `mapStairs()`
- All specific blocktype mapping functions in `BlockObjectToStateSerializer`, e.g. `mapStairs()`
### `pocketmine\item`
- The following hooks have been added:
- `public Item->getPlacementTransaction(Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : ?BlockTransaction` - allows more complex logic for itemblocks to place blocks, without duplicating their placement conditions (used for hanging signs)
### `pocketmine\world`
- `World->setChunk()` now verifies that blockstate IDs in the provided chunk are all registered in `RuntimeBlockStateRegistry`. This should provide earlier detection for custom block registration errors by plugins.
## Internals
- `BlockStateUpgrader` is now almost entirely independent from `BlockStateData`. It's anticipated that the upgrader library will be separable from the core in the future.
- `Block->readStateFromWorld()` is now triggered on chunk load for any position containing a tile. This should allow more effective updating of blocks with properties in their tiles.
# 5.33.1
Released 31st August 2025.
## Fixes
- Fixed banners placed in prior versions getting their tiles deleted (due to missing `Type` tags).

View File

@ -34,9 +34,9 @@
"adhocore/json-comment": "~1.2.0",
"netresearch/jsonmapper": "~v5.0.0",
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~5.3.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-protocol": "~40.0.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~41.0.0+bedrock-1.21.100",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0",

111
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "402ad5667b1e636a8ec6acf2f1b5f055",
"content-hash": "7bf7cd54642c2d65ecdfdcb28f3a64a8",
"packages": [
{
"name": "adhocore/json-comment",
@ -204,16 +204,16 @@
},
{
"name": "pocketmine/bedrock-data",
"version": "5.3.0+bedrock-1.21.100",
"version": "6.0.0+bedrock-1.21.100",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "5279e76261df158d5af187cfaafc1618c1da9e3f"
"reference": "edc0d829175e5e1e57c87001acfd03526c63fd34"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/5279e76261df158d5af187cfaafc1618c1da9e3f",
"reference": "5279e76261df158d5af187cfaafc1618c1da9e3f",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/edc0d829175e5e1e57c87001acfd03526c63fd34",
"reference": "edc0d829175e5e1e57c87001acfd03526c63fd34",
"shasum": ""
},
"type": "library",
@ -224,9 +224,9 @@
"description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/5.3.0+bedrock-1.21.100"
"source": "https://github.com/pmmp/BedrockData/tree/6.0.0+bedrock-1.21.100"
},
"time": "2025-07-30T22:07:56+00:00"
"time": "2025-08-30T17:25:42+00:00"
},
{
"name": "pocketmine/bedrock-item-upgrade-schema",
@ -256,16 +256,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "40.0.0+bedrock-1.21.100",
"version": "41.0.0+bedrock-1.21.100",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca"
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/920ac291fe1b0143b2ebc90b3374ddab0b8531bf",
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf",
"shasum": ""
},
"require": {
@ -296,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/40.0.0+bedrock-1.21.100"
"source": "https://github.com/pmmp/BedrockProtocol/tree/41.0.0+bedrock-1.21.100"
},
"time": "2025-08-06T15:13:45+00:00"
"time": "2025-09-09T20:52:18+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -896,16 +896,16 @@
},
{
"name": "symfony/filesystem",
"version": "v6.4.13",
"version": "v6.4.24",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3"
"reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3",
"reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8",
"reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8",
"shasum": ""
},
"require": {
@ -942,7 +942,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v6.4.13"
"source": "https://github.com/symfony/filesystem/tree/v6.4.24"
},
"funding": [
{
@ -953,27 +953,31 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-25T15:07:50+00:00"
"time": "2025-07-10T08:14:14+00:00"
}
],
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.13.1",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@ -1012,7 +1016,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@ -1020,20 +1024,20 @@
"type": "tidelift"
}
],
"time": "2025-04-29T12:36:36+00:00"
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.5.0",
"version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
"shasum": ""
},
"require": {
@ -1052,7 +1056,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
"dev-master": "5.x-dev"
}
},
"autoload": {
@ -1076,9 +1080,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
},
"time": "2025-05-31T08:24:38+00:00"
"time": "2025-08-13T20:13:15+00:00"
},
{
"name": "phar-io/manifest",
@ -1680,16 +1684,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.47",
"version": "10.5.53",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3"
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653",
"shasum": ""
},
"require": {
@ -1699,7 +1703,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.1",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@ -1716,7 +1720,7 @@
"sebastian/exporter": "^5.1.2",
"sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0",
"sebastian/recursion-context": "^5.0.0",
"sebastian/recursion-context": "^5.0.1",
"sebastian/type": "^4.0.0",
"sebastian/version": "^4.0.1"
},
@ -1761,7 +1765,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.47"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53"
},
"funding": [
{
@ -1785,7 +1789,7 @@
"type": "tidelift"
}
],
"time": "2025-06-20T11:29:11+00:00"
"time": "2025-08-20T14:40:06+00:00"
},
{
"name": "sebastian/cli-parser",
@ -2533,23 +2537,23 @@
},
{
"name": "sebastian/recursion-context",
"version": "5.0.0",
"version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "05909fb5bc7df4c52992396d0116aed689f93712"
"reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
"reference": "05909fb5bc7df4c52992396d0116aed689f93712",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
"reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
"phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@ -2584,15 +2588,28 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
"security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
"type": "tidelift"
}
],
"time": "2023-02-03T07:05:40+00:00"
"time": "2025-08-10T07:50:56+00:00"
},
{
"name": "sebastian/type",

View File

@ -4,6 +4,8 @@ includes:
- tests/phpstan/configs/impossible-generics.neon
- tests/phpstan/configs/php-bugs.neon
- tests/phpstan/configs/phpstan-bugs.neon
- tests/phpstan/configs/property-hook-sadness.neon
- tests/phpstan/configs/reflection-class-sadness.neon
- tests/phpstan/configs/spl-fixed-array-sucks.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon

View File

@ -50,6 +50,7 @@ use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException;
use pocketmine\lang\Translatable;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\auth\AuthKeyProvider;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\CompressBatchTask;
use pocketmine\network\mcpe\compression\Compressor;
@ -270,6 +271,7 @@ class Server{
private int $maxPlayers;
private bool $onlineMode = true;
private AuthKeyProvider $authKeyProvider;
private Network $network;
private bool $networkCompressionAsync = true;
@ -982,6 +984,8 @@ class Server{
$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){
$this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
}
@ -1800,6 +1804,10 @@ class Server{
return $this->forceLanguage;
}
public function getAuthKeyProvider() : AuthKeyProvider{
return $this->authKeyProvider;
}
public function getNetwork() : Network{
return $this->network;
}

View File

@ -31,7 +31,7 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.32.2";
public const BASE_VERSION = "5.33.2";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";

View File

@ -24,8 +24,8 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\BlockEventHelper;
use pocketmine\block\utils\MultiAnyFacing;
use pocketmine\block\utils\MultiAnySupportTrait;
use pocketmine\block\utils\MultiFacing;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Fertilizer;
use pocketmine\item\Item;
@ -36,7 +36,7 @@ use pocketmine\world\World;
use function count;
use function shuffle;
class GlowLichen extends Transparent implements MultiFacing{
class GlowLichen extends Transparent implements MultiAnyFacing{
use MultiAnySupportTrait;
public function getLightLevel() : int{

View File

@ -23,11 +23,11 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\MultiAnyFacing;
use pocketmine\block\utils\MultiAnySupportTrait;
use pocketmine\block\utils\MultiFacing;
use pocketmine\block\utils\SupportType;
final class ResinClump extends Transparent implements MultiFacing{
final class ResinClump extends Transparent implements MultiAnyFacing{
use MultiAnySupportTrait;
public function isSolid() : bool{

View File

@ -91,7 +91,6 @@ class RuntimeBlockStateRegistry{
foreach(VanillaBlocks::getAll() as $block){
$this->register($block);
}
gc_ignore($this);
}
/**

View File

@ -82,7 +82,7 @@ class Banner extends Spawnable{
}
}
$this->type = $nbt->getInt(self::TAG_TYPE);
$this->type = $nbt->getInt(self::TAG_TYPE, self::TYPE_NORMAL);
}
protected function writeSaveData(CompoundTag $nbt) : void{

View File

@ -27,7 +27,10 @@ interface Ageable{
public function getAge() : int;
public function getMaxAge() : int;
/**
* Must be in range 0 - getMaxAge()
* @return $this
*/
public function setAge(int $age) : self;

View File

@ -38,6 +38,8 @@ trait AgeableTrait{
public function getAge() : int{ return $this->age; }
public function getMaxAge() : int{ return self::MAX_AGE; }
/**
* @return $this
*/

View File

@ -25,7 +25,7 @@ namespace pocketmine\block\utils;
use pocketmine\math\Facing;
interface MultiFacing{
interface MultiAnyFacing{
/**
* @return int[]

View File

@ -91,7 +91,6 @@ class SimpleCommandMap implements CommandMap{
public function __construct(private Server $server){
$this->setDefaultCommands();
gc_ignore($this);
}
private function setDefaultCommands() : void{

View File

@ -97,7 +97,6 @@ class CraftingManager{
}
});
}
gc_ignore($this);
}
/** @phpstan-return ObjectSet<\Closure() : void> */

View File

@ -162,7 +162,7 @@ final class CraftingManagerFromDataHelper{
}
$mapper = new \JsonMapper();
$mapper->bStrictObjectTypeChecking = true;
$mapper->bStrictObjectTypeChecking = false; //to allow hydrating ItemStackData - since this is only used for offline data it's safe
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bExceptionOnMissingData = true;

View File

@ -40,7 +40,6 @@ final class FurnaceRecipeManager{
private ObjectSet $recipeRegisteredCallbacks;
public function __construct(){
gc_ignore($this);
$this->recipeRegisteredCallbacks = new ObjectSet();
}

View File

@ -23,7 +23,9 @@ declare(strict_types=1);
namespace pocketmine\crafting\json;
final class ItemStackData{
use function count;
final class ItemStackData implements \JsonSerializable{
/** @required */
public string $name;
@ -40,4 +42,15 @@ final class ItemStackData{
public function __construct(string $name){
$this->name = $name;
}
/**
* @return mixed[]|string
*/
public function jsonSerialize() : array|string{
$result = (array) $this;
if(count($result) === 1 && isset($result["name"])){
return $this->name;
}
return $result;
}
}

View File

@ -118,10 +118,10 @@ use pocketmine\data\bedrock\block\convert\property\EnumFromRawStateMap;
use pocketmine\data\bedrock\block\convert\property\FlattenedCaveVinesVariant;
use pocketmine\data\bedrock\block\convert\property\IntFromRawStateMap;
use pocketmine\data\bedrock\block\convert\property\IntProperty;
use pocketmine\data\bedrock\block\convert\property\OptionSetFromIntProperty;
use pocketmine\data\bedrock\block\convert\property\ValueFromIntProperty;
use pocketmine\data\bedrock\block\convert\property\ValueFromStringProperty;
use pocketmine\data\bedrock\block\convert\property\ValueMappings;
use pocketmine\data\bedrock\block\convert\property\ValueSetFromIntProperty;
use pocketmine\math\Facing;
use function array_map;
use function min;
@ -581,7 +581,7 @@ final class VanillaBlockMappings{
$reg->mapModel(Model::create(Blocks::RESIN_CLUMP(), Ids::RESIN_CLUMP)->properties([$commonProperties->multiFacingFlags]));
$reg->mapModel(Model::create(Blocks::VINES(), Ids::VINE)->properties([
new OptionSetFromIntProperty(
new ValueSetFromIntProperty(
StateNames::VINE_DIRECTION_BITS,
IntFromRawStateMap::int([
Facing::NORTH => BlockLegacyMetadata::VINE_FLAG_NORTH,
@ -1267,7 +1267,7 @@ final class VanillaBlockMappings{
$reg->mapModel(Model::create(Blocks::CHAIN(), Ids::CHAIN)->properties([$commonProperties->pillarAxis]));
$reg->mapModel(Model::create(Blocks::CHISELED_BOOKSHELF(), Ids::CHISELED_BOOKSHELF)->properties([
$commonProperties->horizontalFacingSWNE,
new OptionSetFromIntProperty(
new ValueSetFromIntProperty(
StateNames::BOOKS_STORED,
EnumFromRawStateMap::int(ChiseledBookshelfSlot::class, fn(ChiseledBookshelfSlot $case) => match($case){
//these are (currently) the same as the internal values, but it's best not to rely on those in case Mojang mess with the flags

View File

@ -46,7 +46,7 @@ use pocketmine\block\utils\CoralType;
use pocketmine\block\utils\DyeColor;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\Lightable;
use pocketmine\block\utils\MultiFacing;
use pocketmine\block\utils\MultiAnyFacing;
use pocketmine\block\utils\PillarRotation;
use pocketmine\block\utils\SignLikeRotation;
use pocketmine\block\utils\SlabType;
@ -80,8 +80,8 @@ final class CommonProperties{
/** @phpstan-var ValueFromIntProperty<AnyFacing, int> */
public readonly ValueFromIntProperty $anyFacingClassic;
/** @phpstan-var OptionSetFromIntProperty<MultiFacing, int> */
public readonly OptionSetFromIntProperty $multiFacingFlags;
/** @phpstan-var ValueSetFromIntProperty<MultiAnyFacing, int> */
public readonly ValueSetFromIntProperty $multiFacingFlags;
/** @phpstan-var IntProperty<SignLikeRotation> */
public readonly IntProperty $floorSignLikeRotation;
@ -242,7 +242,7 @@ final class CommonProperties{
fn(AnyFacing $b, int $v) => $b->setFacing($v)
);
$this->multiFacingFlags = new OptionSetFromIntProperty(
$this->multiFacingFlags = new ValueSetFromIntProperty(
StateNames::MULTI_FACE_DIRECTION_BITS,
IntFromRawStateMap::int([
Facing::DOWN => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_DOWN,
@ -252,8 +252,8 @@ final class CommonProperties{
Facing::WEST => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_WEST,
Facing::EAST => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_EAST
]),
fn(MultiFacing $b) => $b->getFaces(),
fn(MultiFacing $b, array $v) => $b->setFaces($v)
fn(MultiAnyFacing $b) => $b->getFaces(),
fn(MultiAnyFacing $b, array $v) => $b->setFaces($v)
);
$this->floorSignLikeRotation = new IntProperty(StateNames::GROUND_SIGN_DIRECTION, 0, 15, fn(SignLikeRotation $b) => $b->getRotation(), fn(SignLikeRotation $b, int $v) => $b->setRotation($v));

View File

@ -32,7 +32,7 @@ use pocketmine\utils\AssumptionFailedError;
* @phpstan-template TOption of int|\UnitEnum
* @phpstan-implements Property<TBlock>
*/
class OptionSetFromIntProperty implements Property{
class ValueSetFromIntProperty implements Property{
private int $maxValue = 0;

View File

@ -30,6 +30,7 @@ use pocketmine\data\bedrock\block\BlockStateStringValues;
* Internally we use null for no connections, but accepting this in the mapping code would require a fair amount of
* extra complexity for this one case. This shim allows us to use the regular systems for handling walls.
* TODO: get rid of this in PM6 and make the internal enum have a NONE case
* @internal
*/
enum WallConnectionTypeShim{
case NONE;

View File

@ -81,7 +81,6 @@ final class BlockStateUpgradeSchema{
private int $schemaId
){
$this->versionId = ($this->maxVersionMajor << 24) | ($this->maxVersionMinor << 16) | ($this->maxVersionPatch << 8) | $this->maxVersionRevision;
gc_ignore($this);
}
/**

View File

@ -45,7 +45,6 @@ final class ItemDeserializer{
private BlockStateDeserializer $blockStateDeserializer
){
new ItemSerializerDeserializerRegistrar($this, null);
gc_ignore($this);
}
/**

View File

@ -55,7 +55,6 @@ final class ItemSerializer{
){
$this->registerSpecialBlockSerializers();
new ItemSerializerDeserializerRegistrar(null, $this);
gc_ignore($this);
}
/**

View File

@ -112,7 +112,6 @@ class Language{
$this->lang = self::loadLang($path, $this->langName);
$this->fallbackLang = self::loadLang($path, $fallback);
gc_ignore($this);
}
public function getName() : string{

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function base64_decode;
@ -32,6 +33,7 @@ use function bin2hex;
use function chr;
use function count;
use function explode;
use function hex2bin;
use function is_array;
use function json_decode;
use function json_encode;
@ -54,6 +56,7 @@ use function strlen;
use function strtr;
use function substr;
use const JSON_THROW_ON_ERROR;
use const OPENSSL_ALGO_SHA256;
use const OPENSSL_ALGO_SHA384;
use const STR_PAD_LEFT;
@ -170,17 +173,17 @@ final class JwtUtils{
/**
* @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);
$rawSignature = self::b64UrlDecode($signature);
$derSignature = self::rawSignatureToDer($rawSignature);
$derSignature = $ec ? self::rawSignatureToDer($rawSignature) : $rawSignature;
$v = openssl_verify(
$header . '.' . $body,
$derSignature,
$signingKey,
self::SIGNATURE_ALGORITHM
self::derPublicKeyToPem($signingKeyDer),
$ec ? self::SIGNATURE_ALGORITHM : OPENSSL_ALGO_SHA256
);
switch($v){
case 0: return false;
@ -238,22 +241,56 @@ final class JwtUtils{
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(Binary::writeInt($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{
$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){
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;
}
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

@ -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\JwtChainLinkBody;
use pocketmine\network\mcpe\protocol\types\login\JwtHeader;
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 JwtHeader $headers */
$headers = $mapper->map($headersArray, new JwtHeader());
}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 JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}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

@ -76,7 +76,6 @@ final class BlockStateDictionary{
$this->stateDataToStateIdLookup[$name] = $stateIds;
}
}
gc_ignore($this);
}
/**

View File

@ -46,9 +46,7 @@ final class ItemTranslator{
private ItemSerializer $itemSerializer,
private ItemDeserializer $itemDeserializer,
private BlockItemIdMap $blockItemIdMap
){
gc_ignore($this->itemTypeDictionary);
}
){}
/**
* @return int[]|null

View File

@ -719,6 +719,7 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//TODO: do we need to handle this?
case PlayerAction::PREDICT_DESTROY_BLOCK:
self::validateFacing($face);
if(!$this->player->breakBlock($pos)){
$this->syncBlocksNearby($pos, $face);
}

View File

@ -27,27 +27,35 @@ use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationFactory;
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\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
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\legacy\LegacyAuthChain;
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\player\Player;
use pocketmine\player\PlayerInfo;
use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function chr;
use function count;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use function md5;
use function ord;
use const JSON_THROW_ON_ERROR;
/**
@ -65,15 +73,95 @@ class LoginPacketHandler extends PacketHandler{
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{
$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());
return true;
return null;
}
$clientData = $this->parseClientData($packet->clientDataJwt);
@ -86,32 +174,25 @@ class LoginPacketHandler extends PacketHandler{
disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
);
return true;
return null;
}
if(!Uuid::isValid($extraData->identity)){
throw new PacketHandlingException("Invalid login UUID");
}
$uuid = Uuid::fromString($extraData->identity);
$arrClientData = (array) $clientData;
$arrClientData["TitleID"] = $extraData->titleId;
if($extraData->XUID !== ""){
if($xuid !== ""){
$playerInfo = new XboxLivePlayerInfo(
$extraData->XUID,
$extraData->displayName,
$uuid,
$xuid,
$username,
$legacyUuid,
$skin,
$clientData->LanguageCode,
$arrClientData
(array) $clientData
);
}else{
$playerInfo = new PlayerInfo(
$extraData->displayName,
$uuid,
$username,
$legacyUuid,
$skin,
$clientData->LanguageCode,
$arrClientData
(array) $clientData
);
}
($this->playerInfoConsumer)($playerInfo);
@ -144,12 +225,10 @@ class LoginPacketHandler extends PacketHandler{
$ev->call();
if(!$ev->isAllowed()){
$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 true;
return $ev->isAuthRequired();
}
/**
@ -162,13 +241,10 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e);
}
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->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper = $this->defaultJsonMapper();
try{
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
}catch(\JsonMapper_Exception $e){
@ -178,68 +254,31 @@ class LoginPacketHandler extends PacketHandler{
}
/**
* @param array<string, mixed> $headerArray
* @throws PacketHandlingException
*/
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{
$mapper = $this->defaultJsonMapper();
try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
}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 JwtChain());
$header = $mapper->map($headerArray, new XboxAuthJwtHeader());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
return $header;
}
/**
* @param array<string, mixed> $bodyArray
* @throws PacketHandlingException
*/
protected function fetchAuthData(JwtChain $chain) : AuthenticationData{
/** @var AuthenticationData|null $extraData */
$extraData = null;
foreach($chain->chain as $jwt){
//validate every chain element
try{
[, $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 AuthenticationData $extraData */
$extraData = $mapper->map($claims["extraData"], new AuthenticationData());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
}
protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{
$mapper = $this->defaultJsonMapper();
try{
$header = $mapper->map($bodyArray, new XboxAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
if($extraData === null){
throw new PacketHandlingException("'extraData' not found in chain data");
}
return $extraData;
return $header;
}
/**
@ -252,11 +291,7 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e);
}
$mapper = new \JsonMapper();
$mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper = $this->defaultJsonMapper();
try{
$clientData = $mapper->map($clientDataClaims, new ClientData());
}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.
* In the future this won't be necessary.
*
* @param null|string[] $legacyCertificate
*
* @throws \InvalidArgumentException
*/
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, 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));
protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{
$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

@ -34,7 +34,6 @@ trait SingletonTrait{
public static function getInstance() : self{
if(self::$instance === null){
self::$instance = self::make();
gc_ignore(self::$instance);
}
return self::$instance;
}

View File

@ -485,7 +485,6 @@ class World implements ChunkManager{
private WritableWorldProvider $provider,
private AsyncPool $workerPool
){
gc_ignore($this);
$this->folderName = $name;
$this->worldId = self::$worldIdCounter++;

View File

@ -56,8 +56,6 @@ final class GlobalBlockStateHandlers{
if(self::$registrar === null){
$deserializer = new BlockStateToObjectDeserializer();
$serializer = new BlockObjectToStateSerializer();
gc_ignore($deserializer);
gc_ignore($serializer);
self::$registrar = new BlockSerializerDeserializerRegistrar($deserializer, $serializer);
VanillaBlockMappings::init(self::$registrar);
}
@ -78,17 +76,15 @@ final class GlobalBlockStateHandlers{
Path::join(BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH, 'nbt_upgrade_schema'),
PHP_INT_MAX
));
$blockIdMetaUpgrader = BlockIdMetaUpgrader::loadFromString(
Filesystem::fileGetContents(Path::join(
BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH,
'id_meta_to_nbt/1.12.0.bin'
)),
LegacyBlockIdToStringIdMap::getInstance(),
$blockStateUpgrader
);
gc_ignore($blockIdMetaUpgrader);
self::$blockDataUpgrader = new BlockDataUpgrader(
$blockIdMetaUpgrader,
BlockIdMetaUpgrader::loadFromString(
Filesystem::fileGetContents(Path::join(
BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH,
'id_meta_to_nbt/1.12.0.bin'
)),
LegacyBlockIdToStringIdMap::getInstance(),
$blockStateUpgrader
),
$blockStateUpgrader
);
}

View File

@ -48,7 +48,7 @@ final class AsyncGeneratorExecutor implements GeneratorExecutor{
\Logger $logger,
private readonly AsyncPool $workerPool,
private readonly GeneratorExecutorSetupParameters $setupParameters,
int $asyncContextId = null
?int $asyncContextId = null
){
$this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor");

View File

@ -870,12 +870,6 @@ parameters:
count: 1
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\.$#'
identifier: argument.type
@ -1158,12 +1152,6 @@ parameters:
count: 2
path: ../../../src/world/World.php
-
message: '#^Parameter \#2 \$x of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 2
path: ../../../src/world/World.php
-
message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#'
identifier: argument.type
@ -1188,12 +1176,6 @@ parameters:
count: 2
path: ../../../src/world/World.php
-
message: '#^Parameter \#3 \$y of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 2
path: ../../../src/world/World.php
-
message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#'
identifier: argument.type
@ -1218,12 +1200,6 @@ parameters:
count: 2
path: ../../../src/world/World.php
-
message: '#^Parameter \#4 \$z of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 2
path: ../../../src/world/World.php
-
message: '#^Method pocketmine\\world\\biome\\BiomeRegistry\:\:getBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#'
identifier: return.type

View File

@ -168,6 +168,12 @@ parameters:
count: 1
path: ../../../src/crafting/ShapedRecipe.php
-
message: '#^Offset ''name'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#'
identifier: isset.offset
count: 1
path: ../../../src/crafting/json/ItemStackData.php
-
message: '#^Property pocketmine\\crash\\CrashDumpData\:\:\$parameters \(list\<string\>\) does not accept array\.$#'
identifier: assign.propertyType

View File

@ -0,0 +1,61 @@
parameters:
ignoreErrors:
-
message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$enderInventory because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Human.php
-
message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$hungerManager because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Human.php
-
message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$inventory because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Human.php
-
message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$offHandInventory because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Human.php
-
message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$xpManager because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Human.php
-
message: '#^Cannot unset property pocketmine\\entity\\Living\:\:\$armorInventory because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Living.php
-
message: '#^Cannot unset property pocketmine\\entity\\Living\:\:\$effectManager because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/entity/Living.php
-
message: '#^Cannot unset property pocketmine\\player\\Player\:\:\$craftingGrid because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/player/Player.php
-
message: '#^Cannot unset property pocketmine\\player\\Player\:\:\$cursorInventory because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/player/Player.php
-
message: '#^Cannot unset property pocketmine\\world\\format\\io\\leveldb\\LevelDB\:\:\$db because it might have hooks in a subclass\.$#'
identifier: unset.possiblyHookedProperty
count: 1
path: ../../../src/world/format/io/leveldb/LevelDB.php

View File

@ -0,0 +1,31 @@
parameters:
ignoreErrors:
-
message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\<covariant pocketmine\\event\\Event\> in PHPDoc tag @param for parameter \$class is redundant, template type T of object of class ReflectionClass has the same variance\.$#'
identifier: generics.callSiteVarianceRedundant
count: 2
path: ../../phpunit/event/HandlerListManagerTest.php
-
message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\<covariant pocketmine\\event\\Event\> in PHPDoc tag @param for parameter \$expect is redundant, template type T of object of class ReflectionClass has the same variance\.$#'
identifier: generics.callSiteVarianceRedundant
count: 1
path: ../../phpunit/event/HandlerListManagerTest.php
-
message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\<covariant pocketmine\\event\\Event\> in PHPDoc tag @return is redundant, template type T of object of class ReflectionClass has the same variance\.$#'
identifier: generics.callSiteVarianceRedundant
count: 3
path: ../../phpunit/event/HandlerListManagerTest.php
-
message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\<covariant pocketmine\\event\\Event\> in PHPDoc tag @var for property pocketmine\\event\\HandlerListManagerTest\:\:\$isValidFunc is redundant, template type T of object of class ReflectionClass has the same variance\.$#'
identifier: generics.callSiteVarianceRedundant
count: 1
path: ../../phpunit/event/HandlerListManagerTest.php
-
message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\<covariant pocketmine\\event\\Event\> in PHPDoc tag @var for property pocketmine\\event\\HandlerListManagerTest\:\:\$resolveParentFunc is redundant, template type T of object of class ReflectionClass has the same variance\.$#'
identifier: generics.callSiteVarianceRedundant
count: 2
path: ../../phpunit/event/HandlerListManagerTest.php

View File

@ -35,12 +35,12 @@ class HandlerListManagerTest extends TestCase{
/**
* @var \Closure
* @phpstan-var \Closure(\ReflectionClass<Event>) : bool
* @phpstan-var \Closure(\ReflectionClass<covariant Event>) : bool
*/
private $isValidFunc;
/**
* @var \Closure
* @phpstan-var \Closure(\ReflectionClass<Event>) : ?\ReflectionClass<Event>
* @phpstan-var \Closure(\ReflectionClass<covariant Event>) : ?\ReflectionClass<covariant Event>
*/
private $resolveParentFunc;
@ -53,7 +53,7 @@ class HandlerListManagerTest extends TestCase{
/**
* @return \Generator|mixed[][]
* @phpstan-return \Generator<int, array{\ReflectionClass<Event>, bool, string}, void, void>
* @phpstan-return \Generator<int, array{\ReflectionClass<covariant Event>, bool, string}, void, void>
*/
public static function isValidClassProvider() : \Generator{
yield [new \ReflectionClass(Event::class), false, "event base should not be handleable"];
@ -65,7 +65,7 @@ class HandlerListManagerTest extends TestCase{
/**
* @dataProvider isValidClassProvider
*
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<covariant Event> $class
*/
public function testIsValidClass(\ReflectionClass $class, bool $isValid, string $reason) : void{
self::assertSame($isValid, ($this->isValidFunc)($class), $reason);
@ -73,7 +73,7 @@ class HandlerListManagerTest extends TestCase{
/**
* @return \Generator|\ReflectionClass[][]
* @phpstan-return \Generator<int, array{\ReflectionClass<Event>, \ReflectionClass<Event>|null}, void, void>
* @phpstan-return \Generator<int, array{\ReflectionClass<covariant Event>, \ReflectionClass<covariant Event>|null}, void, void>
*/
public static function resolveParentClassProvider() : \Generator{
yield [new \ReflectionClass(TestConcreteExtendsAllowHandleEvent::class), new \ReflectionClass(TestAbstractAllowHandleEvent::class)];
@ -85,8 +85,8 @@ class HandlerListManagerTest extends TestCase{
/**
* @dataProvider resolveParentClassProvider
*
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<Event>|null $expect
* @phpstan-param \ReflectionClass<covariant Event> $class
* @phpstan-param \ReflectionClass<covariant Event>|null $expect
*/
public function testResolveParentClass(\ReflectionClass $class, ?\ReflectionClass $expect) : void{
if($expect === null){

View File

@ -209,11 +209,18 @@ class ParserPacketHandler extends PacketHandler{
return $data;
}
/**
* @return mixed[]
*/
private static function objectToOrderedArray(object $object) : array{
$result = (array) ($object instanceof \JsonSerializable ? $object->jsonSerialize() : $object);
private static function objectToOrderedArray(object $object) : mixed{
if($object instanceof \JsonSerializable){
$result = $object->jsonSerialize();
if(is_object($result)){
$result = (array) $result;
}elseif(!is_array($result)){
return $result;
}
}else{
$result = (array) $object;
}
ksort($result, SORT_STRING);
foreach(Utils::promoteKeys($result) as $property => $value){
@ -280,7 +287,7 @@ class ParserPacketHandler extends PacketHandler{
file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n");
echo "updating item registry\n";
$items = array_map(function(ItemTypeEntry $entry) : array{
$items = array_map(function(ItemTypeEntry $entry) : mixed{
return self::objectToOrderedArray($entry);
}, $packet->getEntries());
file_put_contents($this->bedrockDataPath . '/item_registry.json', json_encode($items, JSON_PRETTY_PRINT) . "\n");