2022-09-13 13:14:49 +00:00
|
|
|
<?php
|
|
|
|
namespace Misuzu\SharpChat;
|
|
|
|
|
2023-01-02 23:48:04 +00:00
|
|
|
use Index\Colour\Colour;
|
2023-01-06 20:22:03 +00:00
|
|
|
use Index\Routing\IRouter;
|
2023-01-01 20:23:53 +00:00
|
|
|
use Misuzu\Config\IConfig;
|
2022-09-13 13:14:49 +00:00
|
|
|
|
|
|
|
// Replace
|
|
|
|
use Misuzu\AuthToken;
|
|
|
|
use Misuzu\Emoticon;
|
|
|
|
use Misuzu\Users\User;
|
|
|
|
use Misuzu\Users\UserSession;
|
|
|
|
use Misuzu\Users\UserWarning;
|
|
|
|
use Misuzu\Users\UserNotFoundException;
|
|
|
|
use Misuzu\Users\UserWarningCreationFailedException;
|
|
|
|
|
|
|
|
final class SharpChatRoutes {
|
2023-01-01 20:23:53 +00:00
|
|
|
private IConfig $config;
|
2022-09-13 13:14:49 +00:00
|
|
|
private string $hashKey = 'woomy';
|
|
|
|
|
2023-01-06 20:22:03 +00:00
|
|
|
public function __construct(IRouter $router, IConfig $config) {
|
2023-01-01 20:23:53 +00:00
|
|
|
$this->config = $config;
|
|
|
|
|
2023-01-06 20:50:41 +00:00
|
|
|
$hashKey = $this->config->getValue('hashKey', IConfig::T_STR, '');
|
2022-09-13 13:14:49 +00:00
|
|
|
|
|
|
|
if(empty($hashKey)) {
|
2023-01-06 20:50:41 +00:00
|
|
|
$hashKeyPath = $this->config->getValue('hashKeyPath', IConfig::T_STR, '');
|
2022-09-13 13:14:49 +00:00
|
|
|
if(is_file($hashKeyPath))
|
|
|
|
$this->hashKey = file_get_contents($hashKeyPath);
|
|
|
|
} else {
|
|
|
|
$this->hashKey = $hashKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Public endpoints
|
|
|
|
$router->get('/_sockchat/emotes', [$this, 'emotes']);
|
|
|
|
$router->get('/_sockchat/login', [$this, 'login']);
|
|
|
|
$router->options('/_sockchat/token', [$this, 'token']);
|
|
|
|
$router->get('/_sockchat/token', [$this, 'token']);
|
|
|
|
|
|
|
|
// Private endpoints
|
|
|
|
$router->get('/_sockchat/resolve', [$this, 'resolve']);
|
|
|
|
$router->post('/_sockchat/bump', [$this, 'bump']);
|
|
|
|
$router->post('/_sockchat/verify', [$this, 'verify']);
|
|
|
|
$router->get('/_sockchat/bans', [$this, 'bans']);
|
|
|
|
$router->get('/_sockchat/bans/check', [$this, 'checkBan']);
|
|
|
|
$router->post('/_sockchat/bans/create', [$this, 'createBan']);
|
|
|
|
$router->delete('/_sockchat/bans/remove', [$this, 'removeBan']);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function emotes($response, $request): array {
|
|
|
|
$response->setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
$response->setHeader('Access-Control-Allow-Methods', 'GET');
|
|
|
|
|
|
|
|
$raw = Emoticon::all();
|
|
|
|
$out = [];
|
|
|
|
|
|
|
|
foreach($raw as $emote) {
|
|
|
|
$strings = [];
|
|
|
|
|
|
|
|
foreach($emote->getStrings() as $string)
|
|
|
|
$strings[] = sprintf(':%s:', $string->emote_string);
|
|
|
|
|
|
|
|
$out[] = [
|
|
|
|
'Text' => $strings,
|
|
|
|
'Image' => $emote->getUrl(),
|
|
|
|
'Hierarchy' => $emote->getRank(),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
}
|
|
|
|
|
2023-01-02 23:48:04 +00:00
|
|
|
public function login($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-01-06 20:50:41 +00:00
|
|
|
$chatPath = $this->config->getValue($configKey, IConfig::T_STR, '/');
|
2022-09-13 13:14:49 +00:00
|
|
|
|
|
|
|
$response->redirect(
|
|
|
|
$currentUser === null
|
|
|
|
? url('auth-login')
|
|
|
|
: $chatPath
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function token($response, $request) {
|
|
|
|
$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-01-06 20:50:41 +00:00
|
|
|
$whitelist = $this->config->getValue('origins', IConfig::T_ARR, []);
|
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;
|
|
|
|
|
|
|
|
if(!UserSession::hasCurrent())
|
|
|
|
return ['ok' => false];
|
|
|
|
|
|
|
|
$session = UserSession::getCurrent();
|
|
|
|
$user = $session->getUser();
|
|
|
|
$token = AuthToken::create($user, $session);
|
|
|
|
|
|
|
|
return [
|
|
|
|
'ok' => true,
|
|
|
|
'usr' => $user->getId(),
|
|
|
|
'tkn' => $token->pack(),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function resolve($response, $request): array {
|
|
|
|
$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(),
|
2023-01-02 23:48:04 +00:00
|
|
|
'colour_raw' => Colour::toMisuzu($userInfo->getColour()),
|
2022-09-13 13:14:49 +00:00
|
|
|
'rank' => $rank = $userInfo->getRank(),
|
|
|
|
'perms' => SharpChatPerms::convert($userInfo),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function bump($response, $request) {
|
|
|
|
if(!$request->isStringContent())
|
|
|
|
return 400;
|
|
|
|
|
|
|
|
$userHash = $request->hasHeader('X-SharpChat-Signature')
|
|
|
|
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
|
|
|
|
$bumpString = (string)$request->getContent();
|
|
|
|
$realHash = hash_hmac('sha256', $bumpString, $this->hashKey);
|
|
|
|
|
|
|
|
if(!hash_equals($realHash, $userHash))
|
|
|
|
return;
|
|
|
|
|
|
|
|
$bumpInfo = json_decode($bumpString);
|
|
|
|
|
|
|
|
if(empty($bumpInfo))
|
|
|
|
return;
|
|
|
|
|
|
|
|
foreach($bumpInfo as $bumpUser)
|
|
|
|
try {
|
|
|
|
User::byId($bumpUser->id)->bumpActivity($bumpUser->ip);
|
|
|
|
} catch(UserNotFoundException $ex) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function verify($response, $request): array {
|
|
|
|
if($request->isStreamContent())
|
|
|
|
$authInfo = json_decode((string)$request->getContent());
|
|
|
|
elseif($request->isJsonContent())
|
|
|
|
$authInfo = $request->getContent()->getContent(); // maybe change this api lol, this looks silly
|
|
|
|
else
|
|
|
|
return ['success' => false, 'reason' => 'request'];
|
|
|
|
|
|
|
|
$userHash = $request->hasHeader('X-SharpChat-Signature')
|
|
|
|
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
|
|
|
|
|
|
|
|
if(strlen($userHash) !== 64)
|
|
|
|
return ['success' => false, 'reason' => 'length'];
|
|
|
|
|
|
|
|
if(!isset($authInfo->user_id, $authInfo->token, $authInfo->ip))
|
|
|
|
return ['success' => false, 'reason' => 'data'];
|
|
|
|
|
|
|
|
$realHash = hash_hmac('sha256', implode('#', [$authInfo->user_id, $authInfo->token, $authInfo->ip]), $this->hashKey);
|
|
|
|
|
|
|
|
if(!hash_equals($realHash, $userHash))
|
|
|
|
return ['success' => false, 'reason' => 'hash'];
|
|
|
|
|
|
|
|
try {
|
|
|
|
$userInfo = User::byId($authInfo->user_id);
|
|
|
|
} catch(UserNotFoundException $ex) {
|
|
|
|
return ['success' => false, 'reason' => 'user'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$authMethod = mb_substr($authInfo->token, 0, 5);
|
|
|
|
|
|
|
|
if($authMethod === 'SESS:') {
|
|
|
|
$sessionToken = mb_substr($authInfo->token, 5);
|
|
|
|
|
|
|
|
$authToken = AuthToken::unpack($sessionToken);
|
|
|
|
if($authToken->isValid())
|
|
|
|
$sessionToken = $authToken->getSessionToken();
|
|
|
|
|
|
|
|
try {
|
|
|
|
$sessionInfo = UserSession::byToken($sessionToken);
|
|
|
|
} catch(UserSessionNotFoundException $ex) {
|
|
|
|
return ['success' => false, 'reason' => 'token'];
|
|
|
|
}
|
|
|
|
|
|
|
|
if($sessionInfo->getUserId() !== $userInfo->getId())
|
|
|
|
return ['success' => false, 'reason' => 'user'];
|
|
|
|
|
|
|
|
if($sessionInfo->hasExpired()) {
|
|
|
|
$sessionInfo->delete();
|
|
|
|
return ['success' => false, 'reason' => 'expired'];
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:33:03 +00:00
|
|
|
$sessionInfo->bump($authInfo->ip);
|
2022-09-13 13:14:49 +00:00
|
|
|
} else {
|
|
|
|
return ['success' => false, 'reason' => 'unsupported'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$userInfo->bumpActivity($authInfo->ip);
|
|
|
|
|
|
|
|
return [
|
|
|
|
'success' => true,
|
|
|
|
'user_id' => $userInfo->getId(),
|
|
|
|
'username' => $userInfo->getUsername(),
|
2023-01-02 23:48:04 +00:00
|
|
|
'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),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function bans($response, $request): array {
|
|
|
|
$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(),
|
2023-01-02 23:48:04 +00:00
|
|
|
'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;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function checkBan($response, $request): array {
|
|
|
|
$userHash = $request->hasHeader('X-SharpChat-Signature')
|
|
|
|
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
|
|
|
|
$ipAddress = (string)$request->getParam('a');
|
|
|
|
$userId = (int)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
|
|
|
|
$realHash = hash_hmac('sha256', "check#{$ipAddress}#{$userId}", $this->hashKey);
|
|
|
|
if(!hash_equals($realHash, $userHash))
|
|
|
|
return [];
|
|
|
|
|
|
|
|
$response = [];
|
|
|
|
$warning = UserWarning::byRemoteAddressActive($ipAddress)
|
|
|
|
?? UserWarning::byUserIdActive($userId);
|
|
|
|
|
|
|
|
if($warning !== null) {
|
|
|
|
$response['warning'] = $warning->getId();
|
|
|
|
$response['id'] = $warning->getUserId();
|
|
|
|
$response['user_id'] = $warning->getUserId();
|
|
|
|
$response['ip'] = $warning->getUserRemoteAddress();
|
|
|
|
$response['is_permanent'] = $warning->isPermanent();
|
|
|
|
$response['expires'] = date('c', $response['is_permanent'] ? 0x7FFFFFFF : $warning->getExpirationTime());
|
|
|
|
} else {
|
|
|
|
$response['expires'] = date('c', 0);
|
|
|
|
$response['is_permanent'] = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function createBan($response, $request): int {
|
|
|
|
if(!$request->isFormContent())
|
|
|
|
return 400;
|
|
|
|
|
|
|
|
$userHash = $request->hasHeader('X-SharpChat-Signature')
|
|
|
|
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
|
|
|
|
$content = $request->getContent();
|
|
|
|
$userId = (int)$content->getParam('u', FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
$modId = (int)$content->getParam('m', FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
$duration = (int)$content->getParam('d', FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
$isPermanent = (int)$content->getParam('p', FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
$reason = (string)$content->getParam('r');
|
|
|
|
|
|
|
|
$realHash = hash_hmac('sha256', "create#{$userId}#{$modId}#{$duration}#{$isPermanent}#{$reason}", $this->hashKey);
|
|
|
|
if(!hash_equals($realHash, $userHash))
|
|
|
|
return 403;
|
|
|
|
|
|
|
|
if(empty($reason))
|
|
|
|
$reason = 'Banned through chat.';
|
|
|
|
|
|
|
|
if($isPermanent)
|
|
|
|
$duration = -1;
|
|
|
|
elseif($duration < 1)
|
|
|
|
return 400;
|
|
|
|
|
|
|
|
try {
|
|
|
|
$userInfo = User::byId($userId);
|
|
|
|
} catch(UserNotFoundException $ex) {
|
|
|
|
return 404;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$modInfo = User::byId($modId);
|
|
|
|
} catch(UserNotFoundException $ex) {
|
|
|
|
return 404;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
UserWarning::create(
|
|
|
|
$userInfo,
|
|
|
|
$modInfo,
|
|
|
|
UserWarning::TYPE_BAHN,
|
|
|
|
$duration,
|
|
|
|
$reason
|
|
|
|
);
|
|
|
|
} catch(UserWarningCreationFailedException $ex) {
|
|
|
|
return 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 201;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function removeBan($response, $request): int {
|
|
|
|
$userHash = $request->hasHeader('X-SharpChat-Signature')
|
|
|
|
? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
|
|
|
|
$type = (string)$request->getParam('t');
|
|
|
|
$subject = (string)$request->getParam('s');
|
|
|
|
|
|
|
|
$realHash = hash_hmac('sha256', "remove#{$type}#{$subject}", $this->hashKey);
|
|
|
|
if(!hash_equals($realHash, $userHash))
|
|
|
|
return 403;
|
|
|
|
|
|
|
|
$warning = null;
|
|
|
|
switch($type) {
|
|
|
|
case 'ip':
|
|
|
|
$warning = UserWarning::byRemoteAddressActive($subject);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'user':
|
|
|
|
$warning = UserWarning::byUserIdActive((int)$subject);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if($warning === null)
|
|
|
|
return 404;
|
|
|
|
|
|
|
|
$warning->delete();
|
|
|
|
|
|
|
|
return 204;
|
|
|
|
}
|
|
|
|
}
|