|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * Implementazione conservativa di rate limiting per OpenSTAManager. |
| 5 | + * Usa Illuminate\Cache\RateLimiter quando disponibile; in caso contrario |
| 6 | + * applica un fallback file-based compatibile con i parametri configurati. |
| 7 | + * |
| 8 | + * Requisiti (modalità nativa): |
| 9 | + * - illuminate/cache:^10.0 |
| 10 | + * - illuminate/filesystem:^10.0 |
| 11 | + */ |
| 12 | + |
| 13 | +namespace Security; |
| 14 | + |
| 15 | +class LaravelRateLimiter |
| 16 | +{ |
| 17 | + /** |
| 18 | + * Applica il rate limiting per una determinata "area" (es. 'api'). |
| 19 | + * |
| 20 | + * @param string $area Area logica (es. 'api') |
| 21 | + * @param array $config Configurazione completa di OSM (incluso $rate_limiting) |
| 22 | + * @param array $opts Opzioni aggiuntive (es. ['key_parts' => ['resource' => ..., 'token' => ...]]) |
| 23 | + * |
| 24 | + * @return array [bool $allowed, int $retryAfterSeconds] |
| 25 | + */ |
| 26 | + public static function enforce(string $area, array $config, array $opts = []): array |
| 27 | + { |
| 28 | + $cfg = $config['rate_limiting'] ?? []; |
| 29 | + |
| 30 | + $ip = function_exists('get_client_ip') ? get_client_ip() : ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); |
| 31 | + |
| 32 | + // Whitelist IP: sempre consentito |
| 33 | + $whitelist = (array)($cfg['whitelist_ips'] ?? []); |
| 34 | + if (in_array($ip, $whitelist, true)) { |
| 35 | + return [true, 0]; |
| 36 | + } |
| 37 | + |
| 38 | + // Blacklist IP: sempre bloccato |
| 39 | + $blacklist = (array)($cfg['blacklist_ips'] ?? []); |
| 40 | + if (in_array($ip, $blacklist, true)) { |
| 41 | + return [false, 0]; |
| 42 | + } |
| 43 | + |
| 44 | + // Costruzione chiave e limiti |
| 45 | + [$key, $max, $decay, $storePath] = self::buildKeyAndLimits($area, $cfg, $ip, $opts); |
| 46 | + |
| 47 | + // Prova ad usare il RateLimiter nativo di Illuminate |
| 48 | + if ( |
| 49 | + class_exists('Illuminate\\Cache\\RateLimiter') && |
| 50 | + class_exists('Illuminate\\Cache\\Repository') && |
| 51 | + class_exists('Illuminate\\Cache\\FileStore') && |
| 52 | + class_exists('Illuminate\\Filesystem\\Filesystem') |
| 53 | + ) { |
| 54 | + try { |
| 55 | + if (!is_dir($storePath)) { |
| 56 | + @mkdir($storePath, 0777, true); |
| 57 | + } |
| 58 | + $files = new \Illuminate\Filesystem\Filesystem(); |
| 59 | + $store = new \Illuminate\Cache\FileStore($files, $storePath); |
| 60 | + $repo = new \Illuminate\Cache\Repository($store); |
| 61 | + $rl = new \Illuminate\Cache\RateLimiter($repo); |
| 62 | + |
| 63 | + if ($rl->tooManyAttempts($key, $max)) { |
| 64 | + return [false, $rl->availableIn($key)]; |
| 65 | + } |
| 66 | + |
| 67 | + $rl->hit($key, $decay); |
| 68 | + |
| 69 | + return [true, 0]; |
| 70 | + } catch (\Throwable) { |
| 71 | + // In caso di problemi con Illuminate, prosegue col fallback |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + // Fallback file-based (compatibile con max/decay) |
| 76 | + return self::fallbackEnforce($storePath, $key, $max, $decay); |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Costruisce chiave, limiti e percorso store. |
| 81 | + */ |
| 82 | + private static function buildKeyAndLimits(string $area, array $cfg, string $ip, array $opts): array |
| 83 | + { |
| 84 | + $limits = (array)($cfg['limits'][$area] ?? []); |
| 85 | + $max = (int)($limits['max'] ?? 60); |
| 86 | + $decay = (int)($limits['decay'] ?? 60); |
| 87 | + |
| 88 | + // Strategia chiave: 'user' | 'ip' | 'ip_user' |
| 89 | + $strategy = (string)($cfg['strategy'] ?? 'user'); |
| 90 | + |
| 91 | + $userId = null; |
| 92 | + if (class_exists('Auth')) { |
| 93 | + try { |
| 94 | + $u = \Auth::user(); |
| 95 | + if ($u && isset($u->id)) { |
| 96 | + $userId = (int)$u->id; |
| 97 | + } |
| 98 | + } catch (\Throwable) { |
| 99 | + // ignora |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + $idParts = [$area, $strategy]; |
| 104 | + if ($strategy === 'user') { |
| 105 | + $idParts[] = $userId ?: ('ip:'.$ip); |
| 106 | + } elseif ($strategy === 'ip') { |
| 107 | + $idParts[] = 'ip:'.$ip; |
| 108 | + } else { // ip_user |
| 109 | + $idParts[] = 'u:'.($userId ?? 0); |
| 110 | + $idParts[] = 'ip:'.$ip; |
| 111 | + } |
| 112 | + |
| 113 | + // Ulteriori parti opzionali di chiave (per granularità) |
| 114 | + foreach ((array)($opts['key_parts'] ?? []) as $k => $v) { |
| 115 | + if (!empty($v)) { |
| 116 | + $idParts[] = $k.':'.$v; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + $key = 'osm:rate:'.sha1(implode('|', $idParts)); |
| 121 | + |
| 122 | + $storePath = (string)($cfg['store_path'] ?? (function_exists('base_dir') ? base_dir().'/files/cache/ratelimiter' : __DIR__.'/../../files/cache/ratelimiter')); |
| 123 | + |
| 124 | + return [$key, $max, $decay, $storePath]; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Fallback semplice su file (contatore per finestra temporale), con lock. |
| 129 | + */ |
| 130 | + private static function fallbackEnforce(string $storePath, string $key, int $max, int $decay): array |
| 131 | + { |
| 132 | + if (!is_dir($storePath)) { |
| 133 | + @mkdir($storePath, 0777, true); |
| 134 | + } |
| 135 | + $file = rtrim($storePath, '/\\').DIRECTORY_SEPARATOR.strtr($key, [':' => '_']).'.json'; |
| 136 | + $now = time(); |
| 137 | + $data = ['count' => 0, 'start' => $now]; |
| 138 | + |
| 139 | + $h = @fopen($file, 'c+'); |
| 140 | + if ($h === false) { |
| 141 | + // Se non posso accedere allo store, non blocco (fail-open conservativo) |
| 142 | + return [true, 0]; |
| 143 | + } |
| 144 | + |
| 145 | + try { |
| 146 | + @flock($h, LOCK_EX); |
| 147 | + $contents = stream_get_contents($h); |
| 148 | + if ($contents) { |
| 149 | + $decoded = json_decode($contents, true); |
| 150 | + if (is_array($decoded) && isset($decoded['count'], $decoded['start'])) { |
| 151 | + $data = $decoded; |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + // Reset finestra se scaduta |
| 156 | + if (($now - (int)$data['start']) >= $decay) { |
| 157 | + $data = ['count' => 0, 'start' => $now]; |
| 158 | + } |
| 159 | + |
| 160 | + if ((int)$data['count'] >= $max) { |
| 161 | + $retry = max(0, $decay - ($now - (int)$data['start'])); |
| 162 | + return [false, $retry]; |
| 163 | + } |
| 164 | + |
| 165 | + // Incremento e salvo |
| 166 | + $data['count'] = (int)$data['count'] + 1; |
| 167 | + ftruncate($h, 0); |
| 168 | + rewind($h); |
| 169 | + fwrite($h, json_encode($data)); |
| 170 | + |
| 171 | + return [true, 0]; |
| 172 | + } finally { |
| 173 | + @flock($h, LOCK_UN); |
| 174 | + @fclose($h); |
| 175 | + } |
| 176 | + } |
| 177 | +} |
| 178 | + |
0 commit comments