diff --git a/CHANGELOG.md b/CHANGELOG.md index 946941d..da98519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ ### Refactor - **Config:** Remove params `$throw` in Config()->get() (706cc9a) +- **RateLimit:** Change last param of isRateLimitHit and rate limit store Namespace (4dd571d) - **View:** Make View extends BaseObject (0865cf9) +- **torrent/structure:** Use zui.tree instead javascript `$(this).next('ul').toggle()` (7b20b2c) - **view:** Fix helper/username params (720f37e) diff --git a/README.md b/README.md index 3e60083..521dedb 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ Or you can join our chat group on Telegram -- [@ridpt](https://t.me/ridpt) | [MixPHP](https://github.com/mix-php/mix-framework/tree/v1) | Framework | ( Chinese Version ) | | [siriusphp/validation](https://github.com/siriusphp/validation) | Validator | | | [league/plates](https://github.com/thephpleague/plates) | Template system | | +| [firebase/php-jwt](https://github.com/firebase/php-jwt) | JWT | , | ## License [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FRhilip%2FRidPT.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FRhilip%2FRidPT?ref=badge_large) diff --git a/apps/components/Site.php b/apps/components/Site.php index 34bfc1c..678c390 100644 --- a/apps/components/Site.php +++ b/apps/components/Site.php @@ -12,10 +12,15 @@ use apps\libraries\Mailer; use apps\libraries\Constant; +use Exception; +use Firebase\JWT\ExpiredException; use Rid\Http\View; use Rid\Base\Component; use Rid\Utils\ClassValueCacheUtils; +use Firebase\JWT\JWT; +use RuntimeException; + class Site extends Component { use ClassValueCacheUtils; @@ -35,6 +40,7 @@ public function onRequestBefore() { parent::onRequestBefore(); $this->cur_user = null; + $this->users = []; $this->torrents = []; $this->map_username_to_id = []; @@ -47,9 +53,9 @@ protected static function getStaticCacheNameSpace(): string public function getBanIpsList(): array { - return $this->getCacheValue('ip_ban_list', function () { + return static::getStaticCacheValue('ip_ban_list', function () { return app()->pdo->createCommand('SELECT `ip` FROM `ban_ips`')->queryColumn(); - }); + }, 86400); } public function getTorrent($tid) @@ -120,42 +126,93 @@ public function getCurUser($grant = 'cookies') protected function loadCurUser($grant = 'cookies') { $user_id = false; - if ($grant == 'cookies') $user_id = $this->loadCurUserFromCookies(); - elseif ($grant == 'passkey') $user_id = $this->loadCurUserFromPasskey(); - // elseif ($grant == 'oath2') $user_id = $this->loadCurUserFromOAth2(); + if ($grant == 'cookies') $user_id = $this->loadCurUserIdFromCookies(); + elseif ($grant == 'passkey') $user_id = $this->loadCurUserIdFromPasskey(); + // elseif ($grant == 'oath2') $user_id = $this->loadCurUserIdFromOAth2(); if ($user_id !== false) { $user_id = intval($user_id); - return $this->getUser($user_id); + $curuser = $this->getUser($user_id); + if ($curuser->getStatus() !== models\User::STATUS_DISABLED) // user status shouldn't be disabled + return $this->getUser($user_id); } return false; } - protected function loadCurUserFromCookies() + protected function loadCurUserIdFromCookies() { + $timenow = time(); $user_session_id = app()->request->cookie(Constant::cookie_name); if (is_null($user_session_id)) return false; // quick return when cookies is not exist - if (false === $user_id = app()->redis->zScore(Constant::mapUserSessionToId, $user_session_id)) { - // First check cache - if (false === app()->redis->zScore(Constant::invalidUserSessionZset, $user_session_id)) { - // check session from database to avoid lost - $user_id = app()->pdo->createCommand('SELECT `uid` FROM `user_session_log` WHERE `sid` = :sid LIMIT 1;')->bindParams([ - 'sid' => $user_session_id - ])->queryScalar(); - if (false === $user_id) { // This session is not exist - app()->redis->zAdd(Constant::invalidUserSessionZset, time() + 86400, $user_session_id); - } else { // Remember it - app()->redis->zAdd(Constant::mapUserSessionToId, $user_id, $user_session_id); + $key = env('APP_SECRET_KEY'); + + try { + $decoded = JWT::decode($user_session_id, $key, ['HS256']); + } catch (Exception $e) { + if ($e instanceof ExpiredException) { // Lazy Expired Check ..... + // Since in this case , we can't get payload with jti information directly, we should manually decode jwt content + list($headb64, $bodyb64, $cryptob64) = explode('.', $user_session_id); + $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64)); + $jti = $payload->jti ?? ''; + if ($jti && strlen($jti) === 64) { + app()->redis->zAdd(Constant::invalidUserSessionZset, $timenow + 86400, $jti); + app()->pdo->createCommand('UPDATE `user_session_log` SET `expired` = 1 WHERE `sid` = :sid')->bindParams([ + 'sid' => $jti + ])->execute(); } } + app()->session->set('jwt_error_msg', $e->getMessage()); // Store error msg + return false; } - return $user_id; + $decoded_array = (array)$decoded; // jwt valid data in array + + if (!isset($decoded_array['jti'])) return false; + + // Check if user lock access ip ? + if (isset($decoded_array['secure_login_ip'])) { + $now_ip_crc = sprintf('%08x', crc32(app()->request->getClientIp())); + if (strcasecmp($decoded_array['secure_login_ip'], $now_ip_crc) !== 0) return false; + } + + // Check if user want secure access but his environment is not secure + if (isset($decoded_array['ssl']) && $decoded_array['ssl'] && // User want secure access + !app()->request->isSecure() // User requests is not secure + // TODO our site support ssl feature + ) { + app()->response->redirect(str_replace('http://', 'https://', app()->request->fullUrl())); + app()->response->setHeader('Strict-Transport-Security', 'max-age=1296000; includeSubDomains'); + } + + // Verity $jti is force expired or not ? + $jti = $decoded_array['jti']; + if ($jti !== app()->session->get('jti')) { // Not Match Session record + if (false === app()->redis->zScore(Constant::validUserSessionZset, $jti)) { // Not Record in valid cache + // if this $jti not in valid cache , then check invalid cache + if (app()->redis->zScore(Constant::invalidUserSessionZset, $jti) !== false) { + return false; // This $jti has been marked as invalid + } else { // Invalid cache still not hit, then check $jti from database to avoid lost + $exist_jti = app()->pdo->createCommand('SELECT `id` FROM `user_session_log` WHERE `sid` = :sid AND `expired` = 0 LIMIT 1;')->bindParams([ + 'sid' => $jti + ])->queryScalar(); + + if (false === $exist_jti) { // Absolutely This $jti is not exist or expired + app()->redis->zAdd(Constant::invalidUserSessionZset, $timenow + 86400, $jti); + return false; + } + } + + app()->redis->zAdd(Constant::validUserSessionZset, $decoded_array['exp'] ?? $timenow + 43200, $jti); // Store in valid cache + } + app()->session->set('jti', $jti); // Store the $jti value in session so we can visit $jti in other place + } + + return $decoded_array['user_id'] ?? false; } - protected function loadCurUserFromPasskey() + protected function loadCurUserIdFromPasskey() { $passkey = app()->request->get('passkey'); $user_id = app()->redis->zScore(Constant::mapUserPasskeyToId, $passkey); @@ -225,11 +282,11 @@ public static function ruleCategory(): array public static function CategoryDetail($cat_id): array { - return static::getStaticCacheValue('torrent_category_' . $cat_id ,function () use ($cat_id) { + return static::getStaticCacheValue('torrent_category_' . $cat_id, function () use ($cat_id) { return app()->pdo->createCommand('SELECT * FROM `categories` WHERE id= :cid LIMIT 1;')->bindParams([ 'cid' => $cat_id ])->queryOne(); - },86400); + }, 86400); } public static function ruleCanUsedCategory(): array @@ -241,7 +298,7 @@ public static function ruleCanUsedCategory(): array public static function ruleQuality($quality): array { - if (!in_array($quality, array_keys(self::getQualityTableList()))) throw new \RuntimeException('Unregister quality : ' . $quality); + if (!in_array($quality, array_keys(self::getQualityTableList()))) throw new RuntimeException('Unregister quality : ' . $quality); return static::getStaticCacheValue('enabled_quality_' . $quality, function () use ($quality) { return app()->pdo->createCommand("SELECT * FROM `quality_$quality` WHERE `id` > 0 AND `enabled` = 1 ORDER BY `sort_index`,`id`")->queryAll(); }, 86400); diff --git a/apps/controllers/AuthController.php b/apps/controllers/AuthController.php index 5f1263f..ce40d14 100644 --- a/apps/controllers/AuthController.php +++ b/apps/controllers/AuthController.php @@ -98,33 +98,26 @@ public function actionLogin() if (app()->request->isPost()) { $login = new Auth\UserLoginForm(); - $login->setData(app()->request->post()); - $success = $login->validate(); - - if (!$success) { + if (false === $success = $login->validate()) { $login->LoginFail(); return $this->render('auth/login', [ - "username" => $login->username, - "error_msg" => $login->getError(), + 'username' => $login->username, + 'error_msg' => $login->getError(), 'left_attempts' => $left_attempts ]); } else { - $success = $login->createUserSession(); - if ($success === true) { - $login->updateUserLoginInfo(); - - $return_to = app()->session->pop('login_return_to') ?? '/index'; - if (!app()->request->isSecure() && $login->ssl === 'yes') { // Upgrade the scheme with full url - $return_to = 'https://' . app()->request->header('host') . $return_to; - } + $login->flush(); - return app()->response->redirect($return_to); - } else { - return $this->render('action_fail', [ - 'title' => 'Login Failed', - 'msg' => $success - ]); + $return_to = app()->session->pop('login_return_to') ?? '/index'; + if ($login->ssl === 'yes' // User want secure access + && !app()->request->isSecure() // User requests is not secure + // && true // TODO our site support ssl feature + ) { // Upgrade the scheme with full url + $return_to = str_replace('http://', 'https://', $return_to); + app()->response->setHeader('Strict-Transport-Security', 'max-age=1296000; includeSubDomains'); } + + return app()->response->redirect($return_to); } } else { return $this->render('auth/login', ['left_attempts' => $left_attempts]); @@ -133,7 +126,7 @@ public function actionLogin() public function actionLogout() { - // TODO add CSRF protect + // TODO add CSRF protect and Logout Form app()->site->getCurUser()->deleteUserThisSession(); return app()->response->redirect('/auth/login'); } diff --git a/apps/libraries/Constant.php b/apps/libraries/Constant.php index b896b14..df59903 100644 --- a/apps/libraries/Constant.php +++ b/apps/libraries/Constant.php @@ -14,7 +14,6 @@ class Constant const cookie_name = 'rid'; const mapUsernameToId = 'Map:user_username_to_user_id:hash'; - const mapUserSessionToId = 'Map:user_session_to_user_id:zset'; const mapUserPasskeyToId = 'Map:user_passkey_to_user_id:zset'; // invalid Zset @@ -22,6 +21,9 @@ class Constant const invalidUserSessionZset = 'Session:invalid_user_session:zset'; const invalidUserPasskeyZset = 'Tracker:invalid_user_passkey:zset'; + // valid Zset + const validUserSessionZset = 'Session:valid_user_session:zset'; + // Tracker Use const trackerInvalidInfoHashZset = 'Tracker:invalid_torrent_info_hash:zset'; const trackerAllowedClientList = 'Tracker:allowed_client_list:string'; diff --git a/apps/middleware/AuthByCookiesMiddleware.php b/apps/middleware/AuthByCookiesMiddleware.php index 2014f3e..c8b0a31 100644 --- a/apps/middleware/AuthByCookiesMiddleware.php +++ b/apps/middleware/AuthByCookiesMiddleware.php @@ -31,25 +31,10 @@ public function handle($callable, \Closure $next) } if (false === $curuser) { - $query = app()->request->server('query_string'); - $to = app()->request->server('path_info') . (strlen($query) > 0 ? '?' . $query : ''); - app()->session->set('login_return_to', $to); + app()->cookie->delete(Constant::cookie_name); // Delete exist cookies + app()->session->set('login_return_to', app()->request->fullUrl()); // Store the url which visitor want to hit return app()->response->redirect('/auth/login'); } else { - /** - * Check if session is locked with IP - */ - $userSessionId = app()->request->cookie(Constant::cookie_name); - if (substr($userSessionId, 0, 1) === '1') { - $record_ip_crc = substr($userSessionId, 2, 8); - $this_ip_crc = sprintf('%08x', crc32($now_ip)); - - if (strcasecmp($record_ip_crc, $this_ip_crc) !== 0) { // The Ip isn't matched - app()->cookie->delete(Constant::cookie_name); - return app()->response->redirect('/auth/login'); - } - } - /** Check User Permission to this route * * When user visit - /admin -> Controller : \apps\controllers\AdminController Action: actionIndex diff --git a/apps/models/User.php b/apps/models/User.php index 64de767..f09bfb9 100644 --- a/apps/models/User.php +++ b/apps/models/User.php @@ -68,7 +68,7 @@ class User ]; // User Status - public const STATUS_BANNED = 'banned'; + public const STATUS_DISABLED = 'disabled'; public const STATUS_PENDING = 'pending'; public const STATUS_PARKED = 'parked'; public const STATUS_CONFIRMED = 'confirmed'; @@ -137,6 +137,14 @@ public function getBonusOther() return $this->bonus_other; } + /** + * @return mixed + */ + public function getStatus() + { + return $this->status; + } + protected function getCacheNameSpace(): string { return Constant::userContent($this->id); @@ -530,12 +538,11 @@ public function isPrivilege($require_class) } public function getSessionId() { - return app()->request->cookie(Constant::cookie_name); + return app()->session->get('jti'); } - public function deleteUserThisSession() - { - $user_session_id = app()->request->cookie(Constant::cookie_name); + public function deleteUserThisSession () { // FIXME + $user_session_id = app()->session->get('jti'); app()->pdo->createCommand('UPDATE `user_session_log` SET `expired` = 1 WHERE sid = :sid')->bindParams([ 'sid' => $user_session_id ])->execute(); diff --git a/apps/models/form/Auth/UserLoginForm.php b/apps/models/form/Auth/UserLoginForm.php index 3bc14a7..178cf87 100644 --- a/apps/models/form/Auth/UserLoginForm.php +++ b/apps/models/form/Auth/UserLoginForm.php @@ -11,6 +11,7 @@ use apps\libraries\Constant; use apps\models\User; +use Firebase\JWT\JWT; use Rid\Helpers\StringHelper; use Rid\Validators\CaptchaTrait; use Rid\Validators\Validator; @@ -43,10 +44,10 @@ class UserLoginForm extends Validator private $cookieSecure = false; // Notice : Only change this value when you first run !!!! private $cookieHttpOnly = true; - public function __construct(array $config = []) - { - parent::__construct($config); - } + private $jwt_payload; + + protected $_autoload_data = true; + protected $_autoload_data_from = ['post']; public static function inputRules() { @@ -65,13 +66,24 @@ public static function inputRules() public static function callbackRules() { - return ['validateCaptcha', 'loadUserFromPdo', 'isMaxLoginIpReached']; + return ['validateCaptcha', 'isMaxLoginIpReached', 'loadUserFromPdo', 'isMaxUserSessionsReached']; + } + + /** @noinspection PhpUnused */ + protected function isMaxLoginIpReached() // FIXME may use Trait + { + $test_count = app()->redis->hGet('SITE:fail_login_ip_count', app()->request->getClientIp()) ?: 0; + if ($test_count > config('security.max_login_attempts')) { + $this->buildCallbackFailMsg('Login Attempts', 'User Max Login Attempts Archived.'); + return; + } } + /** @noinspection PhpUnused */ protected function loadUserFromPdo() { - $this->self = app()->pdo->createCommand("SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1")->bindParams([ - "uname" => $this->getData('username'), "email" => $this->getData('username'), + $this->self = app()->pdo->createCommand('SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1')->bindParams([ + 'uname' => $this->getData('username'), 'email' => $this->getData('username'), ])->queryOne(); if (false === $this->self) { // User is not exist @@ -101,18 +113,21 @@ protected function loadUserFromPdo() } // User 's status is banned or pending~ - if (in_array($this->self['status'], [User::STATUS_BANNED, User::STATUS_PENDING])) { - $this->buildCallbackFailMsg('Account', 'User account is not confirmed.'); + if (in_array($this->self['status'], [User::STATUS_DISABLED, User::STATUS_PENDING])) { + $this->buildCallbackFailMsg('Account', 'User account is disabled or may not confirmed.'); return; } } - protected function isMaxLoginIpReached() + /** @noinspection PhpUnused */ + protected function isMaxUserSessionsReached() { - $test_count = app()->redis->hGet('SITE:fail_login_ip_count', app()->request->getClientIp()) ?: 0; - if ($test_count > config('security.max_login_attempts')) { - $this->buildCallbackFailMsg('Login Attempts', 'User Max Login Attempts Archived.'); - return; + $exist_session_count = app()->pdo->createCommand('SELECT COUNT(`id`) FROM `user_session_log` WHERE uid = :uid AND expired = 0')->bindParams([ + 'uid' => $this->self['id'] + ])->queryScalar(); + + if ($exist_session_count >= config('base.max_per_user_session')) { + $this->buildCallbackFailMsg('max_per_user_session', 'Reach the limit of Max User Session.'); } } @@ -122,69 +137,71 @@ public function LoginFail() app()->redis->hIncrBy('SITE:fail_login_ip_count', app()->request->getClientIp(), 1); } - public function createUserSession() + public function flush() { - $userId = $this->self['id']; - - $exist_session_count = app()->redis->zCount(Constant::mapUserSessionToId, $userId, $userId); - if ($exist_session_count < config('base.max_per_user_session')) { - /** - * SessionId Format: - * /^(?P[01])_(?P[a-z0-9]{8})_\w+$/ - * The first character of sessionId is the Flag of secure login, - * if secure login, The second param is the sprintf('%08x',crc32($id)) - * else, Another random string with length 8 - * The prefix of sessionId is in lowercase - * - */ - if ($this->securelogin === 'yes') { - $sid_prefix = '1_' . sprintf('%08x', crc32(app()->request->getClientIp())) . '_'; - } else { - $sid_prefix = '0_' . StringHelper::getRandomString(8) . '_'; - } - $sid_prefix = strtolower($sid_prefix); - do { // To make sure this session is unique ! - $userSessionId = $sid_prefix . StringHelper::getRandomString($this->sessionLength - strlen($sid_prefix)); - - $count = app()->pdo->createCommand('SELECT COUNT(`id`) FROM `user_session_log` WHERE sid = :sid')->bindParams([ - 'sid' => $userSessionId - ])->queryScalar(); - } while ($count != 0); - - // store user login information , ( for example `login ip`,`user_agent`,`last activity at` ) - app()->pdo->createCommand('INSERT INTO `user_session_log`(`uid`, `sid`, `login_ip`, `user_agent` ,`login_at`, `last_access_at`) ' . - 'VALUES (:uid,:sid,INET6_ATON(:login_ip),:ua,NOW(), NOW())')->bindParams([ - 'uid' => $userId, 'sid' => $userSessionId, - 'login_ip' => app()->request->getClientIp(), - 'ua' => app()->request->header('user-agent') - ])->execute(); - - // Add this session id in Redis Cache - app()->redis->zAdd(Constant::mapUserSessionToId, $userId, $userSessionId); - - // Set User Cookie - $cookieExpire = $this->cookieExpires; - if ($this->logout === 'yes') { - $cookieExpire = time() + 15 * 60; - app()->redis->zAdd('Site:Sessions:to_expire', $cookieExpire, $userSessionId); - } + $this->createUserSession(); + $this->updateUserLoginInfo(); + $this->noticeUser(); + } - app()->response->setCookie(Constant::cookie_name, $userSessionId, $cookieExpire, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, $this->cookieHttpOnly); - return true; - } else { - return 'Reach the limit of Max User Session.'; + /** + * Use jwt ways to generate user identity + */ + private function createUserSession() + { + $timenow = time(); + $key = env('APP_SECRET_KEY'); + $login_ip = app()->request->getClientIp(); + + do { // Generate unique JWT ID + $jti = StringHelper::getRandomString(64); + $count = app()->pdo->createCommand('SELECT COUNT(`id`) FROM `user_session_log` WHERE sid = :sid;')->bindParams([ + 'sid' => $jti + ])->queryScalar(); + } while ($count != 0); + + // Official Payload key + $payload = [ + 'iss' => config('base.site_url'), + 'iat' => $timenow, + 'jti' => $jti, + ]; + + $cookieExpire = $this->cookieExpires; + if ($this->logout === 'yes') { + $payload['exp'] = $cookieExpire = $timenow + 15 * 60; // 15 minutes } + + // Custom Payload key + $payload['user_id'] = $this->self['id']; // Store User Id so we can quick load their information + if ($this->securelogin === 'yes') $payload['secure_login_ip'] = sprintf('%08x', crc32($login_ip)); // Store User Login IP ( in CRC32 format ) + if ($this->ssl) $payload['ssl'] = true; // FIXME Check if site support this feature , Store User want full ssl protect + + // Generate JWT content + $this->jwt_payload = $payload; + $jwt = JWT::encode($payload, $key); + + // Sent JWT content AS Cookie + app()->response->setCookie(Constant::cookie_name, $jwt, $cookieExpire, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, $this->cookieHttpOnly); } - public function updateUserLoginInfo() + private function updateUserLoginInfo() { + // FIXME change store data, Store User Login Information in database + app()->pdo->createCommand('INSERT INTO `user_session_log`(`uid`, `sid`, `login_ip`, `user_agent` ,`login_at`, `last_access_at`) ' . + 'VALUES (:uid, :sid, INET6_ATON(:login_ip), :ua, NOW(), NOW())')->bindParams([ + 'uid' => $this->jwt_payload['user_id'], 'sid' => $this->jwt_payload['jti'], + 'login_ip' => app()->request->getClientIp(), + 'ua' => app()->request->header('user-agent') + ])->execute(); + // TODO or move to task queue... app()->pdo->createCommand("UPDATE `users` SET `last_login_at` = NOW() , `last_login_ip` = INET6_ATON(:ip) WHERE `id` = :id")->bindParams([ "ip" => app()->request->getClientIp(), "id" => $this->self["id"] ])->execute(); } - public function noticeUser() + private function noticeUser() { // TODO send email to tail user login } diff --git a/apps/models/form/Traits/actionRateLimitCheckTrait.php b/apps/models/form/Traits/actionRateLimitCheckTrait.php index a3e627d..5c35e2c 100644 --- a/apps/models/form/Traits/actionRateLimitCheckTrait.php +++ b/apps/models/form/Traits/actionRateLimitCheckTrait.php @@ -15,36 +15,50 @@ trait actionRateLimitCheckTrait { - protected function getRateLimitRules(): array + protected static function getRateLimitRules(): array { + /** @noinspection PhpUnusedLocalVariableInspection */ + $pool = 'user_' . app()->site->getCurUser()->getId(); return [ - /* ['key' => 'dl_60', 'period' => 60, 'max' => 5] */ + /* ['key' => 'dl_60', 'period' => 60, 'max' => 5, 'pool' => $pool] */ ]; } - private function isRateLimitHit($action_key, $period, $max_count, $pool = null): bool + private function isRateLimitHit($limit_status) { - $pool = $pool ?? 'user_' . app()->site->getCurUser()->getId(); - $key = Constant::rateLimitPool($pool, $action_key); - $now_ts = time() * 1000; + $pool = $limit_status['pool'] ?? 'default_'; + $action = $limit_status['key'] ?? 'default'; + $key = Constant::rateLimitPool($pool, $action); + + $period = $limit_status['period'] ?? 60; + + $now_ts = time(); $pipe = app()->redis->multi(Redis::PIPELINE); $pipe->zAdd($key, $now_ts, $now_ts); - $pipe->zRemRangeByScore($key, 0, $now_ts - $period * 1000); + $pipe->zRemRangeByScore($key, 0, $now_ts - $period); $pipe->zCard($key); $pipe->expire($key, $period + 1); $replies = $pipe->exec(); $count = $replies[2]; - return $count <= $max_count; + return [$count <= ($limit_status['max'] ?? 10), $count]; + } + + protected function hookRateLimitCheckFailed($limit_status, $count) + { } + /** @noinspection PhpUnused */ protected function rateLimitCheck() { - if (empty($this->getRateLimitRules())) return; // It seems we don't need rate limit + if (empty($this::getRateLimitRules())) return; // It seems we don't need rate limit + + foreach ($this::getRateLimitRules() as $limit_status) { + list($vary, $count) = $this->isRateLimitHit($limit_status); - foreach ($this->getRateLimitRules() as $limit_status) { - if (!$this->isRateLimitHit($limit_status['key'], $limit_status['period'], $limit_status['max'])) { + if (false === $vary) { + $this->hookRateLimitCheckFailed($limit_status, $count); $this->buildCallbackFailMsg('rate', 'rate limit hit'); return; } diff --git a/apps/process/CronTabProcess.php b/apps/process/CronTabProcess.php index 30b66a5..5a8b770 100644 --- a/apps/process/CronTabProcess.php +++ b/apps/process/CronTabProcess.php @@ -90,6 +90,7 @@ protected function clean_expired_zset_cache() { [Constant::trackerInvalidInfoHashZset, 'Success Clean %s invalid info_hash.'], // Valid Zset + [Constant::validUserSessionZset,'Success Clean $s valid user session'], [Constant::trackerValidClientZset, 'Success Clean %s valid bittorrent client.'], [Constant::trackerValidPeerZset, 'Success Clean %s valid peers.'] ]; diff --git a/composer.json b/composer.json index e40476b..ca47856 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "phpmailer/phpmailer": "^6.0", "siriusphp/validation": "^2.2", "soundasleep/html2text": "^1.1", - "robthree/twofactorauth": "^1.6" + "robthree/twofactorauth": "^1.6", + "firebase/php-jwt": "^5.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index ff58aaf..fc494da 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,60 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "595e1892c5feee88300a298a97cb15e5", + "content-hash": "8610af97db6a716b46a2b048c9e7f552", "packages": [ + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "role": "Developer", + "email": "neuman+pear@twilio.com" + }, + { + "name": "Anant Narayanan", + "role": "Developer", + "email": "anant@php.net" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2017-06-27T22:17:23+00:00" + }, { "name": "league/plates", "version": "3.3.0", @@ -474,7 +526,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.2.0", + "php": ">=7.3.0", "ext-gd": "*", "ext-pdo": "*", "ext-json": "*", diff --git a/migration/ridpt.sql b/migration/ridpt.sql index 4eaf643..1728035 100644 --- a/migration/ridpt.sql +++ b/migration/ridpt.sql @@ -3,7 +3,7 @@ -- https://www.phpmyadmin.net/ -- -- Host: 127.0.0.1 --- Generation Time: Aug 10, 2019 at 11:21 AM +-- Generation Time: Aug 10, 2019 at 10:24 PM -- Server version: 8.0.16 -- PHP Version: 7.3.7 @@ -1184,7 +1184,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `password` varchar(60) NOT NULL, `opt` varchar(40) DEFAULT NULL, `email` varchar(80) NOT NULL, - `status` enum('banned','pending','parked','confirmed') NOT NULL DEFAULT 'pending', + `status` enum('disabled','pending','parked','confirmed') NOT NULL DEFAULT 'pending', `class` smallint(6) UNSIGNED NOT NULL DEFAULT '1', `passkey` varchar(32) NOT NULL, `invite_by` int(11) UNSIGNED NOT NULL DEFAULT '0',