clusev/app/Services/WgBridge.php

137 lines
5.2 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Str;
/**
* The app side of the WireGuard write-bridge. `request()` writes a strictly-validated request to
* run/wg-request.json (a host watcher runs it); `result()` reads the host-written result for an id
* the app issued. Validation here is the FIRST gate — the host re-validates every value too.
*/
class WgBridge
{
/** Actions the UI may request; the host whitelists the same set. */
public const ACTIONS = ['add-peer', 'remove-peer', 'gate-up', 'gate-down', 'set-endpoint', 'set-port', 'set-subnet', 'setup'];
private function dir(): string
{
return storage_path('app/restart-signal');
}
/**
* Write a write-request; returns the request id. Throws InvalidArgumentException on a bad
* action or arg (defence-in-depth; the host re-validates).
*
* @param array<string,string> $args
*/
public function request(string $action, array $args = []): string
{
if (! in_array($action, self::ACTIONS, true)) {
throw new \InvalidArgumentException('unknown action');
}
$clean = $this->validateArgs($action, $args);
$id = Str::random(32);
$payload = array_merge(['id' => $id, 'action' => $action, 'at' => time()], $clean);
@mkdir($this->dir(), 0775, true);
file_put_contents($this->dir().'/wg-request.json', json_encode($payload, JSON_UNESCAPED_SLASHES));
return $id;
}
/**
* Read the host result for an id THIS app issued. Returns null until the result exists.
* Includes the QR sidecar SVG when present (add-peer). Never throws.
*
* @return array{ok:bool, message:string, config:?string, qr:?string}|null
*/
public function result(string $id): ?array
{
if (preg_match('/^[A-Za-z0-9]{16,64}$/', $id) !== 1) {
return null;
}
$file = $this->dir()."/wg-result-{$id}.json";
if (! is_file($file)) {
return null;
}
$data = json_decode((string) @file_get_contents($file), true);
if (! is_array($data) || ($data['id'] ?? null) !== $id) {
return null;
}
$qrFile = $this->dir()."/wg-qr-{$id}.svg";
$qr = is_file($qrFile) ? (string) @file_get_contents($qrFile) : null;
return [
'ok' => (bool) ($data['ok'] ?? false),
'message' => (string) ($data['message'] ?? ''),
'config' => isset($data['config']) && is_string($data['config']) ? $data['config'] : null,
'qr' => $qr,
];
}
/**
* @param array<string,string> $args
* @return array<string,string>
*/
private function validateArgs(string $action, array $args): array
{
$name = (string) ($args['name'] ?? '');
switch ($action) {
case 'add-peer':
case 'remove-peer':
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
throw new \InvalidArgumentException('invalid name');
}
return ['name' => $name];
case 'gate-up':
case 'gate-down':
return [];
case 'set-endpoint':
$ep = (string) ($args['endpoint'] ?? '');
if (preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $ep) !== 1) {
throw new \InvalidArgumentException('invalid endpoint');
}
return ['endpoint' => $ep];
case 'set-port':
$port = (string) ($args['port'] ?? '');
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
throw new \InvalidArgumentException('invalid port');
}
return ['port' => $port];
case 'set-subnet':
$subnet = (string) ($args['subnet'] ?? '');
if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
throw new \InvalidArgumentException('invalid subnet');
}
return ['subnet' => $subnet];
case 'setup':
$subnet = (string) ($args['subnet'] ?? '');
$port = (string) ($args['port'] ?? '');
$endpoint = (string) ($args['endpoint'] ?? '');
$name = (string) ($args['name'] ?? '');
if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
throw new \InvalidArgumentException('invalid subnet');
}
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
throw new \InvalidArgumentException('invalid port');
}
if ($endpoint !== '' && preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $endpoint) !== 1) {
throw new \InvalidArgumentException('invalid endpoint');
}
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
throw new \InvalidArgumentException('invalid name');
}
return ['subnet' => $subnet, 'port' => $port, 'endpoint' => $endpoint, 'name' => $name];
}
return [];
}
}