diff --git a/api/app/Controllers/JsonController.php b/api/app/Controllers/JsonController.php index f5b74d7..da5f80e 100644 --- a/api/app/Controllers/JsonController.php +++ b/api/app/Controllers/JsonController.php @@ -24,6 +24,12 @@ class JsonController extends \Controller protected function throttle(string $key, int $max, int $window): bool { $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + // Trusted server-local / whitelisted callers are never throttled: the static-site build + // fetches the read API from the box itself thousands of times per build. The throttle is + // for public abuse (real client IPs), not our own build. See Functions::isTrustedIp. + if (\Functions::isTrustedIp($ip)) { + return false; + } $count = (int) \Db::getValue( "SELECT COUNT(*) FROM `api_requests` WHERE `requesting_ip` = ? AND `request` = ? diff --git a/api/app/Helpers/Functions.php b/api/app/Helpers/Functions.php index daebe19..6c25f36 100644 --- a/api/app/Helpers/Functions.php +++ b/api/app/Helpers/Functions.php @@ -186,6 +186,31 @@ class Functions { + /** + * True for trusted, server-local / whitelisted callers that should bypass public rate + * limits (e.g. the SSG build hammering the read API from the box itself). + * - loopback (127.0.0.1, ::1) + * - the server's own address (same-host request) + * - anything in the optional TRUSTED_IPS config list (comma-separated IPs or CIDRs — + * e.g. the server's public IP, an internal build host, or a private range like 10.0.0.0/8) + * Deliberately does NOT blanket-trust all private ranges: behind a private-IP reverse proxy + * that would exempt every visitor. Add specific ranges to TRUSTED_IPS if your setup wants them. + */ + public static function isTrustedIp($ip) { + if ($ip === '127.0.0.1' || $ip === '::1') return true; + $serverAddr = $_SERVER['SERVER_ADDR'] ?? ''; + if ($serverAddr !== '' && $ip === $serverAddr) return true; + if (defined('TRUSTED_IPS') && TRUSTED_IPS) { + foreach (array_filter(array_map('trim', explode(',', TRUSTED_IPS))) as $entry) { + if ($ip === $entry) return true; + if (strpos($entry, '/') !== false + && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) + && self::ip_in_range($ip, $entry)) return true; + } + } + return false; + } + public static function ip_in_range($ip, $range) { if (strpos($range, '/') == false) $range .= '/32'; diff --git a/api/public/index.php b/api/public/index.php index efc71a4..38d06d7 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -1,4 +1,16 @@ 0, + 'path' => '/', + 'httponly' => true, + 'secure' => $https, + 'samesite' => 'Lax', + ]); +} @session_start(); require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/config.php';