name = $name; $payload->attrs = []; foreach($attrs as $name => $value) { if($value === null) continue; if(!is_scalar($value)) $value = (string)$value; $payload->attrs[] = ['name' => (string)$name, 'value' => $value]; } return $payload; } private static function createErrorPayload(string $code, ?string $text = null): object { $attrs = ['code' => $code]; if($text !== null && $text !== '') $attrs['text'] = $text; return self::createPayload('error', $attrs); } #[HttpMiddleware('/rpc')] public function verifyRequest($response, $request) { $userTime = (int)$request->getHeaderLine('X-Mince-Time'); $userHash = base64_decode((string)$request->getHeaderLine('X-Mince-Hash')); $currentTime = time(); if(empty($userHash) || $userTime < $currentTime - 60 || $userTime > $currentTime + 60) return self::createErrorPayload('verification', 'Request verification failed.'); $paramString = $request->getParamString(); if($request->getMethod() === 'POST') { if(!$request->isFormContent()) return self::createErrorPayload('request', 'Request body is not in expect format.'); $content = $request->getContent(); $bodyString = $content->getParamString(); if(!empty($paramString) && !empty($bodyString)) $paramString .= '&'; $paramString .= $bodyString; } $verifyText = (string)$userTime . '#' . $request->getPath() . '?' . $paramString; $verifyHash = hash_hmac('sha256', $verifyText, $this->secretKey, true); if(!hash_equals($verifyHash, $userHash)) return self::createErrorPayload('verification', 'Request verification failed.'); } #[HttpPost('/rpc/auth')] public function postAuth($response, $request) { $body = $request->getContent(); $id = (string)$body->getParam('id'); $name = (string)$body->getParam('name'); $addr = (string)$body->getParam('ip'); if(empty($name)) return self::createErrorPayload('auth:username', 'Username is invalid.'); if(!filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return self::createErrorPayload('auth:address', 'IP Address is invalid (must be IPv4).'); try { $uuid = Uuid::fromString($id); } catch(InvalidArgumentException $ex) { return self::createErrorPayload('auth:id', 'ID is not a valid UUID format.'); } if(MojangInterop::isOfflineId($uuid)) { if(!MojangInterop::createOfflinePlayerUUID($name)->equals($uuid)) return self::createErrorPayload('auth:offline:tamper', 'ID does not match the expected value, are you trying to tamper?'); } elseif(MojangInterop::isMojangId($uuid)) { // i think there's very little reason to actually support this, offline mode completely forgoes it return self::createErrorPayload('auth:mojang:impl', 'Mojang ID verification not implemented lol sorry.'); } else return self::createErrorPayload('auth:uuid', 'Provided UUID isn\'t an offline ID nor a Mojang ID.'); try { $linkInfo = $this->accountLinks->getLink(uuid: $uuid); } catch(RuntimeException $ex) { $linkInfo = null; } if($linkInfo !== null) { try { $authInfo = $this->authorisations->getAuthorisation(uuid: $linkInfo, remoteAddr: $addr); if($authInfo->isGranted()) { $this->authorisations->markAuthorisationUsed($authInfo); return self::createPayload('auth:ok'); } } catch(RuntimeException $ex) { $authInfo = null; } try { $userInfo = $this->users->getUser(userId: $linkInfo->getUserId()); } catch(RuntimeException $ex) { return 500; } if($authInfo === null) $this->authorisations->createAuthorisation($uuid, $addr); return self::createPayload('auth:authorise', [ 'user_id' => $userInfo->getId(), 'user_name' => $userInfo->getName(), 'user_colour' => $userInfo->getColourRaw(), 'url' => $this->clientsUrl, ]); } try { $verifyInfo = $this->verifications->getVerification(uuid: $uuid, remoteAddr: $addr); $verifyCode = $verifyInfo->getCode(); } catch(RuntimeException $ex) { $verifyCode = $this->verifications->createVerification($uuid, $name, $addr); } return self::createPayload('auth:link', [ 'code' => $verifyCode, 'url' => $this->clientsUrl, ]); } }