Integração Pangolin Proxy
This commit is contained in:
@@ -15,13 +15,31 @@ class ApiAuthController
|
||||
|
||||
if (empty($serial_key)) {
|
||||
View::json(['error' => 'Serial Key required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$serverModel = new Server();
|
||||
$server = $serverModel->first('serial_key', $serial_key);
|
||||
|
||||
if (!$server || $server['status'] !== 'active') {
|
||||
View::json(['error' => 'Invalid or inactive server'], 401);
|
||||
if (!$server) {
|
||||
View::json(['error' => 'Invalid server'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($server['status'] !== 'active') {
|
||||
View::json(['error' => 'Server is inactive'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check client status
|
||||
$conn = \App\Config\Database::getInstance()->getConnection();
|
||||
$stmt = $conn->prepare("SELECT status FROM clients WHERE id = :id");
|
||||
$stmt->execute(['id' => $server['client_id']]);
|
||||
$client = $stmt->fetch();
|
||||
|
||||
if (!$client || $client['status'] !== 'active') {
|
||||
View::json(['error' => 'Client is inactive'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate IP
|
||||
|
||||
@@ -72,6 +72,17 @@ class ClientController
|
||||
'client_id' => $clientId
|
||||
]);
|
||||
|
||||
// Pangolin Integration
|
||||
try {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
if ($pangolinService->isEnabled()) {
|
||||
$pangolinService->syncClient($clientId, 'add');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log error silently, don't break the flow
|
||||
error_log("Pangolin Sync Error (Create): " . $e->getMessage());
|
||||
}
|
||||
|
||||
View::redirect('/admin/clients');
|
||||
}
|
||||
|
||||
@@ -131,19 +142,64 @@ class ClientController
|
||||
$stmtUser = $conn->prepare($sqlUser);
|
||||
$stmtUser->execute($paramsUser);
|
||||
|
||||
// Cascade Deactivate Servers
|
||||
// Pangolin Integration - Buscar IPs dos servidores ATIVOS ANTES de desativá-los
|
||||
$serverIpsToRemove = [];
|
||||
if ($_POST['status'] === 'inactive') {
|
||||
$stmtIps = $conn->prepare("SELECT ip_v4, ip_v6 FROM servers WHERE client_id = :client_id AND status = 'active'");
|
||||
$stmtIps->execute(['client_id' => $id]);
|
||||
$servers = $stmtIps->fetchAll(\PDO::FETCH_ASSOC);
|
||||
foreach ($servers as $server) {
|
||||
if (!empty($server['ip_v4'])) {
|
||||
$serverIpsToRemove[] = $server['ip_v4'];
|
||||
}
|
||||
if (!empty($server['ip_v6'])) {
|
||||
$serverIpsToRemove[] = $server['ip_v6'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cascade Deactivate Servers quando cliente é desativado
|
||||
if ($_POST['status'] === 'inactive') {
|
||||
$sqlServers = "UPDATE servers SET status = 'inactive' WHERE client_id = :client_id";
|
||||
$stmtServers = $conn->prepare($sqlServers);
|
||||
$stmtServers->execute(['client_id' => $id]);
|
||||
}
|
||||
|
||||
// Pangolin Integration - Sincroniza com base no status
|
||||
try {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
if ($pangolinService->isEnabled()) {
|
||||
if ($_POST['status'] === 'active') {
|
||||
$pangolinService->syncClient($id, 'add');
|
||||
} else {
|
||||
// Remove os IPs que foram coletados antes de desativar os servidores
|
||||
if (!empty($serverIpsToRemove)) {
|
||||
$pangolinService->removeServerIps($serverIpsToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Pangolin Sync Error (Update): " . $e->getMessage());
|
||||
}
|
||||
|
||||
View::redirect('/admin/clients');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$clientModel = new Client();
|
||||
|
||||
// Pangolin Integration (Remove before delete to get ASN)
|
||||
try {
|
||||
$client = $clientModel->find($id);
|
||||
if ($client && $client['status'] === 'active') {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
$pangolinService->syncClient($id, 'remove');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Pangolin Sync Error (Delete): " . $e->getMessage());
|
||||
}
|
||||
|
||||
$clientModel->delete($id);
|
||||
View::redirect('/admin/clients');
|
||||
}
|
||||
|
||||
@@ -68,6 +68,16 @@ class ServerController
|
||||
$stmt = \App\Config\Database::getInstance()->getConnection()->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
|
||||
// Pangolin Integration - Sincroniza o cliente após adicionar servidor
|
||||
try {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
if ($pangolinService->isEnabled() && $client['status'] === 'active') {
|
||||
$pangolinService->syncClient($_POST['client_id'], 'add');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Pangolin Sync Error (Server Create): " . $e->getMessage());
|
||||
}
|
||||
|
||||
View::redirect('/admin/servers');
|
||||
}
|
||||
|
||||
@@ -113,6 +123,12 @@ class ServerController
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar IPs antigos para remover do Pangolin se mudaram
|
||||
$oldIpV4 = $server['ip_v4'];
|
||||
$oldIpV6 = $server['ip_v6'];
|
||||
$oldClientId = $server['client_id'];
|
||||
$oldStatus = $server['status'];
|
||||
|
||||
$data = [
|
||||
'id' => $id,
|
||||
'client_id' => $_POST['client_id'],
|
||||
@@ -126,13 +142,88 @@ class ServerController
|
||||
$stmt = \App\Config\Database::getInstance()->getConnection()->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
|
||||
// Pangolin Integration - Sincroniza se IP, status ou cliente mudou
|
||||
try {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
if ($pangolinService->isEnabled()) {
|
||||
$clientModel = new Client();
|
||||
$client = $clientModel->find($_POST['client_id']);
|
||||
|
||||
// Coletar IPs que mudaram para remover
|
||||
$ipsToRemove = [];
|
||||
if ($oldIpV4 !== $_POST['ip_v4']) {
|
||||
$ipsToRemove[] = $oldIpV4;
|
||||
}
|
||||
if ($oldIpV6 !== ($_POST['ip_v6'] ?? null)) {
|
||||
$ipsToRemove[] = $oldIpV6;
|
||||
}
|
||||
|
||||
// Se o servidor era ativo e agora está inativo, remove os IPs
|
||||
if ($oldStatus === 'active' && $_POST['status'] === 'inactive') {
|
||||
$pangolinService->removeServerIps([$_POST['ip_v4'], $_POST['ip_v6'] ?? null]);
|
||||
}
|
||||
// Se o servidor era inativo e agora está ativo, adiciona os IPs
|
||||
elseif ($oldStatus === 'inactive' && $_POST['status'] === 'active') {
|
||||
if ($client && $client['status'] === 'active') {
|
||||
$pangolinService->syncClient($_POST['client_id'], 'add');
|
||||
}
|
||||
}
|
||||
// Se os IPs mudaram
|
||||
elseif (!empty($ipsToRemove) && $_POST['status'] === 'active') {
|
||||
$pangolinService->removeServerIps($ipsToRemove);
|
||||
if ($client && $client['status'] === 'active') {
|
||||
$pangolinService->syncClient($_POST['client_id'], 'add');
|
||||
}
|
||||
}
|
||||
|
||||
// Se o cliente mudou, sincroniza o cliente antigo também
|
||||
if ($oldClientId != $_POST['client_id']) {
|
||||
$oldClient = $clientModel->find($oldClientId);
|
||||
if ($oldClient && $oldClient['status'] === 'active') {
|
||||
$pangolinService->removeServerIps([$oldIpV4, $oldIpV6]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Pangolin Sync Error (Server Update): " . $e->getMessage());
|
||||
}
|
||||
|
||||
View::redirect('/admin/servers');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$serverModel = new Server();
|
||||
$server = $serverModel->find($id);
|
||||
|
||||
if (!$server) {
|
||||
View::redirect('/admin/servers');
|
||||
return;
|
||||
}
|
||||
|
||||
// Capturar IPs antes de deletar
|
||||
$ipsToRemove = [$server['ip_v4']];
|
||||
if (!empty($server['ip_v6'])) {
|
||||
$ipsToRemove[] = $server['ip_v6'];
|
||||
}
|
||||
|
||||
// Pangolin Integration - Remove os IPs do servidor antes de deletá-lo
|
||||
try {
|
||||
$pangolinService = new \App\Services\PangolinService();
|
||||
if ($pangolinService->isEnabled() && $server['status'] === 'active') {
|
||||
$clientModel = new Client();
|
||||
$client = $clientModel->find($server['client_id']);
|
||||
if ($client && $client['status'] === 'active') {
|
||||
$pangolinService->removeServerIps($ipsToRemove);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Pangolin Sync Error (Server Delete): " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Delete the server
|
||||
$serverModel->delete($id);
|
||||
|
||||
View::redirect('/admin/servers');
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,13 @@ class ApiMiddleware
|
||||
try {
|
||||
$conn = \App\Config\Database::getInstance()->getConnection();
|
||||
|
||||
// Find server by Serial Key
|
||||
$stmt = $conn->prepare("SELECT id, machine_id, status, ip_v4 FROM servers WHERE serial_key = :serial");
|
||||
// Find server by Serial Key and join with client to check both statuses
|
||||
$stmt = $conn->prepare("
|
||||
SELECT s.id, s.machine_id, s.status, s.ip_v4, s.client_id, c.status as client_status
|
||||
FROM servers s
|
||||
JOIN clients c ON s.client_id = c.id
|
||||
WHERE s.serial_key = :serial
|
||||
");
|
||||
$stmt->execute(['serial' => $serialKey]);
|
||||
$server = $stmt->fetch();
|
||||
|
||||
@@ -61,6 +66,13 @@ class ApiMiddleware
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if client is active
|
||||
if ($server['client_status'] !== 'active') {
|
||||
$this->log($server['id'], 'auth_blocked', 'Client is inactive');
|
||||
View::json(['error' => 'Client is inactive'], 403);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bind Machine ID if first use
|
||||
if (empty($server['machine_id'])) {
|
||||
$update = $conn->prepare("UPDATE servers SET machine_id = :mid WHERE id = :id");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
284
app/Services/PangolinService.php
Normal file
284
app/Services/PangolinService.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user