config = $config; $hashKey = $this->config->getValue('hashKey', IConfig::T_STR, ''); if(empty($hashKey)) { $hashKeyPath = $this->config->getValue('hashKeyPath', IConfig::T_STR, ''); if(is_file($hashKeyPath)) $this->hashKey = file_get_contents($hashKeyPath); } else { $this->hashKey = $hashKey; } // Simplify default error pages $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'); }); }); // Public endpoints $router->get('/_sockchat/emotes', [$this, 'getEmotes']); $router->get('/_sockchat/login', [$this, 'getLogin']); $router->options('/_sockchat/token', [$this, 'getToken']); $router->get('/_sockchat/token', [$this, 'getToken']); // Private endpoints $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']); } public static function getEmotes($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; } public function getLogin($response, $request): void { $currentUser = User::getCurrent(); $configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'; $chatPath = $this->config->getValue($configKey, IConfig::T_STR, '/'); $response->redirect( $currentUser === null ? url('auth-login') : $chatPath ); } public function getToken($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) { $whitelist = $this->config->getValue('origins', IConfig::T_ARR, []); 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 getResolve($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(), 'colour_raw' => Colour::toMisuzu($userInfo->getColour()), 'rank' => $rank = $userInfo->getRank(), 'perms' => SharpChatPerms::convert($userInfo), ]; } public function postBump($response, $request) { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; if($request->isFormContent()) { $content = $request->getContent(); $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}"; foreach($bumpList as $userId => $ipAddr) $signature .= "#{$userId}:{$ipAddr}"; } else { $bumpString = (string)$request->getContent(); $signature = $bumpString; } $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $realHash = hash_hmac('sha256', $signature, $this->hashKey); if(!hash_equals($realHash, $userHash)) return 403; if(empty($bumpString)) { if($userTime < time() - 60) return 403; } else { $bumpList = []; $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); } public function postVerify($response, $request) { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; if($request->isStreamContent()) $authInfo = json_decode((string)$request->getContent()); elseif($request->isJsonContent()) $authInfo = $request->getContent()->getContent(); // maybe change this api lol, this looks silly elseif($request->isFormContent()) { $content = $request->getContent(); $authMethod = (string)$content->getParam('method'); $authToken = (string)$content->getParam('token'); $ipAddress = (string)$content->getParam('ipaddr'); } else return ['success' => false, 'reason' => 'request']; $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); if(strlen($userHash) !== 64) return ['success' => false, 'reason' => 'length']; if(isset($authInfo) && !empty($authInfo->token) && !empty($authInfo->ip)) { // user_id is discarded now // tokens should be entirely unique anyway $tokenParts = explode(':', $authInfo->token, 2); if(count($tokenParts) < 2) { $authMethod = ''; $authToken = $tokenParts[0]; } else [$authMethod, $authToken] = $tokenParts; $ipAddress = $authInfo->ip; $signature = "{$authInfo->user_id}#{$authInfo->token}#{$authInfo->ip}"; } if(empty($authMethod) || empty($authToken)) return ['success' => false, 'reason' => 'data']; if(!isset($signature)) $signature = "verify#{$authMethod}#{$authToken}#{$ipAddress}"; $realHash = hash_hmac('sha256', $signature, $this->hashKey); if(!hash_equals($realHash, $userHash)) return ['success' => false, 'reason' => 'hash']; if($authMethod === 'SESS' || $authMethod === 'Misuzu') { $authTokenInfo = AuthToken::unpack($authToken); if($authTokenInfo->isValid()) $authToken = $authTokenInfo->getSessionToken(); try { $sessionInfo = UserSession::byToken($authToken); } catch(UserSessionNotFoundException $ex) { return ['success' => false, 'reason' => 'token']; } if($sessionInfo->hasExpired()) { $sessionInfo->delete(); return ['success' => false, 'reason' => 'expired']; } $sessionInfo->bump($ipAddress); $userInfo = $sessionInfo->getUser(); } else { return ['success' => false, 'reason' => 'unsupported']; } if(empty($userInfo)) return ['success' => false, 'reason' => 'user']; $userInfo->bumpActivity($ipAddress); return [ 'success' => true, 'user_id' => $userInfo->getId(), 'username' => $userInfo->getUsername(), 'colour_raw' => Colour::toMisuzu($userInfo->getColour()), '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 getBans($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(), 'colour_raw' => Colour::toMisuzu($userInfo->getColour()), '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 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); $ipAddress = (string)$request->getParam('a'); $userId = (string)$request->getParam('u'); $userIdIsName = (int)$request->getParam('n', FILTER_SANITIZE_NUMBER_INT); $realHash = hash_hmac('sha256', "check#{$userTime}#{$userId}#{$ipAddress}#{$userIdIsName}", $this->hashKey); if(!hash_equals($realHash, $userHash) || $userTime < time() - 60) return 403; 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); } 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()), ]; } public function postBanCreate($response, $request): int { if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent()) return 400; $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $content = $request->getContent(); $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'); $duration = (int)$content->getParam('d', FILTER_SANITIZE_NUMBER_INT); $isPermanent = (int)$content->getParam('p', FILTER_SANITIZE_NUMBER_INT); $reason = (string)$content->getParam('r'); $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) return 403; if(empty($reason)) $reason = 'Banned through chat.'; if($isPermanent) $duration = -1; elseif($duration < 1) return 400; // IPs cannot be banned on their own // substituting with the unused Railgun account for now. if(empty($targetId)) $targetId = 69; if(empty($modId)) $modId = 69; try { $modInfo = User::byId((int)$modId); } catch(UserNotFoundException $ex) { return 404; } try { $userInfo = User::byId((int)$userId); } catch(UserNotFoundException $ex) { return 404; } try { UserWarning::create( $userInfo, $modInfo, UserWarning::TYPE_BAHN, $duration, $reason, null, $userAddr, $modAddr ); } catch(UserWarningCreationFailedException $ex) { return 500; } return 201; } 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); $type = (string)$request->getParam('t'); $subject = (string)$request->getParam('s'); $realHash = hash_hmac('sha256', "revoke#{$userTime}#{$type}#{$subject}", $this->hashKey); if(!hash_equals($realHash, $userHash) || $userTime < time() - 60) return 403; $warning = null; switch($type) { case 'addr': $warning = UserWarning::byRemoteAddressActive($subject); break; case 'user': $warning = UserWarning::byUserIdActive((int)$subject); break; } if($warning === null) return 404; $warning->delete(); return 204; } }