Skip to content

Commit 6203ce6

Browse files
committed
feat: aggiunto rate limiting API con Illuminate RateLimiter e store su file
1 parent 4e23e99 commit 6203ce6

4 files changed

Lines changed: 208 additions & 0 deletions

File tree

api/index.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ function serverError()
3838

3939
include_once __DIR__.'/../core.php';
4040

41+
// Rate limiting per API (se abilitato)
42+
if (($config['rate_limiting']['enabled'] ?? false)) {
43+
[$ok, $retry] = \Security\LaravelRateLimiter::enforce('api', $config, [
44+
'key_parts' => [
45+
'resource' => get('resource'),
46+
'token' => get('token'),
47+
],
48+
]);
49+
if (!$ok) {
50+
http_response_code(429);
51+
exit('Too Many Requests');
52+
}
53+
}
54+
55+
4156
// Permesso di accesso all'API da ogni dispositivo
4257
header('Access-Control-Allow-Origin: *');
4358
header('Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS');

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"guzzlehttp/guzzle": "^7.0.1",
4343
"ifsnop/mysqldump-php": "^2.3",
4444
"illuminate/database": "^10.0",
45+
"illuminate/cache": "^10.0",
46+
"illuminate/filesystem": "^10.0",
4547
"intervention/image": "^3.0",
4648
"jurosh/pdf-merge": "^2.1",
4749
"league/csv": "^9.7.0",

config.example.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,16 @@
7272

7373
// Configura il limite di tempo di esecuzione del file cron.php
7474
$php_time_limit = '';
75+
76+
77+
// Integrazione con Laravel per Rate limiting
78+
$rate_limiting = [
79+
'enabled' => false,
80+
'store_path' => __DIR__.'/files/cache/ratelimiter',
81+
'strategy' => 'user', // 'user' | 'ip' | 'ip_user'
82+
'limits' => [
83+
'api' => ['max' => 60, 'decay' => 60],
84+
],
85+
'whitelist_ips' => [],
86+
'blacklist_ips' => [],
87+
];
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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

Comments
 (0)