PocketMine-MP 5.43.2 git-c9e7b3ab9bd2149f206392522e8eb7e9d8d68cfa
Loading...
Searching...
No Matches
LoginPacketHandler.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\handler;
25
37use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
49use Ramsey\Uuid\Uuid;
50use Ramsey\Uuid\UuidInterface;
51use function base64_decode;
52use function chr;
53use function gettype;
54use function is_object;
55use function json_decode;
56use function md5;
57use function ord;
58use function substr;
59use const JSON_THROW_ON_ERROR;
60
69 public function __construct(
70 private Server $server,
71 private NetworkSession $session,
72 private \Closure $playerInfoConsumer,
73 private \Closure $authCallback
74 ){}
75
76 private static function calculateUuidFromXuid(string $xuid) : UuidInterface{
77 $hash = md5("pocket-auth-1-xuid:" . $xuid, binary: true);
78 $hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); // set version to 3
79 $hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); // set variant to RFC 4122
80
81 return Uuid::fromBytes($hash);
82 }
83
84 public function handleLogin(LoginPacket $packet) : bool{
85 $authInfo = $this->parseAuthInfo($packet->authInfoJson);
86
87 if($authInfo->AuthenticationType === AuthenticationType::FULL->value){
88 try{
89 [$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token);
90 }catch(JwtException $e){
91 throw PacketHandlingException::wrap($e, "Error parsing authentication token");
92 }
93 $header = $this->mapXboxTokenHeader($headerArray);
94 $claims = $this->mapXboxTokenBody($claimsArray);
95
96 $legacyUuid = self::calculateUuidFromXuid($claims->xid);
97 $username = $claims->xname;
98 $xuid = $claims->xid;
99
100 $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
101 if($authRequired === null){
102 //plugin cancelled
103 return true;
104 }
105 $this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired);
106
107 }elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){
108 try{
109 [, $claimsArray, ] = JwtUtils::parse($authInfo->Token);
110 }catch(JwtException $e){
111 throw PacketHandlingException::wrap($e, "Error parsing self-signed authentication token");
112 }
113 $claims = $this->mapSelfSignedTokenBody($claimsArray);
114
115 if(!Uuid::isValid($claims->leguuid)){
116 throw new PacketHandlingException("Invalid UUID string in self-signed certificate: " . $claims->leguuid);
117 }
118 $legacyUuid = Uuid::fromString($claims->leguuid);
119 $username = $claims->xname;
120 $xuid = "";
121
122 $selfSignedKey = base64_decode($claims->cpk, strict: true);
123 if($selfSignedKey === false){
124 throw new PacketHandlingException("Invalid self-signed key");
125 }
126
127 $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
128 if($authRequired === null){
129 //plugin cancelled
130 return true;
131 }
132 $this->processSelfSignedLogin($authInfo->Token, $selfSignedKey, $packet->clientDataJwt, $authRequired);
133 }else{
134 throw new PacketHandlingException("Unsupported authentication type: $authInfo->AuthenticationType");
135 }
136
137 return true;
138 }
139
140 private function processLoginCommon(LoginPacket $packet, string $username, UuidInterface $legacyUuid, string $xuid) : ?bool{
141 if(!Player::isValidUserName($username)){
142 $this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
143
144 return null;
145 }
146
147 $clientData = $this->parseClientData($packet->clientDataJwt);
148
149 try{
150 $skin = $this->session->getTypeConverter()->getSkinAdapter()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
151 }catch(\InvalidArgumentException | InvalidSkinException $e){
152 $this->session->disconnectWithError(
153 reason: "Invalid skin: " . $e->getMessage(),
154 disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
155 );
156
157 return null;
158 }
159
160 if($xuid !== ""){
161 $playerInfo = new XboxLivePlayerInfo(
162 $xuid,
163 $username,
164 $legacyUuid,
165 $skin,
166 $clientData->LanguageCode,
167 (array) $clientData
168 );
169 }else{
170 $playerInfo = new PlayerInfo(
171 $username,
172 $legacyUuid,
173 $skin,
174 $clientData->LanguageCode,
175 (array) $clientData
176 );
177 }
178 ($this->playerInfoConsumer)($playerInfo);
179
180 $ev = new PlayerPreLoginEvent(
181 $playerInfo,
182 $this->session->getIp(),
183 $this->session->getPort(),
184 $this->server->requiresAuthentication()
185 );
186 if($this->server->getNetwork()->getValidConnectionCount() > $this->server->getMaxPlayers()){
187 $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_FULL, KnownTranslationFactory::disconnectionScreen_serverFull());
188 }
189 if(!$this->server->isWhitelisted($playerInfo->getUsername())){
190 $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_WHITELISTED, KnownTranslationFactory::pocketmine_disconnect_whitelisted());
191 }
192
193 $banMessage = null;
194 if(($banEntry = $this->server->getNameBans()->getEntry($playerInfo->getUsername())) !== null){
195 $banReason = $banEntry->getReason();
196 $banMessage = $banReason === "" ? KnownTranslationFactory::pocketmine_disconnect_ban_noReason() : KnownTranslationFactory::pocketmine_disconnect_ban($banReason);
197 }elseif(($banEntry = $this->server->getIPBans()->getEntry($this->session->getIp())) !== null){
198 $banReason = $banEntry->getReason();
199 $banMessage = KnownTranslationFactory::pocketmine_disconnect_ban($banReason !== "" ? $banReason : KnownTranslationFactory::pocketmine_disconnect_ban_ip());
200 }
201 if($banMessage !== null){
202 $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_BANNED, $banMessage);
203 }
204
205 $ev->call();
206 if(!$ev->isAllowed()){
207 $this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
208 return null;
209 }
210
211 return $ev->isAuthRequired();
212 }
213
217 protected function parseAuthInfo(string $authInfo) : AuthenticationInfo{
218 try{
219 $authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR);
220 }catch(\JsonException $e){
221 throw PacketHandlingException::wrap($e);
222 }
223 if(!is_object($authInfoJson)){
224 throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
225 }
226
227 $mapper = $this->defaultJsonMapper("Root authentication info JSON");
228 try{
229 $clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
230 }catch(\JsonMapper_Exception $e){
231 throw PacketHandlingException::wrap($e);
232 }
233 return $clientData;
234 }
235
240 protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{
241 $mapper = $this->defaultJsonMapper("OpenID JWT header");
242 try{
243 $header = $mapper->map($headerArray, new XboxAuthJwtHeader());
244 }catch(\JsonMapper_Exception $e){
245 throw PacketHandlingException::wrap($e);
246 }
247 return $header;
248 }
249
254 protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{
255 $mapper = $this->defaultJsonMapper("OpenID JWT body");
256 try{
257 $header = $mapper->map($bodyArray, new XboxAuthJwtBody());
258 }catch(\JsonMapper_Exception $e){
259 throw PacketHandlingException::wrap($e);
260 }
261 return $header;
262 }
263
268 protected function mapSelfSignedTokenBody(array $bodyArray) : SelfSignedJwtBody{
269 $mapper = $this->defaultJsonMapper("OpenID JWT body");
270 try{
271 $header = $mapper->map($bodyArray, new SelfSignedJwtBody());
272 }catch(\JsonMapper_Exception $e){
273 throw PacketHandlingException::wrap($e);
274 }
275 return $header;
276 }
277
281 protected function parseClientData(string $clientDataJwt) : ClientData{
282 try{
283 [, $clientDataClaims, ] = JwtUtils::parse($clientDataJwt);
284 }catch(JwtException $e){
285 throw PacketHandlingException::wrap($e);
286 }
287
288 $mapper = $this->defaultJsonMapper("ClientData JWT body");
289 try{
290 $clientData = $mapper->map($clientDataClaims, new ClientData());
291 }catch(\JsonMapper_Exception $e){
292 throw PacketHandlingException::wrap($e);
293 }
294 return $clientData;
295 }
296
303 protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{
304 $this->session->setHandler(null); //drop packets received during login verification
305
306 $authKeyProvider = $this->server->getAuthKeyProvider();
307
308 $authKeyProvider->getKey($keyId)->onCompletion(
309 function(array $issuerAndKey) use ($token, $clientData, $authRequired) : void{
310 [$issuer, $mojangPublicKeyPem] = $issuerAndKey;
311 $this->server->getAsyncPool()->submitTask(new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback));
312 },
313 fn() => ($this->authCallback)(false, $authRequired, "Unrecognized authentication key ID: $keyId", null)
314 );
315 }
316
317 protected function processSelfSignedLogin(string $token, string $clientPublicKey, string $clientData, bool $authRequired) : void{
318 $this->session->setHandler(null); //drop packets received during login verification
319
320 $this->server->getAsyncPool()->submitTask(new ProcessSelfSignedLoginTask($token, $clientPublicKey, $clientData, $authRequired, onCompletion: $this->authCallback));
321 }
322
323 private function defaultJsonMapper(string $logContext) : \JsonMapper{
324 $mapper = new \JsonMapper();
325 $mapper->bExceptionOnMissingData = true;
326 $mapper->undefinedPropertyHandler = $this->warnUndefinedJsonPropertyHandler($logContext);
327 $mapper->bStrictObjectTypeChecking = true;
328 $mapper->bEnforceMapType = false;
329 return $mapper;
330 }
331
335 private function warnUndefinedJsonPropertyHandler(string $context) : \Closure{
336 return function(object $object, string $name, mixed $value) use ($context) : void{
337 static $count = 0;
338 if($count++ < 10){
339 $this->session->getLogger()->warning(
340 "$context: Unexpected JSON property for " . (new \ReflectionClass($object))->getShortName() . ": " . Utils::printable(substr($name, 0, 80))
341 );
342 }else{
343 throw new PacketHandlingException("$context: Too many unexpected JSON properties");
344 }
345 };
346 }
347}
__construct(private Server $server, private NetworkSession $session, private \Closure $playerInfoConsumer, private \Closure $authCallback)
processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired)