118 lines
3.5 KiB
PHP
118 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use App\Libraries\RateLimiterService;
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
use Config\Database;
|
|
use Config\RateLimiter as RateLimiterConfig;
|
|
|
|
/**
|
|
* Align rate_limit_blocks with timed cache TTL: purge stale cache keys and delete DB rows.
|
|
*
|
|
* Schedule (example every 10 minutes):
|
|
* *\/10 * * * * cd /path/to/project && php spark rate-limit:reconcile-blocks
|
|
*/
|
|
class RateLimitBlocksReconcile extends BaseCommand
|
|
{
|
|
protected $group = 'Rate limit';
|
|
|
|
protected $name = 'rate-limit:reconcile-blocks';
|
|
|
|
protected $description = 'For active rows whose block TTL has passed: purge cache keys, then delete the DB row.';
|
|
|
|
protected $usage = 'rate-limit:reconcile-blocks [--dry-run]';
|
|
|
|
/** @var array<string, string> */
|
|
protected $options = [
|
|
'--dry-run' => 'Show which rows would be purged without deleting cache or DB.',
|
|
];
|
|
|
|
public function run(array $params)
|
|
{
|
|
$dryRun = CLI::getOption('dry-run') !== null;
|
|
|
|
$db = Database::connect();
|
|
|
|
if (! $db->tableExists('rate_limit_blocks')) {
|
|
CLI::write('Table rate_limit_blocks does not exist. Nothing to do.', 'yellow');
|
|
|
|
return;
|
|
}
|
|
|
|
/** @var RateLimiterConfig $rl */
|
|
$rl = config('RateLimiter');
|
|
$limiter = new RateLimiterService();
|
|
|
|
$rows = $db->table('rate_limit_blocks')
|
|
->where('status', 'active')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$count = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
$duration = $this->blockSecondsForRow($row, $rl);
|
|
if ($duration <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$blockedAt = strtotime((string) $row['blocked_at']);
|
|
if ($blockedAt === false) {
|
|
CLI::write('Skipping id ' . $row['id'] . ': invalid blocked_at.', 'red');
|
|
continue;
|
|
}
|
|
|
|
if (time() < $blockedAt + $duration) {
|
|
continue;
|
|
}
|
|
|
|
$id = (int) $row['id'];
|
|
$cacheId = (string) $row['cache_identifier'];
|
|
$blockType = (string) $row['block_type'];
|
|
|
|
CLI::write(
|
|
($dryRun ? '[dry-run] Would reconcile ' : 'Reconciling ')
|
|
. "{$blockType} id={$id} level={$row['block_level']} display=" . $row['display_identifier'],
|
|
'cyan'
|
|
);
|
|
|
|
if (! $dryRun) {
|
|
if ($blockType === 'ip') {
|
|
$limiter->purgeIpBlockCaches($cacheId);
|
|
} elseif ($blockType === 'user') {
|
|
$limiter->purgeUserBlockCaches($cacheId);
|
|
}
|
|
|
|
$db->table('rate_limit_blocks')->delete(['id' => $id], 1);
|
|
}
|
|
|
|
$count++;
|
|
}
|
|
|
|
CLI::write(
|
|
$dryRun
|
|
? "Dry run complete. {$count} row(s) would be purged and deleted."
|
|
: "Done. Reconciled {$count} expired row(s).",
|
|
'yellow'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
protected function blockSecondsForRow(array $row, RateLimiterConfig $cfg): int
|
|
{
|
|
$blockCfg = ($row['block_type'] ?? '') === 'ip' ? $cfg->ipBlock : $cfg->userBlock;
|
|
$level = (string) ($row['block_level'] ?? '');
|
|
|
|
return match ($level) {
|
|
'soft' => (int) $blockCfg['soft_duration'],
|
|
'medium' => (int) $blockCfg['medium_duration'],
|
|
'hard' => (int) $blockCfg['hard_duration'],
|
|
default => 0,
|
|
};
|
|
}
|
|
}
|