279 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|
|
|