mince/src/RpcRoutes.php

149 lines
5.5 KiB
PHP

<?php
namespace Mince;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Stringable;
use Index\Routing\IRouter;
use Ramsey\Uuid\Uuid;
class RpcRoutes {
public function __construct(
private Users $users,
private AccountLinks $accountLinks,
private Authorisations $authorisations,
private Verifications $verifications,
private string $secretKey,
private string $clientsUrl
) {}
public function register(IRouter $router): void {
$router->use('/rpc', [$this, 'verifyRequest']);
$router->post('/rpc/auth', [$this, 'postAuth']);
}
private static function createPayload(string $name, array $attrs = []): object {
$payload = new stdClass;
$payload->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);
}
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.');
}
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,
]);
}
}