164 lines
6.6 KiB
PHP
164 lines
6.6 KiB
PHP
<?php
|
|
namespace Misuzu\Hanyuu;
|
|
|
|
use RuntimeException;
|
|
use Misuzu\CSRF;
|
|
use Misuzu\Auth\AuthContext;
|
|
use Misuzu\Users\{UsersContext,UserInfo};
|
|
use RPCii\Server\{RpcHandler,RpcHandlerCommon,RpcAction};
|
|
use Index\Colour\Colour;
|
|
use Index\Config\Config;
|
|
use Index\Urls\UrlRegistry;
|
|
|
|
final class HanyuuRpcHandler implements RpcHandler {
|
|
use RpcHandlerCommon;
|
|
|
|
public function __construct(
|
|
private $getBaseUrl,
|
|
private Config $impersonateConfig,
|
|
private UrlRegistry $urls,
|
|
private UsersContext $usersCtx,
|
|
private AuthContext $authCtx
|
|
) {}
|
|
|
|
private static function createPayload(string $name, array $attrs = []): array {
|
|
$payload = ['name' => $name, 'attrs' => []];
|
|
foreach($attrs as $name => $value) {
|
|
if($value === null)
|
|
continue;
|
|
$payload['attrs'][(string)$name] = $value;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
private static function createErrorPayload(string $code, ?string $text = null): array {
|
|
$attrs = ['code' => $code];
|
|
if($text !== null && $text !== '')
|
|
$attrs['text'] = $text;
|
|
|
|
return self::createPayload('error', $attrs);
|
|
}
|
|
|
|
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
|
|
if($impersonator->isSuperUser())
|
|
return true;
|
|
|
|
return in_array(
|
|
$targetId,
|
|
$this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId())),
|
|
true
|
|
);
|
|
}
|
|
|
|
#[RpcAction('mszhau:authCheck')]
|
|
public function procAuthCheck(string $method, string $remoteAddr, string $token, string $avatars = '') {
|
|
if($method !== 'Misuzu')
|
|
return self::createErrorPayload('auth:check:method', 'Requested auth method is not supported.');
|
|
|
|
if(filter_var($remoteAddr, FILTER_VALIDATE_IP) === false)
|
|
return self::createErrorPayload('auth:check:remote_addr', 'Provided remote address is not in a valid format.');
|
|
|
|
$avatarResolutions = trim($avatars);
|
|
if($avatarResolutions === '') {
|
|
$avatarResolutions = [];
|
|
} else {
|
|
$avatarResolutions = explode(',', $avatarResolutions);
|
|
foreach($avatarResolutions as $key => $avatarRes) {
|
|
if(!ctype_digit($avatarRes))
|
|
return self::createErrorPayload('auth:check:avatars', 'Avatar resolution set must be a comma separated set of numbers or empty.');
|
|
$avatarResolutions[$key] = (int)$avatarRes;
|
|
}
|
|
$avatarResolutions = array_unique($avatarResolutions);
|
|
}
|
|
|
|
$baseUrl = ($this->getBaseUrl)();
|
|
$loginUrl = $baseUrl . $this->urls->format('auth-login');
|
|
$registerUrl = $baseUrl . $this->urls->format('auth-register');
|
|
|
|
$tokenPacker = $this->authCtx->createAuthTokenPacker();
|
|
$tokenInfo = $tokenPacker->unpack(trim($token));
|
|
if($tokenInfo->isEmpty())
|
|
return self::createPayload('auth:check:fail', ['reason' => 'empty', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
|
|
|
|
$sessions = $this->authCtx->getSessions();
|
|
try {
|
|
$sessionInfo = $sessions->getSession(sessionToken: $tokenInfo->getSessionToken());
|
|
} catch(RuntimeException $ex) {
|
|
return self::createPayload('auth:check:fail', ['reason' => 'session', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
|
|
}
|
|
|
|
if($sessionInfo->hasExpired()) {
|
|
$sessions->deleteSessions(sessionInfos: $sessionInfo);
|
|
return self::createPayload('auth:check:fail', ['reason' => 'expired', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
|
|
}
|
|
|
|
$sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
|
|
|
|
$users = $this->usersCtx->getUsers();
|
|
$userInfo = $userInfoReal = $users->getUser($sessionInfo->getUserId(), 'id');
|
|
if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
|
|
try {
|
|
$userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
|
|
} catch(RuntimeException $ex) {
|
|
$userInfo = $userInfoReal;
|
|
}
|
|
}
|
|
|
|
$response = [];
|
|
$response['session'] = [
|
|
'token' => $sessionInfo->getToken(),
|
|
'created_at' => $sessionInfo->getCreatedTime(),
|
|
'expires_at' => $sessionInfo->getExpiresTime(),
|
|
'lifetime_extends' => $sessionInfo->shouldBumpExpires(),
|
|
];
|
|
|
|
$banInfo = $this->usersCtx->tryGetActiveBan($userInfo);
|
|
if($banInfo !== null)
|
|
$response['ban'] = [
|
|
'severity' => $banInfo->getSeverity(),
|
|
'reason' => $banInfo->getPublicReason(),
|
|
'created_at' => $banInfo->getCreatedTime(),
|
|
'is_permanent' => $banInfo->isPermanent(),
|
|
'expires_at' => $banInfo->getExpiresTime(),
|
|
'duration_str' => $banInfo->getDurationString(),
|
|
'remaining_str' => $banInfo->getRemainingString(),
|
|
];
|
|
|
|
$gatherRequestedAvatars = function($userInfo) use ($avatarResolutions, $baseUrl) {
|
|
$formatAvatarUrl = fn($res = 0) => (
|
|
$baseUrl . $this->urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => $res])
|
|
);
|
|
|
|
$avatars = ['original' => $formatAvatarUrl()];
|
|
|
|
foreach($avatarResolutions as $avatarRes)
|
|
$avatars[sprintf('x%d', $avatarRes)] = $formatAvatarUrl($avatarRes);
|
|
|
|
return $avatars;
|
|
};
|
|
|
|
$extractUserInfo = fn($userInfo) => [
|
|
'id' => $userInfo->getId(),
|
|
'name' => $userInfo->getName(),
|
|
'colour' => (string)$users->getUserColour($userInfo),
|
|
'rank' => $users->getUserRank($userInfo),
|
|
'is_super' => $userInfo->isSuperUser(),
|
|
'country_code' => $userInfo->getCountryCode(),
|
|
'is_deleted' => $userInfo->isDeleted(),
|
|
'has_totp' => $userInfo->hasTOTPKey(),
|
|
'profile_url' => $baseUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]),
|
|
'avatars' => $gatherRequestedAvatars($userInfo),
|
|
];
|
|
|
|
$response['user'] = $extractUserInfo($userInfo);
|
|
if($userInfo !== $userInfoReal) {
|
|
$response['guise'] = $extractUserInfo($userInfoReal);
|
|
|
|
$csrfp = CSRF::create($sessionInfo->getToken());
|
|
$response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrfp->createToken()]);
|
|
}
|
|
|
|
return self::createPayload('auth:check:success', $response);
|
|
}
|
|
}
|