Replaced Misuzu interop stuff with RPC library.

This commit is contained in:
Pachira 2024-08-16 19:30:24 +00:00
parent 8d6f7c5b7b
commit b43c1ad2fd
12 changed files with 347 additions and 157 deletions

View file

@ -8,7 +8,8 @@
"matomo/device-detector": "^6.1",
"sentry/sdk": "^4.0",
"phpseclib/phpseclib": "~3.0",
"nesbot/carbon": "^3.7"
"nesbot/carbon": "^3.7",
"flashwave/aiwass": "^1.0"
},
"autoload": {
"classmap": [

41
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "16b27dc9d1837e8435dc95cecc4eeea2",
"content-hash": "8cfa8c2738ab51aba3efea42afb68771",
"packages": [
{
"name": "carbonphp/carbon-doctrine-types",
@ -366,6 +366,45 @@
],
"time": "2023-10-06T06:47:41+00:00"
},
{
"name": "flashwave/aiwass",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://patchii.net/flashii/aiwass.git",
"reference": "de27da54b603f0fdc06dab89341908e73ea7a880"
},
"require": {
"ext-msgpack": ">=2.2",
"flashwave/index": "^0.2408.40014",
"php": ">=8.3"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Aiwass\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"bsd-3-clause-clear"
],
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"description": "Shared HTTP RPC client/server library.",
"homepage": "https://railgun.sh/aiwass",
"time": "2024-08-16T15:59:19+00:00"
},
{
"name": "flashwave/index",
"version": "v0.2408.40014",

View file

@ -14,8 +14,8 @@ class HanyuuContext {
private SasaeEnvironment $templating;
private SiteInfo $siteInfo;
private MisuzuInterop $misuzuInterop;
private object $authInfo;
private MisuzuRpcClient $misuzuRpc;
private MisuzuAuthInfo $authInfo;
private Apps\AppsContext $appsCtx;
private OAuth2\OAuth2Context $oauth2Ctx;
@ -24,7 +24,7 @@ class HanyuuContext {
$this->config = $config;
$this->dbConn = $dbConn;
$this->siteInfo = new SiteInfo($config->scopeTo('site'));
$this->misuzuInterop = new MisuzuInterop($config->scopeTo('misuzu'));
$this->misuzuRpc = new MisuzuRpcClient($config->scopeTo('misuzu'));
$this->appsCtx = new Apps\AppsContext($dbConn);
$this->oauth2Ctx = new OAuth2\OAuth2Context($dbConn);
@ -41,8 +41,8 @@ class HanyuuContext {
]);
}
public function getMisuzu(): MisuzuInterop {
return $this->misuzuInterop;
public function getMisuzuRpc(): MisuzuRpcClient {
return $this->misuzuRpc;
}
public function getDatabase(): IDbConnection {
@ -62,7 +62,7 @@ class HanyuuContext {
return new FsDbMigrationRepo(HAU_DIR_MIGRATIONS);
}
public function getAuthInfo(): object {
public function getAuthInfo(): MisuzuAuthInfo {
return $this->authInfo;
}
@ -83,7 +83,7 @@ class HanyuuContext {
$routingCtx = new RoutingContext($this->getTemplating());
$routingCtx->getRouter()->use('/', function($response, $request) {
$this->authInfo = $this->misuzuInterop->authCheck(
$this->authInfo = $this->misuzuRpc->authCheck(
'Misuzu',
(string)$request->getCookie('msz_auth'),
$_SERVER['REMOTE_ADDR'],

44
src/MisuzuAuthBanInfo.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace Hanyuu;
class MisuzuAuthBanInfo {
public function __construct(
private array $info
) {}
public function getSeverity(): int {
return array_key_exists('severity', $this->info) && is_int($this->info['severity'])
? $this->info['severity'] : 0;
}
public function getReason(): string {
return array_key_exists('reason', $this->info) && is_string($this->info['reason'])
? $this->info['reason'] : '';
}
public function getCreatedAt(): int {
return array_key_exists('created_at', $this->info) && is_int($this->info['created_at'])
? $this->info['created_at'] : 0;
}
public function isPermanent(): bool {
return array_key_exists('is_permanent', $this->info)
&& is_bool($this->info['is_permanent'])
&& $this->info['is_permanent'];
}
public function getExpiresAt(): int {
return array_key_exists('expires_at', $this->info) && is_int($this->info['expires_at'])
? $this->info['expires_at'] : 0;
}
public function getDurationString(): string {
return array_key_exists('duration_str', $this->info) && is_string($this->info['duration_str'])
? $this->info['duration_str'] : '';
}
public function getRemainingString(): string {
return array_key_exists('remaining_str', $this->info) && is_string($this->info['remaining_str'])
? $this->info['remaining_str'] : '';
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace Hanyuu;
class MisuzuAuthGuiseInfo extends MisuzuAuthUserInfo {
public function __construct(
private array $info
) {
parent::__construct($info);
}
public function getRevertUrl(): string {
return array_key_exists('revert_url', $this->info) && is_string($this->info['revert_url'])
? $this->info['revert_url'] : '';
}
}

51
src/MisuzuAuthInfo.php Normal file
View file

@ -0,0 +1,51 @@
<?php
namespace Hanyuu;
class MisuzuAuthInfo {
public function __construct(
private array $info
) {}
public function isFailure(): bool {
return array_key_exists('reason', $this->info);
}
public function getFailureReason(): string {
return array_key_exists('reason', $this->info) && is_string($this->info['reason'])
? $this->info['reason'] : '';
}
public function getLoginUrl(): string {
return array_key_exists('login_url', $this->info) && is_string($this->info['login_url'])
? $this->info['login_url'] : '';
}
public function getRegisterUrl(): string {
return array_key_exists('register_url', $this->info) && is_string($this->info['register_url'])
? $this->info['register_url'] : '';
}
public function getSessionInfo(): MisuzuAuthSessionInfo {
return new MisuzuAuthSessionInfo($this->info['session'] ?? []);
}
public function isBanned(): bool {
return array_key_exists('ban', $this->info);
}
public function getBanInfo(): MisuzuAuthBanInfo {
return new MisuzuAuthBanInfo($this->info['ban'] ?? []);
}
public function getUserInfo(): MisuzuAuthUserInfo {
return new MisuzuAuthUserInfo($this->info['user'] ?? []);
}
public function isGuise(): bool {
return array_key_exists('guise', $this->info);
}
public function getGuiseInfo(): MisuzuAuthGuiseInfo {
return new MisuzuAuthGuiseInfo($this->info['guise'] ?? []);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Hanyuu;
class MisuzuAuthSessionInfo {
public function __construct(
private array $info
) {}
public function getToken(): string {
return array_key_exists('token', $this->info) && is_string($this->info['token'])
? $this->info['token'] : '';
}
public function getCreatedAt(): int {
return array_key_exists('created_at', $this->info) && is_int($this->info['created_at'])
? $this->info['created_at'] : 0;
}
public function getExpiresAt(): int {
return array_key_exists('expires_at', $this->info) && is_int($this->info['expires_at'])
? $this->info['expires_at'] : 0;
}
public function doesLifetimeExtend(): bool {
return array_key_exists('lifetime_extends', $this->info)
&& is_bool($this->info['lifetime_extends'])
&& $this->info['lifetime_extends'];
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Hanyuu;
class MisuzuAuthUserInfo {
public function __construct(
private array $info
) {}
public function getId(): string {
return array_key_exists('id', $this->info) && is_string($this->info['id'])
? $this->info['id'] : '';
}
public function getName(): string {
return array_key_exists('name', $this->info) && is_string($this->info['name'])
? $this->info['name'] : '';
}
public function getColour(): string {
return array_key_exists('colour', $this->info) && is_string($this->info['colour'])
? $this->info['colour'] : 'inherit';
}
public function getRank(): int {
return array_key_exists('rank', $this->info) && is_int($this->info['rank'])
? $this->info['rank'] : 0;
}
public function isSuper(): bool {
return array_key_exists('is_super', $this->info)
&& is_bool($this->info['is_super'])
&& $this->info['is_super'];
}
public function getCountryCode(): string {
return array_key_exists('country_code', $this->info) && is_string($this->info['country_code'])
? $this->info['country_code'] : '';
}
public function isDeleted(): bool {
return array_key_exists('is_deleted', $this->info)
&& is_bool($this->info['is_deleted'])
&& $this->info['is_deleted'];
}
public function has2fa(): bool {
return array_key_exists('has_totp', $this->info)
&& is_bool($this->info['has_totp'])
&& $this->info['has_totp'];
}
public function getProfileUrl(): string {
return array_key_exists('profile_url', $this->info) && is_string($this->info['profile_url'])
? $this->info['profile_url'] : '';
}
public function getAvatars(): array {
return array_key_exists('avatars', $this->info) && is_array($this->info['avatars'])
? $this->info['avatars'] : [];
}
public function getAvatar(string $variant): string {
$avatars = $this->getAvatars();
if(array_key_exists($variant, $avatars))
return $avatars[$variant];
if(array_key_exists('original', $avatars))
return $avatars['original'];
return '';
}
}

View file

@ -1,111 +0,0 @@
<?php
namespace Hanyuu;
use stdClass;
use RuntimeException;
use Index\UriBase64;
use Syokuhou\IConfig;
class MisuzuInterop {
public function __construct(
private IConfig $config
) {}
private function getEndpoint(): string {
return $this->config->getString('endpoint');
}
private function getSecret(): string {
return $this->config->getString('secret');
}
public function authCheck(string $method, string $token, string $remoteAddr, array $avatars = []): object {
$result = $this->callRpc('auth-check', body: [
'method' => $method,
'token' => $token,
'remote_addr' => $remoteAddr,
'avatars' => implode(',', $avatars),
]);
if(!str_starts_with($result->name, 'auth:check:'))
return new stdClass;
return $result->attrs;
}
private function callRpc(string $action, array $params = [], array $body = []): object {
$time = time();
$url = sprintf('%s/_hanyuu/%s', $this->getEndpoint(), $action);
if(empty($params))
$params = '';
else {
$params = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$url .= sprintf('?%s', $params);
}
$hasBody = !empty($body);
$body = $hasBody ? http_build_query($body, '', '&', PHP_QUERY_RFC3986) : '';
$signature = UriBase64::encode(hash_hmac(
'sha256',
sprintf('[%s|%s|%s]', $time, $url, $body),
$this->getSecret(),
true
));
$headers = [
sprintf('X-Hanyuu-Timestamp: %s', $time),
sprintf('X-Hanyuu-Signature: %s', $signature),
];
if($hasBody)
$headers[] = 'Content-Type: application/x-www-form-urlencoded';
$req = curl_init($url);
try {
curl_setopt_array($req, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HEADER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Hanyuu',
CURLOPT_HTTPHEADER => $headers,
]);
if($hasBody)
curl_setopt_array($req, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($req);
if($response === false)
throw new RuntimeException(curl_error($req));
} finally {
curl_close($req);
}
$decoded = json_decode($response);
if($decoded === null)
throw new RuntimeException($response);
if(empty($decoded->name) || !isset($decoded->attrs))
throw new RuntimeException('Missing name or attrs attribute in response body.');
if($decoded->name === 'error') {
$line = $decoded->attrs->code ?? 'unknown';
if(!empty($decoded->attrs->text))
$line = sprintf('[%s] %s', $line, $decoded->attrs->text);
throw new RuntimeException($line);
}
return $decoded;
}
}

39
src/MisuzuRpcClient.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace Hanyuu;
use stdClass;
use RuntimeException;
use Aiwass\Client\RpcClient;
use Index\UriBase64;
use Syokuhou\IConfig;
class MisuzuRpcClient {
private RpcClient $client;
public function __construct(IConfig $config) {
$this->client = RpcClient::createHmac(
sprintf('%s/_hanyuu', $config->getString('endpoint')),
fn() => $config->getString('secret')
);
}
public function getRpcClient(): RpcClient {
return $this->client;
}
public function authCheck(string $method, string $token, string $remoteAddr, array $avatars = []): MisuzuAuthInfo {
$result = $this->client->procedure('mszhau:authCheck', [
'method' => $method,
'token' => $token,
'remoteAddr' => $remoteAddr,
'avatars' => implode(',', $avatars),
]);
if(empty($result['name']))
throw new RuntimeException('Unexpected response from Misuzu.');
if(!str_starts_with($result['name'], 'auth:check:'))
throw new RuntimeException($result['name']);
return new MisuzuAuthInfo($result['attrs']);
}
}

View file

@ -66,10 +66,13 @@ final class OAuth2Routes extends RouteHandler {
#[HttpGet('/oauth2/authorise')]
public function getAuthorise($response, $request) {
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return $this->templating->render('oauth2/login', ['auth' => $authInfo]);
if($authInfo->isFailure())
return $this->templating->render('oauth2/login', [
'login_url' => $authInfo->getLoginUrl(),
'register_url' => $authInfo->getRegisterUrl(),
]);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->getSessionInfo()->getToken());
return $this->templating->render('oauth2/authorise', [
'csrfp_token' => $csrfp->createToken(),
@ -84,12 +87,12 @@ final class OAuth2Routes extends RouteHandler {
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
if($authInfo->isFailure())
return ['error' => 'auth'];
$content = $request->getContent();
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->getSessionInfo()->getToken());
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
return ['error' => 'csrf'];
@ -145,7 +148,7 @@ final class OAuth2Routes extends RouteHandler {
try {
$authsInfo = $authsData->createAuthorisation(
$appInfo,
$authInfo->user->id,
$authInfo->getUserInfo()->getId(),
$redirectUriId,
$codeChallenge,
$codeChallengeMethod,
@ -166,10 +169,11 @@ final class OAuth2Routes extends RouteHandler {
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
if($authInfo->isFailure())
return ['error' => 'auth'];
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$sessionInfo = $authInfo->getSessionInfo();
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $sessionInfo->getToken());
if(!$csrfp->verifyToken((string)$request->getParam('csrfp')))
return ['error' => 'csrf'];
@ -196,6 +200,7 @@ final class OAuth2Routes extends RouteHandler {
return ['error' => 'scope'];
}
$userInfo = $authInfo->getUserInfo();
$result = [
'app' => [
'name' => $appInfo->getName(),
@ -206,21 +211,23 @@ final class OAuth2Routes extends RouteHandler {
],
],
'user' => [
'name' => $authInfo->user->name,
'colour' => $authInfo->user->colour,
'profile_uri' => $authInfo->user->profile_url,
'avatar_uri' => $authInfo->user->avatars->x120,
'name' => $userInfo->getName(),
'colour' => $userInfo->getColour(),
'profile_uri' => $userInfo->getProfileUrl(),
'avatar_uri' => $userInfo->getAvatar('x120'),
],
];
if(isset($authInfo->guise))
if($authInfo->isGuise()) {
$guiseInfo = $authInfo->getGuiseInfo();
$result['user']['guise'] = [
'name' => $authInfo->guise->name,
'colour' => $authInfo->guise->colour,
'profile_uri' => $authInfo->guise->profile_url,
'revert_uri' => $authInfo->guise->revert_url,
'avatar_uri' => $authInfo->guise->avatars->x60,
'name' => $guiseInfo->getName(),
'colour' => $guiseInfo->getColour(),
'profile_uri' => $guiseInfo->getProfileUrl(),
'revert_uri' => $guiseInfo->getRevertUrl(),
'avatar_uri' => $guiseInfo->getAvatar('x60'),
];
}
return $result;
}
@ -228,10 +235,13 @@ final class OAuth2Routes extends RouteHandler {
#[HttpGet('/oauth2/verify')]
public function getVerify($response, $request) {
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return $this->templating->render('oauth2/login', ['auth' => $authInfo]);
if($authInfo->isFailure())
return $this->templating->render('oauth2/login', [
'login_url' => $authInfo->getLoginUrl(),
'register_url' => $authInfo->getRegisterUrl(),
]);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->getSessionInfo()->getToken());
return $this->templating->render('oauth2/verify', [
'csrfp_token' => $csrfp->createToken(),
@ -246,12 +256,12 @@ final class OAuth2Routes extends RouteHandler {
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
if($authInfo->isFailure())
return ['error' => 'auth'];
$content = $request->getContent();
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->getSessionInfo()->getToken());
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
return ['error' => 'csrf'];
@ -270,7 +280,7 @@ final class OAuth2Routes extends RouteHandler {
return ['error' => 'invalid'];
$approved = $approve === 'yes';
$devicesData->setDeviceApproval($deviceInfo, $approved, $authInfo->user->id);
$devicesData->setDeviceApproval($deviceInfo, $approved, $authInfo->getUserInfo()->getId());
return [
'approval' => $approved ? 'approved' : 'denied',
@ -282,10 +292,10 @@ final class OAuth2Routes extends RouteHandler {
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
if($authInfo->isFailure())
return ['error' => 'auth'];
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->getSessionInfo()->getToken());
if(!$csrfp->verifyToken((string)$request->getParam('csrfp')))
return ['error' => 'csrf'];
@ -306,6 +316,7 @@ final class OAuth2Routes extends RouteHandler {
return ['error' => 'code'];
}
$userInfo = $authInfo->getUserInfo();
$result = [
'req' => [
'code' => $deviceInfo->getUserCode(),
@ -319,21 +330,23 @@ final class OAuth2Routes extends RouteHandler {
],
],
'user' => [
'name' => $authInfo->user->name,
'colour' => $authInfo->user->colour,
'profile_uri' => $authInfo->user->profile_url,
'avatar_uri' => $authInfo->user->avatars->x120,
'name' => $userInfo->getName(),
'colour' => $userInfo->getColour(),
'profile_uri' => $userInfo->getProfileUrl(),
'avatar_uri' => $userInfo->getAvatar('x120'),
],
];
if(isset($authInfo->guise))
if($authInfo->isGuise()) {
$guiseInfo = $authInfo->getGuiseInfo();
$result['user']['guise'] = [
'name' => $authInfo->guise->name,
'colour' => $authInfo->guise->colour,
'profile_uri' => $authInfo->guise->profile_url,
'revert_uri' => $authInfo->guise->revert_url,
'avatar_uri' => $authInfo->guise->avatars->x60,
'name' => $guiseInfo->getName(),
'colour' => $guiseInfo->getColour(),
'profile_uri' => $guiseInfo->getProfileUrl(),
'revert_uri' => $guiseInfo->getRevertUrl(),
'avatar_uri' => $guiseInfo->getAvatar('x60'),
];
}
return $result;
}

View file

@ -10,7 +10,7 @@
<p>A third-party application is requesting permission to access your account.</p>
{% endif %}
<p>You must be logged in to authorise applications. <a href="{{ auth.login_url }}" target="_blank">Log in</a> or <a href="{{ auth.register_url }}" target="_blank">create an account</a> and reload this page to try again.</p>
<p>You must be logged in to authorise applications. <a href="{{ login_url }}" target="_blank">Log in</a> or <a href="{{ register_url }}" target="_blank">create an account</a> and reload this page to try again.</p>
{% if app is defined and app.isTrusted %}
<p>You will be redirected to the following application after logging in.</p>