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()); } } }