Files
Virtnet.bond/extensions/Servers/Proxmox/Services/CoreRouterClient.php
2025-11-30 11:57:22 +01:00

279 lines
11 KiB
PHP

<?php
namespace Paymenter\Extensions\Servers\Proxmox\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
class CoreRouterClient
{
public function listStates(int $limit = null): array
{
$queryParams = [];
// Add limit parameter if specified or use config default
// Increase default limit significantly to ensure we capture all connections
$stateLimit = $limit ?? config('corerouter.state_limit', 1000);
if ($stateLimit > 0) {
$queryParams['limit'] = $stateLimit;
}
// Normal flow: fetch all states
try {
$response = $this->request('/firewall/states', $queryParams);
} catch (\RuntimeException $e) {
// Try alternative endpoint path if standard fails
if (str_contains($e->getMessage(), '404')) {
try {
$response = $this->request('/firewall/state/list', $queryParams);
} catch (\RuntimeException $e2) {
// Re-throw with original error if alternative also fails
throw $e;
}
} else {
throw $e;
}
}
// The API returns data directly in the 'data' field as an array
// Response structure: {"code":200,"status":"ok","data":[...],"_links":{...}}
$data = Arr::get($response, 'data', []);
// Check for pagination links - if there are more results, fetch them
$links = Arr::get($response, '_links', []);
$allStates = is_array($data) ? $data : [];
// Handle pagination if present (data might be nested in items)
if (isset($allStates['items']) && is_array($allStates['items'])) {
$allStates = $allStates['items'];
}
// Fetch ALL available states by continuing pagination until no more pages
// Continue pagination if:
// 1. There's a next link, OR
// 2. We got exactly the requested limit (suggests there might be more)
$pageCount = 1;
$hasNextLink = isset($links['next']) && !empty($links['next']);
$gotFullLimit = count($allStates) >= $stateLimit;
$maxPages = 1000; // Safety limit to prevent infinite loops (should never reach this)
while (($hasNextLink || $gotFullLimit) && $pageCount < $maxPages) {
try {
// Extract offset and limit from next URL or increment offset
$offset = count($allStates);
$nextQueryParams = array_merge($queryParams, ['offset' => $offset, 'limit' => $stateLimit]);
$nextResponse = $this->request('/firewall/states', $nextQueryParams);
$nextData = Arr::get($nextResponse, 'data', []);
if (isset($nextData['items']) && is_array($nextData['items'])) {
$nextData = $nextData['items'];
}
if (is_array($nextData) && !empty($nextData)) {
$allStates = array_merge($allStates, $nextData);
$links = Arr::get($nextResponse, '_links', []);
$hasNextLink = isset($links['next']) && !empty($links['next']);
$gotFullLimit = count($nextData) >= $stateLimit;
$pageCount++;
// Log progress every 10 pages to track fetching
if ($pageCount % 10 === 0) {
\Log::debug('Core Router API pagination progress', [
'pages_fetched' => $pageCount,
'total_states_so_far' => count($allStates),
'has_next_link' => $hasNextLink,
]);
}
} else {
// No more data, stop pagination
break;
}
} catch (\Throwable $e) {
\Log::warning('Core Router API pagination failed', [
'page' => $pageCount + 1,
'error' => $e->getMessage(),
]);
break;
}
}
// Log a warning if we hit the safety limit (should never happen)
if ($pageCount >= $maxPages) {
\Log::warning('Core Router API pagination reached safety limit', [
'pages' => $pageCount,
'total_states' => count($allStates),
]);
}
// Log pagination results
if ($pageCount > 1) {
\Log::debug('Core Router API fetched all states via pagination', [
'pages_fetched' => $pageCount,
'total_states' => count($allStates),
'final_page_count' => $pageCount,
]);
} else {
\Log::debug('Core Router API fetched states (single page)', [
'total_states' => count($allStates),
]);
}
// Ensure we return an array
return is_array($allStates) ? $allStates : [];
}
public function gatewayStatus(): array
{
// Try alternative endpoint paths - gateway status endpoint may vary
$endpoints = [
'/diagnostics/gateways/status',
'/diagnostics/gateway/status',
'/diagnostics/gateways',
];
foreach ($endpoints as $endpoint) {
try {
$response = $this->requestOptional($endpoint);
if ($response !== null) {
// The API returns data directly in the 'data' field as an array
$data = Arr::get($response, 'data', []);
// Handle pagination if present (data might be nested in items)
if (isset($data['items']) && is_array($data['items'])) {
$data = $data['items'];
}
return is_array($data) ? $data : [];
}
} catch (\Throwable $e) {
// Continue to next endpoint
continue;
}
}
// If all endpoints fail, return empty array (gateway data is optional)
return [];
}
/**
* Make an optional API request that returns null on 404 instead of throwing
*/
protected function requestOptional(string $endpoint, array $query = []): ?array
{
try {
return $this->request($endpoint, $query);
} catch (\RuntimeException $e) {
// Return null for 404 errors (endpoint not found)
if (str_contains($e->getMessage(), '404') || str_contains($e->getMessage(), 'not found')) {
return null;
}
// Re-throw other errors
throw $e;
}
}
protected function request(string $endpoint, array $query = []): array
{
$baseUrl = rtrim((string) config('corerouter.base_url'), '/');
if (!$baseUrl) {
throw new \RuntimeException('Core Router API base URL is not configured.');
}
// Ensure endpoint starts with /
$endpoint = '/' . ltrim($endpoint, '/');
// If base_url doesn't include /api/v2, add it to the endpoint
if (!str_contains($baseUrl, '/api/v2')) {
$endpoint = '/api/v2' . $endpoint;
}
$fullUrl = $baseUrl . $endpoint;
$response = $this->http()
->get($fullUrl, $query);
// Check if response is HTML (indicates wrong URL or auth failure)
$contentType = $response->header('Content-Type', '');
$body = $response->body();
if (str_contains($contentType, 'text/html') || (strlen($body) > 0 && str_starts_with(trim($body), '<!DOCTYPE'))) {
$statusCode = $response->status();
if ($statusCode === 404) {
throw new \RuntimeException(
"Core Router API endpoint not found (404). " .
"Please verify the base URL includes /api/v2 (e.g., https://your-corerouter/api/v2). " .
"Attempted URL: {$fullUrl}"
);
} elseif ($statusCode === 401 || $statusCode === 403) {
throw new \RuntimeException(
"Core Router API authentication failed ({$statusCode}). " .
"Please verify your API credentials (API key or username/password) are correct."
);
} else {
throw new \RuntimeException(
"Core Router API returned an unexpected response (HTTP {$statusCode}). " .
"This may indicate the API is not properly configured or accessible. " .
"Attempted URL: {$fullUrl}"
);
}
}
if ($response->failed()) {
$jsonMessage = $response->json('message');
$statusCode = $response->status();
if ($jsonMessage) {
throw new \RuntimeException("Core Router API request failed (HTTP {$statusCode}): {$jsonMessage}");
} else {
throw new \RuntimeException(
"Core Router API request failed (HTTP {$statusCode}). " .
"Attempted URL: {$fullUrl}. " .
"Response: " . substr($body, 0, 200)
);
}
}
$jsonResponse = $response->json();
// Validate response structure
if (!is_array($jsonResponse)) {
throw new \RuntimeException(
"Core Router API returned invalid response format. " .
"Expected JSON array/object, got: " . gettype($jsonResponse) . ". " .
"Attempted URL: {$fullUrl}"
);
}
return $jsonResponse;
}
protected function http(): PendingRequest
{
$apiKey = (string) config('corerouter.api_key');
$username = (string) config('corerouter.username');
$password = (string) config('corerouter.password');
$request = Http::timeout((int) config('corerouter.timeout', 10))
->acceptJson()
->withOptions([
'verify' => (bool) config('corerouter.verify_ssl', true),
]);
if ($apiKey) {
$request = $request->withHeaders([
'X-API-Key' => $apiKey,
]);
} elseif ($username && $password) {
$request = $request->withBasicAuth($username, $password);
} else {
throw new \RuntimeException('Core Router API credentials are not configured.');
}
return $request;
}
}