misuzu/src/SharpChat/SharpChatRoutes.php

537 lines
19 KiB
PHP
Raw Normal View History

2022-09-13 13:14:49 +00:00
<?php
namespace Misuzu\SharpChat;
use Index\Colour\Colour;
use Index\Routing\IRouter;
use Index\Http\HttpFx;
use Misuzu\AuthToken;
2023-01-01 20:23:53 +00:00
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;
2022-09-13 13:14:49 +00:00
// Replace
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserWarning;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserWarningCreationFailedException;
2023-02-08 00:06:15 +00:00
use Misuzu\Users\UserSessionNotFoundException;
2022-09-13 13:14:49 +00:00
final class SharpChatRoutes {
2023-01-01 20:23:53 +00:00
private IConfig $config;
private Emotes $emotes;
2022-09-13 13:14:49 +00:00
private string $hashKey = 'woomy';
public function __construct(IRouter $router, IConfig $config, Emotes $emotes) {
2023-01-01 20:23:53 +00:00
$this->config = $config;
$this->emotes = $emotes;
2023-01-01 20:23:53 +00:00
2023-07-18 21:48:44 +00:00
$hashKey = $this->config->getString('hashKey', '');
2022-09-13 13:14:49 +00:00
if(empty($hashKey)) {
2023-07-18 21:48:44 +00:00
$hashKeyPath = $this->config->getString('hashKeyPath', '');
2022-09-13 13:14:49 +00:00
if(is_file($hashKeyPath))
$this->hashKey = file_get_contents($hashKeyPath);
} else {
$this->hashKey = $hashKey;
}
2023-02-08 00:06:15 +00:00
// Simplify default error pages
if($router instanceof HttpFx)
$router->use('/_sockchat', function() use($router) {
$router->addErrorHandler(400, function($response) {
$response->setContent('HTTP 400');
});
$router->addErrorHandler(403, function($response) {
$response->setContent('HTTP 403');
});
$router->addErrorHandler(404, function($response) {
$response->setContent('HTTP 404');
});
$router->addErrorHandler(500, function($response) {
$response->setContent('HTTP 500');
});
$router->addErrorHandler(503, function($response) {
$response->setContent('HTTP 503');
});
2023-02-08 00:06:15 +00:00
});
2022-09-13 13:14:49 +00:00
// Public endpoints
2023-02-08 00:06:15 +00:00
$router->get('/_sockchat/emotes', [$this, 'getEmotes']);
$router->get('/_sockchat/login', [$this, 'getLogin']);
$router->options('/_sockchat/token', [$this, 'getToken']);
$router->get('/_sockchat/token', [$this, 'getToken']);
2022-09-13 13:14:49 +00:00
// Private endpoints
2023-02-08 00:06:15 +00:00
$router->get('/_sockchat/resolve', [$this, 'getResolve']);
$router->post('/_sockchat/bump', [$this, 'postBump']);
$router->post('/_sockchat/verify', [$this, 'postVerify']);
$router->get('/_sockchat/bans', [$this, 'getBans']);
$router->get('/_sockchat/bans/list', [$this, 'getBanList']);
$router->get('/_sockchat/bans/check', [$this, 'getBanCheck']);
$router->post('/_sockchat/bans/create', [$this, 'postBanCreate']);
$router->delete('/_sockchat/bans/revoke', [$this, 'deleteBanRevoke']);
2022-09-13 13:14:49 +00:00
}
public function getEmotes($response, $request): array {
2022-09-13 13:14:49 +00:00
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$emotes = $this->emotes->getAllEmotes(withStrings: true);
2022-09-13 13:14:49 +00:00
$out = [];
foreach($emotes as $emoteInfo) {
2022-09-13 13:14:49 +00:00
$strings = [];
foreach($emoteInfo->getStrings() 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;
}
2023-02-08 00:06:15 +00:00
public function getLogin($response, $request): void {
2022-09-13 13:14:49 +00:00
$currentUser = User::getCurrent();
2023-01-01 20:23:53 +00:00
$configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal';
2023-07-18 21:48:44 +00:00
$chatPath = $this->config->getString($configKey, '/');
2022-09-13 13:14:49 +00:00
$response->redirect(
$currentUser === null
? url('auth-login')
: $chatPath
);
}
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;
2023-05-21 18:15:04 +00:00
if(!UserSession::hasCurrent() || !AuthToken::hasCurrent())
2022-09-13 13:14:49 +00:00
return ['ok' => false];
2023-05-21 18:15:04 +00:00
$token = AuthToken::getCurrent();
2022-09-13 13:14:49 +00:00
$session = UserSession::getCurrent();
2023-05-21 18:15:04 +00:00
if($session->getToken() !== $token->getSessionToken())
return ['ok' => false];
2022-09-13 13:14:49 +00:00
$user = $session->getUser();
2023-05-21 18:15:04 +00:00
$userId = $token->hasImpersonatedUserId() && $user->isSuper()
? $token->getImpersonatedUserId()
: $user->getId();
2022-09-13 13:14:49 +00:00
return [
'ok' => true,
2023-05-21 18:15:04 +00:00
'usr' => $userId,
2022-09-13 13:14:49 +00:00
'tkn' => $token->pack(),
];
}
2023-02-08 00:06:15 +00:00
public function getResolve($response, $request): array {
2022-09-13 13:14:49 +00:00
$userHash = $request->hasHeader('X-SharpChat-Signature')
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$method = (string)$request->getParam('m');
$param = (string)$request->getParam('p');
$realHash = hash_hmac('sha256', "resolve#{$method}#{$param}", $this->hashKey);
if(!hash_equals($realHash, $userHash))
return [];
try {
switch($method) {
case 'id':
$userInfo = User::byId((int)$param);
break;
case 'name':
$userInfo = User::byUsername($param);
break;
}
} catch(UserNotFoundException $ex) {}
if(!isset($userInfo))
return [];
return [
'user_id' => $userInfo->getId(),
'username' => $userInfo->getUsername(),
'colour_raw' => Colour::toMisuzu($userInfo->getColour()),
2022-09-13 13:14:49 +00:00
'rank' => $rank = $userInfo->getRank(),
'perms' => SharpChatPerms::convert($userInfo),
];
}
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 {
$bumpString = (string)$request->getContent();
$signature = $bumpString;
$userTime = 0;
$bumpList = [];
2023-02-08 00:06:15 +00:00
}
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;
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
if(empty($bumpString)) {
if($userTime < time() - 60)
return 403;
} else {
$bumpInfo = json_decode($bumpString);
if(empty($bumpInfo))
return;
foreach($bumpInfo as $bumpUser)
if(!empty($bumpUser->id) && !empty($bumpUser->ip))
$bumpList[$bumpUser->id] = $bumpUser->ip;
}
foreach($bumpList as $userId => $ipAddr)
User::byId($userId)->bumpActivity($ipAddr);
2022-09-13 13:14:49 +00:00
}
2023-02-08 00:06:15 +00:00
public function postVerify($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
2022-09-13 13:14:49 +00:00
if($request->isStreamContent())
$authInfo = json_decode((string)$request->getContent());
elseif($request->isJsonContent())
$authInfo = $request->getContent()->getContent(); // maybe change this api lol, this looks silly
2023-02-08 00:06:15 +00:00
elseif($request->isFormContent()) {
$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($authInfo->token) && !empty($authInfo->ip)) {
2023-02-08 00:06:15 +00:00
// user_id is discarded now
// tokens should be entirely unique anyway
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
$tokenParts = explode(':', $authInfo->token, 2);
if(count($tokenParts) < 2) {
$authMethod = '';
$authToken = $tokenParts[0];
} else [$authMethod, $authToken] = $tokenParts;
2022-09-13 13:14:49 +00:00
2023-02-08 00:06:15 +00:00
$ipAddress = $authInfo->ip;
$sigUserId = $authInfo->user_id ?? 0; // still need it for the signature
$signature = "{$sigUserId}#{$authInfo->token}#{$authInfo->ip}";
2022-09-13 13:14:49 +00:00
}
if(empty($authMethod) || empty($authToken) || empty($ipAddress))
2023-02-08 00:06:15 +00:00
return ['success' => false, 'reason' => 'data'];
if(empty($signature))
2023-02-08 00:06:15 +00:00
$signature = "verify#{$authMethod}#{$authToken}#{$ipAddress}";
2022-09-13 13:14:49 +00:00
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') {
$authTokenInfo = AuthToken::unpack($authToken);
if($authTokenInfo->isValid())
$authToken = $authTokenInfo->getSessionToken();
2022-09-13 13:14:49 +00:00
try {
2023-02-08 00:06:15 +00:00
$sessionInfo = UserSession::byToken($authToken);
2022-09-13 13:14:49 +00:00
} catch(UserSessionNotFoundException $ex) {
return ['success' => false, 'reason' => 'token'];
}
if($sessionInfo->hasExpired()) {
$sessionInfo->delete();
return ['success' => false, 'reason' => 'expired'];
}
2023-02-08 00:06:15 +00:00
$sessionInfo->bump($ipAddress);
$userInfo = $sessionInfo->getUser();
2023-05-21 18:15:04 +00:00
if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuper()) {
$userInfoReal = $userInfo;
try {
$userInfo = User::byId($authTokenInfo->getImpersonatedUserId());
} catch(UserNotFoundException $ex) {
$userInfo = $userInfoReal;
}
}
2022-09-13 13:14:49 +00:00
} else {
return ['success' => false, 'reason' => 'unsupported'];
}
2023-02-08 00:06:15 +00:00
if(empty($userInfo))
return ['success' => false, 'reason' => 'user'];
$userInfo->bumpActivity($ipAddress);
2022-09-13 13:14:49 +00:00
return [
'success' => true,
'user_id' => $userInfo->getId(),
'username' => $userInfo->getUsername(),
'colour_raw' => Colour::toMisuzu($userInfo->getColour()),
2022-09-13 13:14:49 +00:00
'rank' => $rank = $userInfo->getRank(),
'hierarchy' => $rank,
'is_silenced' => date('c', $userInfo->isSilenced() || $userInfo->isBanned() ? ($userInfo->isActiveWarningPermanent() ? strtotime('10 years') : $userInfo->getActiveWarningExpiration()) : 0),
'perms' => SharpChatPerms::convert($userInfo),
];
}
2023-02-08 00:06:15 +00:00
public function getBans($response, $request): array {
2022-09-13 13:14:49 +00:00
$userHash = $request->hasHeader('X-SharpChat-Signature')
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$realHash = hash_hmac('sha256', 'givemethebeans', $this->hashKey);
if(!hash_equals($realHash, $userHash))
return [];
$warnings = UserWarning::byActive();
$bans = [];
foreach($warnings as $warning) {
if(!$warning->isBan() || $warning->hasExpired())
continue;
$isPermanent = $warning->isPermanent();
$userInfo = $warning->getUser();
$bans[] = [
'user_id' => $userInfo->getId(),
'id' => $userInfo->getId(),
'username' => $userInfo->getUsername(),
'colour_raw' => Colour::toMisuzu($userInfo->getColour()),
2022-09-13 13:14:49 +00:00
'rank' => $rank = $userInfo->getRank(),
'ip' => $warning->getUserRemoteAddress(),
'is_permanent' => $isPermanent,
'expires' => date('c', $isPermanent ? 0x7FFFFFFF : $warning->getExpirationTime()),
'perms' => SharpChatPerms::convert($userInfo),
];
}
return $bans;
}
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;
$warnings = UserWarning::byActive();
$bans = [];
foreach($warnings as $warning) {
if(!$warning->isBan() || $warning->hasExpired())
continue;
$isPerma = $warning->isPermanent();
$userInfo = $warning->getUser();
$bans[] = [
'is_ban' => true,
'user_id' => (string)$userInfo->getId(),
'user_name' => $userInfo->getUsername(),
'user_colour' => Colour::toMisuzu($userInfo->getColour()),
'ip_addr' => $warning->getUserRemoteAddress(),
'is_perma' => $isPerma,
'expires' => date('c', $isPerma ? 0xFFFFFFFF : $warning->getExpirationTime()),
];
}
return $bans;
}
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
2023-02-08 00:06:15 +00:00
if(!empty($ipAddress))
$warning = UserWarning::byRemoteAddressActive($ipAddress);
if(empty($warning) && !empty($userId)) {
if($userIdIsName)
try {
$userInfo = User::byUsername($userId);
$userId = $userInfo->getId();
} catch(UserNotFoundException $ex) {
$userId = 0;
}
$warning = UserWarning::byUserIdActive((int)$userId);
2022-09-13 13:14:49 +00:00
}
2023-02-08 00:06:15 +00:00
if($warning === null)
return ['is_ban' => false];
$isPerma = $warning->isPermanent();
return [
'is_ban' => true,
'user_id' => (string)$warning->getUserId(),
'ip_addr' => $warning->getUserRemoteAddress(),
'is_perma' => $isPerma,
'expires' => date('c', $isPerma ? 0xFFFFFFFF : $warning->getExpirationTime()),
];
2022-09-13 13:14:49 +00:00
}
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.';
if($isPermanent)
$duration = -1;
elseif($duration < 1)
return 400;
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 {
2023-02-08 00:06:15 +00:00
$modInfo = User::byId((int)$modId);
2022-09-13 13:14:49 +00:00
} catch(UserNotFoundException $ex) {
return 404;
}
try {
2023-02-08 00:06:15 +00:00
$userInfo = User::byId((int)$userId);
2022-09-13 13:14:49 +00:00
} catch(UserNotFoundException $ex) {
return 404;
}
try {
UserWarning::create(
$userInfo,
$modInfo,
UserWarning::TYPE_BAHN,
$duration,
2023-02-08 00:06:15 +00:00
$reason,
null,
$userAddr,
$modAddr
2022-09-13 13:14:49 +00:00
);
} catch(UserWarningCreationFailedException $ex) {
return 500;
}
return 201;
}
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;
$warning = null;
switch($type) {
2023-02-08 00:06:15 +00:00
case 'addr':
2022-09-13 13:14:49 +00:00
$warning = UserWarning::byRemoteAddressActive($subject);
break;
case 'user':
$warning = UserWarning::byUserIdActive((int)$subject);
break;
}
if($warning === null)
return 404;
$warning->delete();
return 204;
}
}