Files
api/services/nginx/app/routes/edgeGatewaysRoute.php
T

337 lines
14 KiB
PHP

<?php
namespace routes;
use classes\authentication;
use classes\edge_gateway_manager;
use classes\response;
use objects\logs_o;
use traits\route_t;
class edgeGatewaysRoute
{
use route_t;
public function run(): void
{
$this->get('/edge-gateways', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$departmentId = self::isParametersSet(['department_id']) ? (int)self::getParameter('department_id') : null;
if ($departmentId !== null && $departmentId > 0) {
$this->requireDepartmentAccess((int)$departmentId);
}
$response->success((new edge_gateway_manager())->listGateways($departmentId));
}, [
'modules_shelly_config' => 'Manage department edge gateways for local Shelly control',
]);
$this->get('/edge-gateways/{id}', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$gateway = (new edge_gateway_manager())->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
$response->success($gateway);
}, [
'modules_shelly_config' => 'View department edge gateway detail',
]);
$this->post('/edge-gateways/install-token', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['department_id']);
$departmentId = (int)self::getParameter('department_id');
self::requireParameterIntPositive($departmentId, 'department_id');
$this->requireDepartmentAccess($departmentId);
$user = (new authentication())->get_user();
$response->success(
(new edge_gateway_manager())->createInstallToken(
$departmentId,
self::isParametersSet(['label']) ? (string)self::getParameter('label') : null,
$user ? (int)$user->id : null
),
201
);
}, [
'modules_shelly_config' => 'Create a one-time Raspberry Pi edge gateway installer token',
]);
$this->post('/edge-gateways/{id}/discovery', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$manager = new edge_gateway_manager();
$gateway = $manager->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
$user = (new authentication())->get_user();
$response->success($manager->queueDiscovery($gatewayId, $user ? (int)$user->id : null));
}, [
'modules_shelly_config' => 'Trigger a Shelly LAN discovery job on an edge gateway',
]);
$this->put('/edge-gateways/{id}/bindings', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['bindings']);
self::requireType(self::getParameter('bindings'), self::TYPE_ARRAY());
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$manager = new edge_gateway_manager();
$gateway = $manager->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
$user = (new authentication())->get_user();
$response->success($manager->setRelayBindings(
$gatewayId,
(array)self::getParameter('bindings'),
$user ? (int)$user->id : null
));
}, [
'modules_shelly_config' => 'Approve or override relay bindings for an edge gateway',
]);
$this->post('/edge-gateways/{id}/update-jobs', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['target_version']);
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$manager = new edge_gateway_manager();
$gateway = $manager->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
$user = (new authentication())->get_user();
$response->success($manager->queueUpdate(
$gatewayId,
(string)self::getParameter('target_version'),
self::isParametersSet(['release_channel']) ? (string)self::getParameter('release_channel') : edge_gateway_manager::DEFAULT_RELEASE_CHANNEL,
$user ? (int)$user->id : null
), 201);
}, [
'modules_shelly_config' => 'Queue an automatic edge gateway update',
]);
$this->post('/edge-gateways/{id}/shell-sessions', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['reason']);
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$manager = new edge_gateway_manager();
$gateway = $manager->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
self::requireMinLength('reason', 5);
$user = (new authentication())->get_user();
$response->success($manager->createShellSession(
$gatewayId,
(string)self::getParameter('reason'),
$user ? (int)$user->id : null
), 201);
}, [
'modules_shelly_config' => 'Approve a break-glass root shell on an edge gateway',
]);
$this->post('/edge-gateways/{id}/rotate-credentials', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$manager = new edge_gateway_manager();
$gateway = $manager->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
$user = (new authentication())->get_user();
$response->success($manager->rotateGatewayCredentials($gatewayId, $user ? (int)$user->id : null));
}, [
'modules_shelly_config' => 'Rotate edge gateway agent credentials',
]);
$this->post('/departments/{id}/gateway-cutover', function () {
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$departmentId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($departmentId, 'id');
$this->requireDepartmentAccess($departmentId);
self::requireParameters(['transport_mode']);
$user = (new authentication())->get_user();
$response->success((new edge_gateway_manager())->setDepartmentTransportMode(
$departmentId,
(string)self::getParameter('transport_mode'),
$user ? (int)$user->id : null
));
}, [
'modules_shelly_config' => 'Cut a department over from Shelly cloud to local edge gateways',
]);
$this->get('/edge-agent/install.sh', fn() => $this->renderInstallScript());
$this->get('/edge-agent/artifacts/agent.mjs', fn() => $this->renderAgentArtifact('agent.mjs', 'application/javascript; charset=utf-8'));
$this->get('/edge-agent/artifacts/package.json', fn() => $this->renderAgentArtifact('package.json', 'application/json; charset=utf-8'));
$this->post('/edge-agent/claim', fn() => $this->handleAgentClaim());
$this->post('/edge-agent/gateways/{id}/heartbeat', fn() => $this->handleAgentHeartbeat());
$this->post('/edge-agent/gateways/{id}/commands/poll', fn() => $this->handleAgentCommandPoll());
$this->post('/edge-agent/gateways/{id}/commands/{jobId}/result', fn() => $this->handleAgentCommandResult());
$this->post('/edge-agent/internal/agent/auth', fn() => $this->handleInternalAgentAuth());
$this->post('/edge-agent/internal/shell/auth', fn() => $this->handleInternalShellAuth());
$this->post('/edge-agent/internal/shell-sessions/{id}/close', fn() => $this->handleInternalShellClose());
}
private function renderInstallScript(): void
{
$manager = new edge_gateway_manager();
$token = trim((string)$this->fromQuery('token'));
if ($token === '') {
http_response_code(400);
echo 'Missing token';
exit;
}
header('Content-Type: text/x-shellscript; charset=utf-8');
echo $manager->buildInstallScript($token);
exit;
}
private function renderAgentArtifact(string $fileName, string $contentType): void
{
$artifactPath = dirname(WD, 3) . '/services/edge-agent/dist/' . $fileName;
if (!is_file($artifactPath)) {
http_response_code(404);
echo 'Missing edge agent artifact';
exit;
}
header('Content-Type: ' . $contentType);
echo file_get_contents($artifactPath);
exit;
}
private function handleAgentClaim(): void
{
global /** @var response $response */ $response;
self::requireParameters(['token']);
$payload = self::getParametersAsArray();
$response->success((new edge_gateway_manager())->claimGateway(
(string)$payload['token'],
trim((string)($payload['hostname'] ?? gethostname() ?: 'unknown-gateway')),
isset($payload['installed_version']) ? (string)$payload['installed_version'] : null,
(array)($payload['metadata'] ?? [])
), 201);
}
private function handleAgentHeartbeat(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$payload = self::getParametersAsArray();
$token = trim((string)($payload['agent_token'] ?? $this->fromRequest('agent_token')));
if ($token === '') {
$response->error('Missing edge gateway agent token', 401);
}
$response->success((new edge_gateway_manager())->recordHeartbeat($gatewayId, $token, $payload));
}
private function handleAgentCommandPoll(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($gatewayId, 'id');
$payload = self::getParametersAsArray();
$token = trim((string)($payload['agent_token'] ?? $this->fromRequest('agent_token')));
if ($token === '') {
$response->error('Missing edge gateway agent token', 401);
}
$waitSeconds = isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : edge_gateway_manager::COMMAND_POLL_TIMEOUT_SECONDS;
$response->success((new edge_gateway_manager())->pollCommand($gatewayId, $token, $waitSeconds));
}
private function handleAgentCommandResult(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$jobId = (int)$this->fromRoute('jobId');
self::requireParameterIntPositive($gatewayId, 'id');
self::requireParameterIntPositive($jobId, 'jobId');
$payload = self::getParametersAsArray();
$token = trim((string)($payload['agent_token'] ?? $this->fromRequest('agent_token')));
if ($token === '') {
$response->error('Missing edge gateway agent token', 401);
}
$response->success((new edge_gateway_manager())->submitCommandResult(
$gatewayId,
$jobId,
$token,
(bool)($payload['ok'] ?? false),
isset($payload['payload']) && is_array($payload['payload']) ? (array)$payload['payload'] : [],
isset($payload['error']) ? (string)$payload['error'] : null
));
}
private function handleInternalAgentAuth(): void
{
global /** @var response $response */ $response;
$this->requireInternalSecret();
self::requireParameters(['gatewayId', 'agentToken']);
$gateway = (new edge_gateway_manager())->authenticateGateway(
(int)self::getParameter('gatewayId'),
(string)self::getParameter('agentToken')
);
$response->success($gateway->asArray());
}
private function handleInternalShellAuth(): void
{
global /** @var response $response */ $response;
$this->requireInternalSecret();
self::requireParameters(['sessionToken']);
$response->success((new edge_gateway_manager())->validateShellSessionToken((string)self::getParameter('sessionToken')));
}
private function handleInternalShellClose(): void
{
global /** @var response $response */ $response;
$this->requireInternalSecret();
self::requireParameters(['sessionToken']);
$payload = self::getParametersAsArray();
$response->success((new edge_gateway_manager())->closeShellSession(
(string)$payload['sessionToken'],
(string)($payload['transcript'] ?? ''),
(string)($payload['closedReason'] ?? 'closed')
));
}
private function requireInternalSecret(): void
{
global /** @var response $response */ $response;
$expected = trim((string)(getenv('EDGE_INTERNAL_SECRET') ?: getenv('EDGE_BROKER_SHARED_SECRET') ?: ''));
if ($expected === '') {
return;
}
$headers = function_exists('getallheaders') ? getallheaders() : [];
$provided = trim((string)($headers['X-Edge-Internal-Secret'] ?? $headers['X-Edge-Broker-Secret'] ?? $this->fromRequest('internal_secret') ?? ''));
if ($provided === '' || !hash_equals($expected, $provided)) {
$response->error('Forbidden', 403);
}
}
}