PocketMine-MP 5.37.2 git-e507eb5e50da3ead3ae88ed2324df21e75820019
Loading...
Searching...
No Matches
FetchAuthKeysTask.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\auth;
25
35use function gettype;
36use function is_array;
37use function is_object;
38use function json_decode;
39use const JSON_THROW_ON_ERROR;
40
42 private const KEYS_ON_COMPLETION = "completion";
43
44 private const MINECRAFT_SERVICES_DISCOVERY_URL = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
45 private const AUTHORIZATION_SERVICE_URI_FALLBACK = "https://authorization.franchise.minecraft-services.net";
46 private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
47 private const AUTHORIZATION_SERVICE_KEYS_PATH = "/.well-known/keys";
48
50 private ?NonThreadSafeValue $keys = null;
51 private string $issuer;
52
54 private ?NonThreadSafeValue $errors = null;
55
59 public function __construct(
60 \Closure $onCompletion
61 ){
62 $this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
63 }
64
65 public function onRun() : void{
67 $errors = [];
68
69 try{
70 $authServiceUri = $this->getAuthServiceURI();
71 }catch(\RuntimeException $e){
72 $errors[] = $e->getMessage();
73 $authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
74 }
75
76 try {
77 $openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
78 $jwksUri = $openIdConfig->jwks_uri;
79
80 $this->issuer = $openIdConfig->issuer;
81 } catch (\RuntimeException $e) {
82 $errors[] = $e->getMessage();
83 $jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
84
85 $this->issuer = $authServiceUri;
86 }
87
88 try{
89 $this->keys = new NonThreadSafeValue($this->getKeys($jwksUri));
90 }catch(\RuntimeException $e){
91 $errors[] = $e->getMessage();
92 }
93
94 $this->errors = $errors === [] ? null : new NonThreadSafeValue($errors);
95 }
96
100 private static function fetchURL(string $url, int $expectedCode) : InternetRequestResult{
101 try{
102 $result = Internet::simpleCurl($url);
103 if($result->getCode() !== $expectedCode){
104 throw new \RuntimeException("Unexpected HTTP response code accessing \"$url\": " . $result->getCode());
105 }
106 return $result;
107 }catch(InternetException $e){
108 throw new \RuntimeException("Failed accessing \"$url\": " . $e->getMessage(), 0, $e);
109 }
110 }
111
112 private function getAuthServiceURI() : string{
113 $result = self::fetchURL(self::MINECRAFT_SERVICES_DISCOVERY_URL, 200);
114 try{
115 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
116 }catch(\JsonException $e){
117 throw new \RuntimeException($e->getMessage(), 0, $e);
118 }
119 if(!is_object($json)){
120 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
121 }
122
123 $mapper = new \JsonMapper();
124 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
125 $mapper->bExceptionOnMissingData = true;
126 $mapper->bStrictObjectTypeChecking = true;
127 $mapper->bEnforceMapType = false;
128 $mapper->bRemoveUndefinedAttributes = true;
129 try{
131 $discovery = $mapper->map($json, new MinecraftServicesDiscovery());
132 }catch(\JsonMapper_Exception $e){
133 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
134 }
135
136 return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
137 }
138
139 private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{
140 $result = self::fetchURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH, 200);
141
142 try{
143 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
144 }catch(\JsonException $e){
145 throw new \RuntimeException($e->getMessage(), 0, $e);
146 }
147 if(!is_object($json)){
148 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
149 }
150
151 $mapper = new \JsonMapper();
152 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
153 $mapper->bExceptionOnMissingData = true;
154 $mapper->bStrictObjectTypeChecking = true;
155 $mapper->bEnforceMapType = false;
156 $mapper->bRemoveUndefinedAttributes = true;
157 try{
159 $configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration());
160 }catch(\JsonMapper_Exception $e){
161 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
162 }
163
164 return $configuration;
165 }
166
170 private function getKeys(string $jwksUri) : array{
171 $result = self::fetchURL($jwksUri, 200);
172
173 try{
174 $json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
175 }catch(\JsonException $e){
176 throw new \RuntimeException($e->getMessage(), 0, $e);
177 }
178
179 if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){
180 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
181 }
182
183 $mapper = new \JsonMapper();
184 $mapper->bExceptionOnUndefinedProperty = true;
185 $mapper->bExceptionOnMissingData = true;
186 $mapper->bStrictObjectTypeChecking = true;
187 $mapper->bEnforceMapType = false;
188 $mapper->bRemoveUndefinedAttributes = true;
189
190 $keys = [];
191 foreach($keysArray as $keyJson){
192 if(!is_array($keyJson)){
193 throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object");
194 }
195
196 try{
198 $key = $mapper->map($keyJson, new AuthServiceKey());
199 $keys[$key->kid] = $key;
200 }catch(\JsonMapper_Exception $e){
201 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
202 }
203 }
204
205 return $keys;
206 }
207
208 public function onCompletion() : void{
213 $callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
214 $callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());
215 }
216}
storeLocal(string $key, mixed $complexData)