$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 $args * @return array */ 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]; } return []; } }