Two self-contained framework fixes, no schema dependencies: - throttle(): exempt trusted callers via new Functions::isTrustedIp() — loopback, the server's own IP, and an optional TRUSTED_IPS config allowlist (IPs/CIDRs). Fixes the SSG build (which fetches the read API from the box thousands of times per build) tripping the public rate limit and baking empty data into the deploy. Public client IPs stay limited. - public/index.php: set secure session cookie params (httponly, SameSite=Lax, secure on https) before session_start, so session/login state rides on a hardened cookie. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
48 lines
1.8 KiB
PHP
48 lines
1.8 KiB
PHP
<?php
|
|
namespace App\Controllers;
|
|
|
|
/** Base for JSON endpoints: response envelope + IP throttle. Extends core \Controller. */
|
|
class JsonController extends \Controller
|
|
{
|
|
/** Emit { ok, data, error } with the right HTTP status, then stop. */
|
|
protected function json($data = null, int $status = 200, ?array $error = null): void
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: application/json; charset=UTF-8');
|
|
echo json_encode([
|
|
'ok' => $error === null,
|
|
'data' => $data,
|
|
'error' => $error, // ['code' => ..., 'message' => ...] or null
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Returns true when the caller has EXCEEDED $max hits on $key within $window seconds.
|
|
* Reuses the api_requests table (no new table needed).
|
|
*/
|
|
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` = ?
|
|
AND `created_date` > (NOW() - INTERVAL ? SECOND)",
|
|
[$ip, $key, $window]
|
|
);
|
|
\Db::insert('api_requests', [
|
|
'requesting_ip' => $ip,
|
|
'request' => $key,
|
|
'service' => 'foundation',
|
|
'domainURI' => $_SERVER['HTTP_HOST'] ?? '',
|
|
'created_date' => date('Y-m-d H:i:s'),
|
|
]);
|
|
return $count >= $max;
|
|
}
|
|
}
|