misuzu/src/SharpChat/SharpChatRoutes.php

420 lines
15 KiB
PHP
Raw Normal View History

2022-09-13 13:14:49 +00:00
<?php
namespace Misuzu\SharpChat;
use RuntimeException;
use Index\Colour\Colour;
2024-03-30 03:14:03 +00:00
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler};
use Syokuhou\IConfig;
2023-09-10 00:04:53 +00:00
use Misuzu\RoutingContext;
use Misuzu\Auth\{AuthContext,AuthInfo,Sessions};
use Misuzu\Emoticons\Emotes;
2023-08-30 22:37:21 +00:00
use Misuzu\Perms\Permissions;
2023-09-08 20:40:48 +00:00
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\{Bans,UsersContext,UserInfo};
2022-09-13 13:14:49 +00:00
2024-03-30 03:14:03 +00:00
final class SharpChatRoutes extends RouteHandler {
private string $hashKey;
2022-09-13 13:14:49 +00:00
2023-07-28 20:06:12 +00:00
public function __construct(
private IConfig $config,
private IConfig $impersonateConfig, // this sucks lol
2023-09-08 20:40:48 +00:00
private URLRegistry $urls,
private UsersContext $usersCtx,
2023-09-08 00:43:00 +00:00
private AuthContext $authCtx,
private Emotes $emotes,
private Permissions $perms,
2023-09-08 00:43:00 +00:00
private AuthInfo $authInfo
2023-07-28 20:06:12 +00:00
) {
$this->hashKey = $this->config->getString('hashKey', 'woomy');
}
2022-09-13 13:14:49 +00:00
2024-03-30 03:14:03 +00:00
#[HttpOptions('/_sockchat/emotes')]
#[HttpGet('/_sockchat/emotes')]
public function getEmotes($response, $request): array|int {
2022-09-13 13:14:49 +00:00
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Access-Control-Allow-Headers', 'Cache-Control');
if($request->getMethod() === 'OPTIONS')
return 204;
2022-09-13 13:14:49 +00:00
2023-08-07 12:59:08 +00:00
$emotes = $this->emotes->getEmotes(orderBy: 'order');
2022-09-13 13:14:49 +00:00
$out = [];
foreach($emotes as $emoteInfo) {
2022-09-13 13:14:49 +00:00
$strings = [];
foreach($this->emotes->getEmoteStrings($emoteInfo) as $stringInfo)
$strings[] = sprintf(':%s:', $stringInfo->getString());
2022-09-13 13:14:49 +00:00
$out[] = [
'Text' => $strings,
'Image' => $emoteInfo->getUrl(),
'Hierarchy' => $emoteInfo->getMinRank(),
2022-09-13 13:14:49 +00:00
];
}
return $out;
}
2024-03-30 03:14:03 +00:00
#[HttpGet('/_sockchat/login')]
2023-09-08 00:43:00 +00:00
public function getLogin($response, $request) {
if(!$this->authInfo->isLoggedIn()) {
2023-09-08 20:40:48 +00:00
$response->redirect($this->urls->format('auth-login'));
return;
}
$response->redirect($this->config->getString(
($request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'),
'/'
));
2022-09-13 13:14:49 +00:00
}
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
if($impersonator->isSuperUser())
return true;
2024-02-21 00:31:25 +00:00
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
return in_array($targetId, $whitelist, true);
}
2024-03-30 03:14:03 +00:00
#[HttpOptions('/_sockchat/token')]
#[HttpGet('/_sockchat/token')]
2023-02-08 00:06:15 +00:00
public function getToken($response, $request) {
2022-09-13 13:14:49 +00:00
$host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : '';
$origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : '';
$originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
if(!empty($originHost) && $originHost !== $host) {
2023-07-18 21:48:44 +00:00
$whitelist = $this->config->getArray('origins', []);
2022-09-13 13:14:49 +00:00
if(!in_array($originHost, $whitelist))
return 403;
$originProto = strtolower(parse_url($origin, PHP_URL_SCHEME));
$origin = $originProto . '://' . $originHost;
$response->setHeader('Access-Control-Allow-Origin', $origin);
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
$response->setHeader('Access-Control-Allow-Credentials', 'true');
$response->setHeader('Vary', 'Origin');
}
if($request->getMethod() === 'OPTIONS')
return 204;
$tokenInfo = $this->authInfo->getTokenInfo();
2022-09-13 13:14:49 +00:00
if(!$tokenInfo->hasSessionToken())
return ['ok' => false, 'err' => 'token'];
2023-05-21 18:15:04 +00:00
2023-07-28 20:06:12 +00:00
try {
2023-09-08 00:43:00 +00:00
$sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $tokenInfo->getSessionToken());
2023-07-28 20:06:12 +00:00
} catch(RuntimeException $ex) {
return ['ok' => false, 'err' => 'session'];
}
if($sessionInfo->hasExpired())
return ['ok' => false, 'err' => 'expired'];
if($sessionInfo->getUserId() !== $tokenInfo->getUserId())
return ['ok' => false, 'err' => 'user'];
2023-07-28 20:06:12 +00:00
$userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
$userId = $tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())
? $tokenInfo->getImpersonatedUserId()
2023-07-28 20:06:12 +00:00
: $userInfo->getId();
2022-09-13 13:14:49 +00:00
2023-09-08 00:43:00 +00:00
$tokenPacker = $this->authCtx->createAuthTokenPacker();
2022-09-13 13:14:49 +00:00
return [
'ok' => true,
2023-07-28 20:06:12 +00:00
'usr' => (int)$userId,
'tkn' => $tokenPacker->pack($tokenInfo),
2022-09-13 13:14:49 +00:00
];
}
2024-03-30 03:14:03 +00:00
#[HttpPost('/_sockchat/bump')]
2023-02-08 00:06:15 +00:00
public function postBump($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature'))
2022-09-13 13:14:49 +00:00
return 400;
2023-02-08 00:06:15 +00:00
if($request->isFormContent()) {
$content = $request->getContent();
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
$bumpList = $content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
if(!is_array($bumpList))
return 400;
$userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
$signature = "bump#{$userTime}";
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
foreach($bumpList as $userId => $ipAddr)
$signature .= "#{$userId}:{$ipAddr}";
} else return 400;
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$realHash = hash_hmac('sha256', $signature, $this->hashKey);
if(!hash_equals($realHash, $userHash))
return 403;
if($userTime < time() - 60)
return 403;
2023-02-08 00:06:15 +00:00
foreach($bumpList as $userId => $ipAddr)
$this->usersCtx->getUsers()->recordUserActivity($userId, remoteAddr: $ipAddr);
2022-09-13 13:14:49 +00:00
}
2024-03-30 03:14:03 +00:00
#[HttpPost('/_sockchat/verify')]
2023-02-08 00:06:15 +00:00
public function postVerify($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
if($request->isFormContent()) {
2023-02-08 00:06:15 +00:00
$content = $request->getContent();
$authMethod = (string)$content->getParam('method');
$authToken = (string)$content->getParam('token');
$ipAddress = (string)$content->getParam('ipaddr');
} else
2022-09-13 13:14:49 +00:00
return ['success' => false, 'reason' => 'request'];
2023-02-08 00:06:15 +00:00
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
2022-09-13 13:14:49 +00:00
if(strlen($userHash) !== 64)
return ['success' => false, 'reason' => 'length'];
if(empty($authMethod) || empty($authToken) || empty($ipAddress))
2023-02-08 00:06:15 +00:00
return ['success' => false, 'reason' => 'data'];
$signature = "verify#{$authMethod}#{$authToken}#{$ipAddress}";
2023-02-08 00:06:15 +00:00
$realHash = hash_hmac('sha256', $signature, $this->hashKey);
if(!hash_equals($realHash, $userHash))
return ['success' => false, 'reason' => 'hash'];
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
if($authMethod === 'SESS' || $authMethod === 'Misuzu') {
2023-09-08 00:43:00 +00:00
$tokenPacker = $this->authCtx->createAuthTokenPacker();
$tokenInfo = $tokenPacker->unpack($authToken);
if($tokenInfo->isEmpty()) {
// don't support using the raw session key for Misuzu format
if($authMethod !== 'SESS')
return ['success' => false, 'reason' => 'format'];
$sessionToken = $authToken;
} else
$sessionToken = $tokenInfo->getSessionToken();
2022-09-13 13:14:49 +00:00
try {
2023-09-08 00:43:00 +00:00
$sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $sessionToken);
} catch(RuntimeException $ex) {
2022-09-13 13:14:49 +00:00
return ['success' => false, 'reason' => 'token'];
}
if($sessionInfo->hasExpired()) {
2023-09-08 00:43:00 +00:00
$this->authCtx->getSessions()->deleteSessions(sessionInfos: $sessionInfo);
2022-09-13 13:14:49 +00:00
return ['success' => false, 'reason' => 'expired'];
}
2023-09-08 00:43:00 +00:00
$this->authCtx->getSessions()->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
2023-02-08 00:06:15 +00:00
$userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
2023-05-21 18:15:04 +00:00
$userInfoReal = $userInfo;
try {
$userInfo = $this->usersCtx->getUsers()->getUser($tokenInfo->getImpersonatedUserId(), 'id');
} catch(RuntimeException $ex) {
2023-05-21 18:15:04 +00:00
$userInfo = $userInfoReal;
}
}
2022-09-13 13:14:49 +00:00
} else {
return ['success' => false, 'reason' => 'unsupported'];
}
$this->usersCtx->getUsers()->recordUserActivity($userInfo, remoteAddr: $ipAddress);
$userColour = $this->usersCtx->getUsers()->getUserColour($userInfo);
$userRank = $this->usersCtx->getUsers()->getUserRank($userInfo);
$chatPerms = $this->perms->getPermissions('chat', $userInfo);
2022-09-13 13:14:49 +00:00
return [
'success' => true,
'user_id' => (int)$userInfo->getId(),
'username' => $userInfo->getName(),
'colour_raw' => Colour::toMisuzu($userColour),
'rank' => $userRank,
'hierarchy' => $userRank,
'perms' => $chatPerms->getCalculated(),
2023-11-07 14:38:53 +00:00
'super' => $userInfo->isSuperUser(),
2022-09-13 13:14:49 +00:00
];
}
2024-03-30 03:14:03 +00:00
#[HttpGet('/_sockchat/bans/list')]
2023-02-08 00:06:15 +00:00
public function getBanList($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);
$realHash = hash_hmac('sha256', "list#{$userTime}", $this->hashKey);
if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
return 403;
$list = [];
$bans = $this->usersCtx->getBans()->getBans(activeOnly: true);
2023-02-08 00:06:15 +00:00
foreach($bans as $banInfo) {
$userId = $banInfo->getUserId();
$userInfo = $this->usersCtx->getUserInfo($userId);
$userColour = $this->usersCtx->getUserColour($userInfo);
$isPerma = $banInfo->isPermanent();
$list[] = [
2023-02-08 00:06:15 +00:00
'is_ban' => true,
'user_id' => $userId,
'user_name' => $userInfo->getName(),
'user_colour' => Colour::toMisuzu($userColour),
'ip_addr' => '::',
2023-02-08 00:06:15 +00:00
'is_perma' => $isPerma,
'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime())
2023-02-08 00:06:15 +00:00
];
}
return $list;
2023-02-08 00:06:15 +00:00
}
2024-03-30 03:14:03 +00:00
#[HttpGet('/_sockchat/bans/check')]
2023-02-08 00:06:15 +00:00
public function getBanCheck($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);
2022-09-13 13:14:49 +00:00
$ipAddress = (string)$request->getParam('a');
2023-02-08 00:06:15 +00:00
$userId = (string)$request->getParam('u');
$userIdIsName = (int)$request->getParam('n', FILTER_SANITIZE_NUMBER_INT);
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
$realHash = hash_hmac('sha256', "check#{$userTime}#{$userId}#{$ipAddress}#{$userIdIsName}", $this->hashKey);
if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
return 403;
2022-09-13 13:14:49 +00:00
if($userIdIsName)
try {
$userInfo = $this->usersCtx->getUsers()->getUser($userId, 'name');
$userId = (string)$userInfo->getId();
} catch(RuntimeException $ex) {
$userId = '';
}
2022-09-13 13:14:49 +00:00
$banInfo = $this->usersCtx->tryGetActiveBan($userId);
if($banInfo === null)
2023-02-08 00:06:15 +00:00
return ['is_ban' => false];
$isPerma = $banInfo->isPermanent();
2023-02-08 00:06:15 +00:00
return [
'is_ban' => true,
'user_id' => $banInfo->getUserId(),
'ip_addr' => '::',
2023-02-08 00:06:15 +00:00
'is_perma' => $isPerma,
'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime()),
2023-02-08 00:06:15 +00:00
];
2022-09-13 13:14:49 +00:00
}
2024-03-30 03:14:03 +00:00
#[HttpPost('/_sockchat/bans/create')]
2023-02-08 00:06:15 +00:00
public function postBanCreate($response, $request): int {
if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent())
2022-09-13 13:14:49 +00:00
return 400;
2023-02-08 00:06:15 +00:00
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
2022-09-13 13:14:49 +00:00
$content = $request->getContent();
2023-02-08 00:06:15 +00:00
$userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
$userId = (string)$content->getParam('ui', FILTER_SANITIZE_NUMBER_INT);
$userAddr = (string)$content->getParam('ua');
$modId = (string)$content->getParam('mi', FILTER_SANITIZE_NUMBER_INT);
$modAddr = (string)$content->getParam('ma');
2022-09-13 13:14:49 +00:00
$duration = (int)$content->getParam('d', FILTER_SANITIZE_NUMBER_INT);
$isPermanent = (int)$content->getParam('p', FILTER_SANITIZE_NUMBER_INT);
$reason = (string)$content->getParam('r');
2023-02-08 00:06:15 +00:00
$signature = implode('#', [
'create', $userTime, $userId, $userAddr,
$modId, $modAddr, $duration, $isPermanent, $reason,
]);
$realHash = hash_hmac('sha256', $signature, $this->hashKey);
if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
2022-09-13 13:14:49 +00:00
return 403;
if(empty($reason))
$reason = 'Banned through chat.';
// maybe also adds the last couple lines of the chat log to the private reason
$comment = sprintf('User IP address: %s, Moderator IP address: %s', $userAddr, $modAddr);
2022-09-13 13:14:49 +00:00
if($isPermanent)
$expires = null;
else {
$now = time();
$expires = $now + $duration;
if($expires < $now)
return 400;
}
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
// IPs cannot be banned on their own
// substituting with the unused Railgun account for now.
if(empty($userId))
$userId = 69;
2023-02-08 00:06:15 +00:00
if(empty($modId))
$modId = 69;
2022-09-13 13:14:49 +00:00
try {
$modInfo = $this->usersCtx->getUsers()->getUser($modId, 'id');
} catch(RuntimeException $ex) {
2022-09-13 13:14:49 +00:00
return 404;
}
try {
$userInfo = $this->usersCtx->getUsers()->getUser($userId, 'id');
} catch(RuntimeException $ex) {
2022-09-13 13:14:49 +00:00
return 404;
}
try {
$this->usersCtx->getBans()->createBan(
2022-09-13 13:14:49 +00:00
$userInfo,
$expires,
2023-02-08 00:06:15 +00:00
$reason,
$comment,
modInfo: $modInfo
2022-09-13 13:14:49 +00:00
);
} catch(RuntimeException $ex) {
2022-09-13 13:14:49 +00:00
return 500;
}
return 201;
}
2024-03-30 03:14:03 +00:00
#[HttpDelete('/_sockchat/bans/revoke')]
2023-02-08 00:06:15 +00:00
public function deleteBanRevoke($response, $request): int {
if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);
2022-09-13 13:14:49 +00:00
$type = (string)$request->getParam('t');
$subject = (string)$request->getParam('s');
2023-02-08 00:06:15 +00:00
$realHash = hash_hmac('sha256', "revoke#{$userTime}#{$type}#{$subject}", $this->hashKey);
if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
2022-09-13 13:14:49 +00:00
return 403;
if($type !== 'user')
return 404;
2022-09-13 13:14:49 +00:00
$banInfo = $this->usersCtx->tryGetActiveBan($subject);
if($banInfo === null)
2022-09-13 13:14:49 +00:00
return 404;
$this->usersCtx->getBans()->deleteBans($banInfo);
2022-09-13 13:14:49 +00:00
return 204;
}
}