The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud' was misleading. Renames the directory, the edge client module, and every doc/script/identifier that used the term. - cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths) - edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient - cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md - CLOUD_* env/config keys -> SERVER_* - tests renamed to match Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
45 lines
2 KiB
PHP
45 lines
2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Api;
|
|
|
|
use App\Config;
|
|
use App\Http\{Request, Response};
|
|
use App\Security\Hmac;
|
|
use App\Repository\{DeviceRepository, ZoneRepository, ThresholdRepository, NonceRepository, RateLimiterRepository};
|
|
|
|
final class ConfigController
|
|
{
|
|
public function __construct(
|
|
private DeviceRepository $devices, private ZoneRepository $zones, private ThresholdRepository $thresholds,
|
|
private NonceRepository $nonces, private Config $cfg,
|
|
private ?RateLimiterRepository $rateLimiter = null,
|
|
) {}
|
|
|
|
public function get(Request $req, int $now, string $clientIp = ''): Response
|
|
{
|
|
// Pre-auth per-IP anti-flood, like /api/events — HMAC + DB reads on every hit.
|
|
if ($this->rateLimiter !== null && $clientIp !== ''
|
|
&& $this->rateLimiter->hit('ip:' . $clientIp, intdiv($now, 60)) > $this->cfg->ingestIpRateLimitPerMin()) {
|
|
return Response::json(['error' => 'rate_limited'], 429);
|
|
}
|
|
$deviceId = (string)$req->query('device_id');
|
|
$secret = $deviceId !== '' ? $this->devices->secretFor($deviceId) : null;
|
|
$nonce = $req->header('X-Nonce') ?? ''; $ts = $req->header('X-Timestamp') ?? ''; $sig = $req->header('X-Signature') ?? '';
|
|
if ($secret === null || !Hmac::verify($secret, $nonce, $ts, $req->body(), $sig)) {
|
|
return Response::json(['error' => 'unauthorized'], 401);
|
|
}
|
|
if (abs($now - (int)(float)$ts) > $this->cfg->timestampWindow()) {
|
|
return Response::json(['error' => 'stale'], 401);
|
|
}
|
|
if ($nonce === '' || $this->nonces->seen($nonce)) {
|
|
return Response::json(['error' => 'replay'], 401);
|
|
}
|
|
$this->nonces->remember($nonce, $now + $this->cfg->nonceTtl());
|
|
|
|
return Response::json([
|
|
'version' => $this->devices->configVersion($deviceId),
|
|
'zones' => $this->zones->forDevice($deviceId),
|
|
'thresholds' => $this->thresholds->forDevice($deviceId),
|
|
], 200);
|
|
}
|
|
}
|