150 lines
5.5 KiB
PHP
150 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,
|
||
|
]);
|
||
|
}
|
||
|
}
|