FEAT_API_RATE_LIMIT_COND
This commit is contained in:
parent
7c0736e347
commit
c520b87af5
@ -454,7 +454,7 @@ $routes->group("/api", ["filter" => [ 'ratelimit' , 'authJWT' ] ], function ($r
|
||||
// $routes->post("employeeRest/createOrUpdateEmployeePolicySiAmount", "EmployeeRestController::createOrUpdateEmployeePolicySiAmount");
|
||||
// $routes->post("employeeRest/calculatePremium", "EmployeeRestController::calculatePremium");
|
||||
// $routes->post("updateMpin", "RestAuthenticationController::updateMpin");
|
||||
$routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature' , 'authJWT' ] ], function ($routes) {
|
||||
$routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'appSignature' , 'authJWT','JwtApiRateLimitFilter' ] ], function ($routes) {
|
||||
|
||||
$routes->post('logout', 'RestAuthenticationController::logout');
|
||||
|
||||
@ -507,7 +507,7 @@ $routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'rate
|
||||
|
||||
});
|
||||
|
||||
$routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], function ($routes) {
|
||||
$routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) {
|
||||
|
||||
//Employee login api's
|
||||
$routes->post("verifyEmployeeNumber", "RestAuthenticationController::verifyEmployeeWithMobileNumber");
|
||||
|
||||
@ -8,6 +8,7 @@ use App\Libraries\MyLogger;
|
||||
use App\Libraries\GmailAPI;
|
||||
use App\Libraries\MyGoogleDrive;
|
||||
use App\Libraries\DataServiceSqlite;
|
||||
use App\Libraries\RateLimiterService;
|
||||
use App\Controllers\Home;
|
||||
|
||||
/**
|
||||
@ -80,5 +81,14 @@ class Services extends BaseService
|
||||
|
||||
return new MyGoogleDrive();
|
||||
}
|
||||
|
||||
public static function limiter($getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('limiter');
|
||||
}
|
||||
|
||||
return new RateLimiterService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -142,6 +142,7 @@ class RestAuthenticationController extends AdminController
|
||||
log_message('error', ' ');
|
||||
log_message('error', '************************ PRE END ********************************');
|
||||
$result = ['user_verification' => false , 'message' => "User not found"];
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200);
|
||||
}
|
||||
|
||||
@ -320,6 +321,7 @@ class RestAuthenticationController extends AdminController
|
||||
}
|
||||
|
||||
$result = ['user_verification' => false , 'message' => "User not found"];
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200);
|
||||
}
|
||||
|
||||
@ -445,10 +447,12 @@ class RestAuthenticationController extends AdminController
|
||||
$otp = isset($this->request->getJSON()->otp) ? $this->request->getJSON()->otp : null;
|
||||
|
||||
if(empty($otp)){
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'OTP is required','code' => 400,'message' => 'OTP is required'], 200);
|
||||
}
|
||||
|
||||
if (empty($mobile_number) && empty($email_id)) {
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'failed','code' => 400,'message' => 'Mobile number or Email ID is required'], 200);
|
||||
}
|
||||
|
||||
@ -472,9 +476,11 @@ class RestAuthenticationController extends AdminController
|
||||
// Call the third-party API function
|
||||
$apiResponse = $this->callThirdPartyAPI($retailApiParams, 'getVerifiedRetailUserData');
|
||||
// print_r($apiResponse); die;
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => [], 'post_enrollment' => json_decode($apiResponse, true)], 200);
|
||||
}
|
||||
|
||||
recordRateLimitFailure();
|
||||
return $this->respond(['status' => 'Invalid OTP','code' => 404,'data' => "", 'message' => "Invalid OTP"],200);
|
||||
}
|
||||
|
||||
|
||||
@ -38,8 +38,9 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// $this->limiter->unblockUser('9698262411');die;
|
||||
$fingerprint = generateFingerprint();
|
||||
|
||||
// echo $fingerprint;die;
|
||||
// 1. IP-level check
|
||||
$ipResult = $this->limiter->checkIp($fingerprint, 'authApi');
|
||||
if ($ipResult) {
|
||||
@ -47,7 +48,8 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
}
|
||||
|
||||
// 2. User-level block check (identity may not be present yet on first hit)
|
||||
$identity = $this->resolveIdentity($request);
|
||||
$identity = resolveIdentity($request);
|
||||
// echo $identity;die;
|
||||
if ($identity) {
|
||||
$userResult = $this->limiter->checkUser($identity);
|
||||
if ($userResult) {
|
||||
@ -70,15 +72,33 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
|
||||
// Routes where rate-limit failure should be recorded
|
||||
// $allowedRoutes = [
|
||||
// 'employeeRest/verifyEmployeeNumber',
|
||||
// 'employeeRest/getVerifiedUserData',
|
||||
// 'employeeRest/verifyEmployeeEmailId',
|
||||
// 'employeeRest/verifyHrWithMobileNumber',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/getVerifiedHrData',
|
||||
// ];
|
||||
|
||||
// $currentPath = service('request')->getPath();
|
||||
// if (!in_array($currentPath, $allowedRoutes)) {
|
||||
// return; // Don't record failures for unrelated routes
|
||||
// }
|
||||
|
||||
// Only act on failed responses (4xx from auth failures)
|
||||
$statusCode = $response->getStatusCode();
|
||||
// print_r($response);die();
|
||||
if ($statusCode < 400 || $statusCode === 429 || $statusCode === 403 || $statusCode === 451) {
|
||||
return; // 2xx/3xx = success; 429/403/451 already handled
|
||||
}
|
||||
|
||||
$fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint();
|
||||
$identity = $request->getGlobal('rateLimitIdentity')
|
||||
?? $this->resolveIdentity($request);
|
||||
$fingerprint = $request->getVar('rateLimitFingerprint') ?? generateFingerprint();
|
||||
$identity = $request->getVar('rateLimitIdentity')
|
||||
?? resolveIdentity($request);
|
||||
|
||||
// Record failure at IP level
|
||||
$this->limiter->recordIpFailure($fingerprint);
|
||||
@ -89,37 +109,6 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// HELPERS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract identity from POST body or GET params.
|
||||
* Looks for 'email' or 'mobile_number'.
|
||||
*/
|
||||
protected function resolveIdentity(RequestInterface $request): ?string
|
||||
{
|
||||
// Try POST body first
|
||||
$email = $request->getPost('email');
|
||||
$mobile = $request->getPost('mobile_number');
|
||||
|
||||
// Fallback to GET params
|
||||
if (! $email && ! $mobile) {
|
||||
$email = $request->getGet('email');
|
||||
$mobile = $request->getGet('mobile_number');
|
||||
}
|
||||
|
||||
if ($email) {
|
||||
return strtolower(trim($email));
|
||||
}
|
||||
|
||||
if ($mobile) {
|
||||
return trim($mobile);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return a JSON response for blocked/throttled requests.
|
||||
*/
|
||||
|
||||
@ -40,7 +40,7 @@ class JwtApiRateLimitFilter implements FilterInterface
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
$fingerprint = generateFingerprint();
|
||||
|
||||
// echo $fingerprint;die;
|
||||
// 1. IP-level throttle + block check
|
||||
$ipResult = $this->limiter->checkIp($fingerprint, 'jwtApi');
|
||||
if ($ipResult) {
|
||||
@ -50,7 +50,8 @@ class JwtApiRateLimitFilter implements FilterInterface
|
||||
// 2. Resolve user identity from JWT
|
||||
// Uses your existing JWT helper functions.
|
||||
// If neither returns a value, fall back to IP-only limiting.
|
||||
$identity = $this->resolveIdentityFromJwt();
|
||||
// $identity = $this->resolveIdentityFromJwt();
|
||||
$identity = '';
|
||||
|
||||
if ($identity) {
|
||||
// User-level throttle (request count based for JWT routes)
|
||||
@ -84,7 +85,8 @@ class JwtApiRateLimitFilter implements FilterInterface
|
||||
}
|
||||
|
||||
$fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint();
|
||||
$identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt();
|
||||
// $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt();
|
||||
$identity = $request->getGlobal('rateLimitIdentity') ?? '';
|
||||
|
||||
$this->limiter->recordIpFailure($fingerprint);
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ class HttpRequestHelper
|
||||
|
||||
$data = [
|
||||
'ip' => $request->getIPAddress(),
|
||||
'real_ip' => getRealClientIP(),
|
||||
'platform' => $platform,
|
||||
'browser' => $browser,
|
||||
'method' => strtoupper($request->getMethod()),
|
||||
|
||||
@ -773,19 +773,104 @@ function getRealClientIP()
|
||||
return $request->getIPAddress();
|
||||
}
|
||||
|
||||
function generateFingerprint()
|
||||
function generateFingerprint(): string
|
||||
{
|
||||
$request = service('request');
|
||||
|
||||
$ua = $request->getUserAgent()->getAgentString();
|
||||
// echo $ua;
|
||||
// die;
|
||||
$ip = getRealClientIP();
|
||||
// echo $ip;die();
|
||||
// Use only subnet (first 3 blocks) to tolerate IP change
|
||||
$ipParts = explode('.', $ip);
|
||||
$ipSubnet = $ipParts[0] . '.' . $ipParts[1] . '.' . $ipParts[2];
|
||||
|
||||
// $secret = env('app.sessionFingerprintSalt');
|
||||
// Normalize localhost
|
||||
if ($ip === '127.0.0.1' || $ip === '::1') {
|
||||
$ipGroup = 'localhost';
|
||||
}
|
||||
// IPv4 handling
|
||||
elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$parts = explode('.', $ip);
|
||||
// Use /24 subnet (first 3 octets)
|
||||
$ipGroup = $parts[0] . '.' . $parts[1] . '.' . $parts[2];
|
||||
}
|
||||
// IPv6 handling
|
||||
elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
// Use first 4 blocks of IPv6 (rough /64 grouping)
|
||||
$blocks = explode(':', $ip);
|
||||
$ipGroup = implode(':', array_slice($blocks, 0, 4));
|
||||
}
|
||||
// Fallback
|
||||
else {
|
||||
$ipGroup = 'unknown';
|
||||
}
|
||||
|
||||
// return hash('sha256', $ua . '|' . $ipSubnet . '|' . $secret);
|
||||
return hash('sha256', $ua . '|' . $ipSubnet );
|
||||
}
|
||||
// return $ua . '_' . $ipGroup;
|
||||
return hash('sha256', $ua . '|' . $ipGroup);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract identity from POST body or GET params.
|
||||
* Looks for 'email' or 'mobile_number'.
|
||||
*/
|
||||
function resolveIdentity($request): ?string
|
||||
{
|
||||
// Try POST body first
|
||||
$email = $request->getPost('email');
|
||||
// print_r($email);die;
|
||||
$mobile = $request->getPost('mobile_number');
|
||||
|
||||
// Fallback to GET params
|
||||
if (! $email && ! $mobile) {
|
||||
$email = $request->getGet('email');
|
||||
$mobile = $request->getGet('mobile_number');
|
||||
}
|
||||
// Fallback to JSON params
|
||||
if (! $email && ! $mobile) {
|
||||
$req_data = $request->getJSON();
|
||||
// print_r( $req_data);
|
||||
|
||||
$mobile = $req_data->mobile_number ?? null;
|
||||
// return trim($mobile_number);
|
||||
|
||||
$email = $req_data->email ?? null;
|
||||
|
||||
if (!$email)
|
||||
{
|
||||
$email = $req_data->email_id ?? null;
|
||||
}
|
||||
// return trim($email);
|
||||
}
|
||||
|
||||
if ($email) {
|
||||
return strtolower(trim($email));
|
||||
}
|
||||
|
||||
if ($mobile) {
|
||||
return trim($mobile);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function recordRateLimitFailure(string $context = 'authApi'): void
|
||||
{
|
||||
/** @var IncomingRequest $request */
|
||||
$request = \Config\Services::request();
|
||||
|
||||
$limiter = \Config\Services::limiter(); // or your custom limiter service
|
||||
|
||||
$fingerprint = $request->getVar('rateLimitFingerprint')
|
||||
?? generateFingerprint();
|
||||
|
||||
$identity = $request->getVar('rateLimitIdentity')
|
||||
?? resolveIdentity($request);
|
||||
|
||||
// Record IP-level failure
|
||||
$limiter->recordIpFailure($fingerprint);
|
||||
|
||||
// Record user-level failure
|
||||
if (!empty($identity)) {
|
||||
$limiter->recordUserFailure($identity, $context);
|
||||
}
|
||||
}
|
||||
385
app/Libraries/RateLimiterService.php
Normal file
385
app/Libraries/RateLimiterService.php
Normal file
@ -0,0 +1,385 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use Config\RateLimiter as RateLimiterConfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
|
||||
/**
|
||||
* RateLimiterService
|
||||
*
|
||||
* Handles all rate limiting logic:
|
||||
* - IP-level throttle + progressive blocking (soft/medium/hard)
|
||||
* - User-level progressive blocking (soft/medium/hard) by email or mobile_number
|
||||
* - Manual block / unblock helpers callable from anywhere
|
||||
*
|
||||
* Block levels: 'soft' | 'medium' | 'hard'
|
||||
* All blocks are MANUAL UNBLOCK ONLY (no auto-expiry on block state).
|
||||
* Counters and violation counts use cache TTLs; block records do not expire.
|
||||
*/
|
||||
class RateLimiterService
|
||||
{
|
||||
protected RateLimiterConfig $config;
|
||||
protected CacheInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('RateLimiter');
|
||||
$this->cache = \Config\Services::cache();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PUBLIC — IP LEVEL
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check & throttle by fingerprint (IP+UA based).
|
||||
* Returns null on pass, or an array ['level'=>..., 'message'=>...] on block.
|
||||
*/
|
||||
public function checkIp(string $fingerprint, string $routeType = 'jwtApi'): ?array
|
||||
{
|
||||
// echo $fingerprint;die;
|
||||
// 1. Is the IP already blocked?
|
||||
$blockInfo = $this->getIpBlock($fingerprint);
|
||||
// print_rr($blockInfo);die();
|
||||
if ($blockInfo) {
|
||||
// Count hit while blocked → maybe escalate
|
||||
$this->recordIpBlockHit($fingerprint, $blockInfo['level']);
|
||||
return $this->blockedResponse('ip', $blockInfo['level']);
|
||||
}
|
||||
|
||||
// 2. Throttle check
|
||||
$cfg = $this->config->ipBlock;
|
||||
$countKey = $this->config->cacheKeys['ip_count'] . $fingerprint;
|
||||
$count = (int) ($this->cache->get($countKey) ?? 0);
|
||||
|
||||
if ($count === 0) {
|
||||
$this->cache->save($countKey, 1, $cfg['window']);
|
||||
} else {
|
||||
$this->cache->save($countKey, $count + 1, $cfg['window']);
|
||||
}
|
||||
|
||||
if (($count + 1) > $cfg['limit']) {
|
||||
// Over limit → record violation
|
||||
$violated = $this->incrementIpViolation($fingerprint);
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockIp($fingerprint, 'soft');
|
||||
return $this->blockedResponse('ip', 'soft');
|
||||
}
|
||||
return [
|
||||
'level' => 'throttle',
|
||||
'message' => 'Too many requests. Please slow down.',
|
||||
'status' => $this->config->statusCodes['throttle'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a "bad outcome" for IP (e.g. controller calls this after failed auth).
|
||||
* Same escalation path as throttle violations.
|
||||
*/
|
||||
public function recordIpFailure(string $fingerprint): ?array
|
||||
{
|
||||
$blockInfo = $this->getIpBlock($fingerprint);
|
||||
if ($blockInfo) {
|
||||
$this->recordIpBlockHit($fingerprint, $blockInfo['level']);
|
||||
return $this->blockedResponse('ip', $blockInfo['level']);
|
||||
}
|
||||
|
||||
$violated = $this->incrementIpViolation($fingerprint);
|
||||
$cfg = $this->config->ipBlock;
|
||||
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockIp($fingerprint, 'soft');
|
||||
return $this->blockedResponse('ip', 'soft');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually block an IP at a given level.
|
||||
*/
|
||||
public function blockIp(string $fingerprint, string $level = 'soft'): void
|
||||
{
|
||||
$cfg = $this->config->ipBlock;
|
||||
$blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint;
|
||||
|
||||
$duration = $this->blockDuration($cfg, $level);
|
||||
|
||||
$data = [
|
||||
'level' => $level,
|
||||
'blocked_at' => time(),
|
||||
'fingerprint'=> $fingerprint,
|
||||
];
|
||||
|
||||
// Duration 0 = store for 10 years (permanent until manual unblock)
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually unblock an IP. Clears block, violations, and counters.
|
||||
*/
|
||||
public function unblockIp(string $fingerprint): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$this->cache->delete($keys['ip_block'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_violations'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_count'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_block_hits'] . $fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current IP block info or null if not blocked.
|
||||
*/
|
||||
public function getIpBlock(string $fingerprint): ?array
|
||||
{
|
||||
$blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint;
|
||||
$data = $this->cache->get($blockKey);
|
||||
return $data ?: null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PUBLIC — USER LEVEL
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check if a user (by email or mobile) is blocked.
|
||||
* Returns null on pass, or block response array on block.
|
||||
*/
|
||||
public function checkUser(string $identity): ?array
|
||||
{
|
||||
$blockInfo = $this->getUserBlock($identity);
|
||||
if ($blockInfo) {
|
||||
$this->recordUserBlockHit($identity, $blockInfo['level']);
|
||||
return $this->blockedResponse('user', $blockInfo['level']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed attempt for a user identity.
|
||||
* Called from controller after() or manually after a failed verification.
|
||||
* Handles escalation: free → soft → medium → hard
|
||||
*/
|
||||
public function recordUserFailure(string $identity, string $routeType = 'authApi'): ?array
|
||||
{
|
||||
$blockInfo = $this->getUserBlock($identity);
|
||||
|
||||
if ($blockInfo) {
|
||||
// Already blocked — count hit and maybe escalate
|
||||
$this->recordUserBlockHit($identity, $blockInfo['level']);
|
||||
return $this->blockedResponse('user', $blockInfo['level']);
|
||||
}
|
||||
|
||||
// Not blocked yet — increment violation count
|
||||
$violated = $this->incrementUserViolation($identity, $routeType);
|
||||
$cfg = $this->config->userBlock;
|
||||
$routeCfg = $this->config->{$routeType};
|
||||
|
||||
if ($violated >= $routeCfg['violation_soft']) {
|
||||
$this->blockUser($identity, 'soft');
|
||||
return $this->blockedResponse('user', 'soft');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually block a user identity at a given level.
|
||||
*/
|
||||
public function blockUser(string $identity, string $level = 'soft'): void
|
||||
{
|
||||
$cfg = $this->config->userBlock;
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity);
|
||||
|
||||
$duration = $this->blockDuration($cfg, $level);
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
|
||||
$data = [
|
||||
'level' => $level,
|
||||
'blocked_at' => time(),
|
||||
'identity' => $identity,
|
||||
];
|
||||
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually unblock a user identity. Independent — does NOT touch IP block.
|
||||
*/
|
||||
public function unblockUser(string $identity): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
|
||||
$this->cache->delete($keys['user_block'] . $hashed);
|
||||
$this->cache->delete($keys['user_violations'] . $hashed);
|
||||
$this->cache->delete($keys['user_count'] . $hashed);
|
||||
$this->cache->delete($keys['user_block_hits'] . $hashed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user block info or null if not blocked.
|
||||
*/
|
||||
public function getUserBlock(string $identity): ?array
|
||||
{
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity);
|
||||
$data = $this->cache->get($blockKey);
|
||||
return $data ?: null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// USER THROTTLE (for JWT API routes — request count based)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Throttle check for a known user on JWT routes.
|
||||
* Increments request counter; if over limit records violation.
|
||||
*/
|
||||
public function checkUserThrottle(string $identity, string $routeType = 'jwtApi'): ?array
|
||||
{
|
||||
$blockCheck = $this->checkUser($identity);
|
||||
if ($blockCheck) {
|
||||
return $blockCheck;
|
||||
}
|
||||
|
||||
$cfg = $this->config->{$routeType};
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$countKey = $this->config->cacheKeys['user_count'] . $hashed;
|
||||
$count = (int) ($this->cache->get($countKey) ?? 0);
|
||||
|
||||
if ($count === 0) {
|
||||
$this->cache->save($countKey, 1, $cfg['window']);
|
||||
} else {
|
||||
$this->cache->save($countKey, $count + 1, $cfg['window']);
|
||||
}
|
||||
|
||||
if (($count + 1) > $cfg['limit']) {
|
||||
$violated = $this->incrementUserViolation($identity, $routeType);
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockUser($identity, 'soft');
|
||||
return $this->blockedResponse('user', 'soft');
|
||||
}
|
||||
return [
|
||||
'level' => 'throttle',
|
||||
'message' => 'Too many requests. Please slow down.',
|
||||
'status' => $this->config->statusCodes['throttle'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PRIVATE HELPERS
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Increment IP violation counter and return new count.
|
||||
*/
|
||||
protected function incrementIpViolation(string $fingerprint): int
|
||||
{
|
||||
$key = $this->config->cacheKeys['ip_violations'] . $fingerprint;
|
||||
$count = (int) ($this->cache->get($key) ?? 0) + 1;
|
||||
// Keep violation record for the block window duration
|
||||
$this->cache->save($key, $count, $this->config->ipBlock['window'] * 10);
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a hit while IP is already blocked; escalate if thresholds met.
|
||||
*/
|
||||
protected function recordIpBlockHit(string $fingerprint, string $currentLevel): void
|
||||
{
|
||||
$cfg = $this->config->ipBlock;
|
||||
$hitKey = $this->config->cacheKeys['ip_block_hits'] . $fingerprint;
|
||||
$hits = (int) ($this->cache->get($hitKey) ?? 0) + 1;
|
||||
$this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600);
|
||||
|
||||
if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockIp($fingerprint, 'medium');
|
||||
} elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockIp($fingerprint, 'hard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment user violation counter and return new count.
|
||||
*/
|
||||
protected function incrementUserViolation(string $identity, string $routeType): int
|
||||
{
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$key = $this->config->cacheKeys['user_violations'] . $hashed;
|
||||
$count = (int) ($this->cache->get($key) ?? 0) + 1;
|
||||
$window = $this->config->{$routeType}['window'] ?? 180;
|
||||
$this->cache->save($key, $count, $window * 10);
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a hit while user is already blocked; escalate if thresholds met.
|
||||
*/
|
||||
protected function recordUserBlockHit(string $identity, string $currentLevel): void
|
||||
{
|
||||
$cfg = $this->config->userBlock;
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$hitKey = $this->config->cacheKeys['user_block_hits'] . $hashed;
|
||||
$hits = (int) ($this->cache->get($hitKey) ?? 0) + 1;
|
||||
$this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600);
|
||||
|
||||
if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockUser($identity, 'medium');
|
||||
} elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockUser($identity, 'hard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve block duration from config based on level.
|
||||
*/
|
||||
protected function blockDuration(array $cfg, string $level): int
|
||||
{
|
||||
return match ($level) {
|
||||
'soft' => $cfg['soft_duration'],
|
||||
'medium' => $cfg['medium_duration'],
|
||||
'hard' => $cfg['hard_duration'],
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a standardised blocked response array.
|
||||
*/
|
||||
protected function blockedResponse(string $type, string $level): array
|
||||
{
|
||||
$messages = [
|
||||
'soft' => 'Your access has been temporarily suspended. Please contact support.',
|
||||
'medium' => 'Your access has been restricted due to repeated violations.',
|
||||
'hard' => 'Your access has been permanently blocked. Please contact support.',
|
||||
];
|
||||
|
||||
return [
|
||||
'level' => $level,
|
||||
'type' => $type,
|
||||
'message' => $messages[$level] ?? 'Access denied.',
|
||||
'status' => $this->config->statusCodes[$level],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash user identity (email or mobile) for cache key safety.
|
||||
*/
|
||||
protected function hashIdentity(string $identity): string
|
||||
{
|
||||
// return strtolower(trim($identity));
|
||||
return hash('sha256', strtolower(trim($identity)));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user