Integração Pangolin Proxy

This commit is contained in:
2025-12-06 21:11:34 -03:00
parent dc7c446254
commit 5291d8ccae
2008 changed files with 1062 additions and 477 deletions

View File

@@ -82,4 +82,30 @@ class ASNService
return false;
}
public static function getPrefixes($asn)
{
$asn = ltrim(strtoupper($asn), 'AS');
$url = "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" . $asn;
$context = stream_context_create(['http' => ['timeout' => 5]]);
$response = @file_get_contents($url, false, $context);
if ($response === FALSE) {
return [];
}
$data = json_decode($response, true);
$prefixes = [];
if (isset($data['status']) && $data['status'] === 'ok' && isset($data['data']['prefixes'])) {
foreach ($data['data']['prefixes'] as $item) {
if (isset($item['prefix'])) {
$prefixes[] = $item['prefix'];
}
}
}
return array_unique($prefixes);
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace App\Services;
use App\Models\Setting;
use App\Config\Database;
class PangolinService
{
private $apiUrl;
private $token;
private $orgId;
private $resourceName;
private $enabled;
private $resourceIdCache = null;
public function __construct()
{
$settingModel = new Setting();
$this->apiUrl = rtrim($settingModel->get('pangolin_url') ?? '', '/');
$this->token = $settingModel->get('pangolin_token');
$this->orgId = $settingModel->get('pangolin_org_id');
$this->resourceName = $settingModel->get('pangolin_resource_name');
$this->enabled = $settingModel->get('pangolin_enabled') === '1';
}
public function isEnabled()
{
return $this->enabled && !empty($this->apiUrl) && !empty($this->token) && !empty($this->orgId) && !empty($this->resourceName);
}
/**
* Obtém o resourceId numérico a partir do orgId + niceId (resourceName)
* Busca da lista de resources e filtra pelo niceId
*/
private function getResourceId()
{
if ($this->resourceIdCache !== null) {
return $this->resourceIdCache;
}
// Busca a lista de resources da organização
$endpoint = "/v1/org/{$this->orgId}/resources";
$result = $this->callApi('GET', $endpoint);
if ($result && isset($result['data']['resources']) && is_array($result['data']['resources'])) {
foreach ($result['data']['resources'] as $resource) {
if (isset($resource['niceId']) && $resource['niceId'] === $this->resourceName) {
$this->resourceIdCache = $resource['resourceId'];
$this->log("Resource ID encontrado: " . $this->resourceIdCache . " para niceId: " . $this->resourceName);
return $this->resourceIdCache;
}
}
$this->log("Resource com niceId '{$this->resourceName}' não encontrado na lista.");
}
$this->log("Falha ao obter Resource ID - Resposta: " . json_encode($result));
return null;
}
/**
* Obtém os IPs dos servidores ativos de um cliente
*/
private function getClientServerIps($clientId)
{
$ips = [];
try {
$conn = Database::getInstance()->getConnection();
$stmt = $conn->prepare("SELECT ip_v4, ip_v6 FROM servers WHERE client_id = :client_id AND status = 'active'");
$stmt->execute(['client_id' => $clientId]);
$servers = $stmt->fetchAll(\PDO::FETCH_ASSOC);
foreach ($servers as $server) {
if (!empty($server['ip_v4'])) {
$ips[] = $server['ip_v4'];
}
if (!empty($server['ip_v6'])) {
$ips[] = $server['ip_v6'];
}
}
} catch (\Exception $e) {
$this->log("Erro ao buscar IPs dos servidores: " . $e->getMessage());
}
return $ips;
}
/**
* Sincroniza os IPs dos servidores de um cliente com o Pangolin.
* @param int $clientId ID do cliente
* @param string $action 'add' ou 'remove'
*/
public function syncClient($clientId, $action)
{
if (!$this->isEnabled()) {
$this->log("Integração desabilitada ou mal configurada.");
return;
}
$resourceId = $this->getResourceId();
if (!$resourceId) {
$this->log("Não foi possível obter o Resource ID. Abortando sincronização.");
return;
}
$ips = $this->getClientServerIps($clientId);
if (empty($ips) && $action === 'add') {
$this->log("Nenhum IP de servidor encontrado para cliente ID: $clientId");
return;
}
if (!empty($ips)) {
$this->log("Cliente ID $clientId - IPs encontrados: " . count($ips) . " (" . implode(', ', $ips) . ")");
}
if ($action === 'add') {
foreach ($ips as $ip) {
$this->addRule($resourceId, $ip);
}
} elseif ($action === 'remove') {
foreach ($ips as $ip) {
$this->removeRule($resourceId, $ip);
}
}
}
/**
* Remove IPs específicos do Pangolin (usado quando servidor é deletado/modificado)
* @param array $ips Lista de IPs a remover
*/
public function removeServerIps($ips)
{
if (!$this->isEnabled()) {
return;
}
$resourceId = $this->getResourceId();
if (!$resourceId) {
return;
}
foreach ($ips as $ip) {
if (!empty($ip)) {
$this->removeRule($resourceId, $ip);
}
}
}
/**
* Adiciona uma regra ACCEPT para um IP.
* PUT /v1/resource/{resourceId}/rule
*/
private function addRule($resourceId, $ip)
{
$endpoint = "/v1/resource/{$resourceId}/rule";
$data = [
'action' => 'ACCEPT',
'match' => 'IP',
'value' => $ip,
'priority' => 100,
'enabled' => true
];
$result = $this->callApi('PUT', $endpoint, $data);
if ($result && isset($result['success']) && $result['success']) {
$this->log("Regra adicionada com sucesso para IP: $ip");
} else {
$msg = isset($result['message']) ? $result['message'] : json_encode($result);
$this->log("Falha ao adicionar regra para IP: $ip - $msg");
}
return $result;
}
/**
* Remove uma regra baseada no IP.
* Primeiro lista as regras para encontrar o ID, depois deleta.
*/
private function removeRule($resourceId, $ip)
{
// Listar regras existentes
$rules = $this->listRules($resourceId);
if (!$rules || !isset($rules['data']['rules'])) {
$this->log("Não foi possível listar regras para encontrar IP: $ip");
return;
}
$found = false;
foreach ($rules['data']['rules'] as $rule) {
if (
isset($rule['match']) && $rule['match'] === 'IP' &&
isset($rule['value']) && $rule['value'] === $ip &&
isset($rule['ruleId'])
) {
$found = true;
$this->deleteRule($resourceId, $rule['ruleId']);
}
}
if (!$found) {
$this->log("Regra para IP $ip não encontrada nas regras existentes.");
}
}
/**
* Lista todas as regras de um resource.
*/
private function listRules($resourceId)
{
$endpoint = "/v1/resource/{$resourceId}/rules";
return $this->callApi('GET', $endpoint);
}
/**
* Deleta uma regra pelo ID.
*/
private function deleteRule($resourceId, $ruleId)
{
$endpoint = "/v1/resource/{$resourceId}/rule/{$ruleId}";
$result = $this->callApi('DELETE', $endpoint);
if ($result && isset($result['success']) && $result['success']) {
$this->log("Regra ID $ruleId removida com sucesso.");
} else {
$msg = isset($result['message']) ? $result['message'] : json_encode($result);
$this->log("Falha ao remover regra ID $ruleId - $msg");
}
return $result;
}
private function callApi($method, $endpoint, $data = [])
{
$url = $this->apiUrl . $endpoint;
$this->log("API Call: [$method] $url");
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
if (!empty($data)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$this->log("Request Body: " . json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
$this->log("Response HTTP $httpCode: " . substr($response, 0, 2000));
if ($error) {
$this->log("Curl Error: $error");
return ['success' => false, 'error' => $error];
}
return json_decode($response, true);
}
private function log($message)
{
try {
$conn = Database::getInstance()->getConnection();
$stmt = $conn->prepare("INSERT INTO api_logs (action, message) VALUES ('pangolin_sync', :message)");
$stmt->execute(['message' => substr($message, 0, 65000)]);
} catch (\Exception $e) {
error_log("Pangolin Log Error: " . $e->getMessage());
}
}
}