clusev/app/Services/WgBridge.php

145 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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', 'gate-ssh-on', 'gate-ssh-off', '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':
case 'gate-ssh-on':
case 'gate-ssh-off':
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 (! $this->validPort($port)) {
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 (! $this->validPort($port)) {
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 [];
}
/** A valid 165535 UDP port given as a numeric string. */
private function validPort(string $port): bool
{
return preg_match('/^\d{1,5}$/', $port) === 1 && (int) $port >= 1 && (int) $port <= 65535;
}
}