/
home
/
suroeste
/
public_html
/
payments.transportessuroeste.com
/
src
/
Controllers
/
/home/suroeste/public_html/payments.transportessuroeste.com/src/Controllers
mkdir
upload
Name
Size
Mode
Actions
ApiController.php
10921
0644
edit
dl
rm
Edit:
/home/suroeste/public_html/payments.transportessuroeste.com/src/Controllers/ApiController.php
(10921B)
<?php /** * ============================================================================ * API CONTROLLER - Controlador Principal * ============================================================================ */ namespace TransportesSuroeste\Controllers; use TransportesSuroeste\Services\PaymentService; use TransportesSuroeste\Services\LogService; use TransportesSuroeste\Validators\Validator; use TransportesSuroeste\Middleware\SecurityMiddleware; use TransportesSuroeste\Exceptions\ApiException; use TransportesSuroeste\Exceptions\SecurityException; class ApiController { private PaymentService $paymentService; private ?array $client = null; // IPs oficiales de ePayco (rangos AWS) private const EPAYCO_IP_RANGES = ['52.', '18.', '34.', '54.', '3.']; public function __construct() { $this->paymentService = new PaymentService(); } public function setClient(array $client): void { $this->client = $client; } /** * POST /api/v1/payments - Crear nueva transacción */ public function createPayment(): array { $data = $this->getJsonInput(); $data = SecurityMiddleware::sanitizeArray($data); $validated = Validator::validatePayment($data); $result = $this->paymentService->createPayment($validated, $this->client); LogService::info('access', 'Payment created', [ 'transaction_uuid' => $result['transaction_uuid'], 'client' => $this->client['name'] ]); return $this->success($result, 'Transacción creada exitosamente', 201); } /** * GET /api/v1/payments/{uuid} - Consultar transacción */ public function getPayment(string $uuid): array { $result = $this->paymentService->getTransactionStatus($uuid, 'uuid'); if (!$result) { throw new ApiException('Transacción no encontrada', 404); } return $this->success($result); } /** * GET /api/v1/payments/ticket/{reference} - Consultar por ticket */ public function getPaymentByTicket(string $reference): array { $result = $this->paymentService->getTransactionStatus($reference, 'ticket'); if (!$result) { throw new ApiException('Transacción no encontrada', 404); } return $this->success($result); } /** * GET /api/v1/banks/pse - Obtener bancos PSE */ public function getPseBanks(): array { $banks = $this->paymentService->getPseBanks(); return $this->success([ 'banks' => $banks, 'total' => count($banks) ], 'Bancos PSE obtenidos'); } /** * POST /api/v1/webhook/epayco - Webhook de ePayco (SEGURO) * GET/POST /api/v1/callback - Callback de ePayco (SEGURO) * * Validaciones: * 1. Método HTTP (GET/POST) * 2. Campos requeridos de ePayco * 3. IP de origen (producción) * 4. Firma SHA256 de ePayco * 5. Lista blanca de campos * 6. Sanitización de valores */ public function handleCallback(): array { $clientIp = LogService::getClientIp(); // 1. Validar método HTTP $method = $_SERVER['REQUEST_METHOD']; if (!in_array($method, ['GET', 'POST'])) { LogService::security('webhook_blocked', 'Método no permitido', [ 'method' => $method, 'ip' => $clientIp ]); throw new SecurityException('Método no permitido', 405); } // 2. Obtener datos según método $data = $method === 'POST' ? array_merge($_POST, $this->getJsonInput()) : $_GET; // 3. Validar que hay datos if (empty($data)) { LogService::security('webhook_blocked', 'Datos vacíos', ['ip' => $clientIp]); throw new ApiException('Datos requeridos', 400); } // 4. Validar campos requeridos de ePayco $requiredFields = ['x_ref_payco', 'x_transaction_id', 'x_response', 'x_amount', 'x_signature']; $missingFields = []; foreach ($requiredFields as $field) { if (!isset($data[$field]) || $data[$field] === '') { $missingFields[] = $field; } } if (!empty($missingFields)) { LogService::security('webhook_blocked', 'Campos faltantes', [ 'missing' => $missingFields, 'ip' => $clientIp ]); throw new ApiException('Campos requeridos: ' . implode(', ', $missingFields), 400); } // 5. Validar IP de origen (solo en producción) if (!EPAYCO_CONFIG['test_mode']) { if (!$this->isValidEpaycoIp($clientIp)) { LogService::security('webhook_blocked', 'IP no autorizada', [ 'ip' => $clientIp, 'ref_payco' => $data['x_ref_payco'] ]); throw new SecurityException('Origen no autorizado', 403); } } // 6. Validar firma de ePayco if (!$this->validateEpaycoSignature($data)) { LogService::security('webhook_blocked', 'Firma inválida', [ 'ref_payco' => $data['x_ref_payco'], 'ip' => $clientIp, 'signature' => substr($data['x_signature'] ?? '', 0, 20) . '...' ]); throw new SecurityException('Firma inválida', 403); } // 7. Filtrar y sanitizar solo campos permitidos $data = $this->sanitizeEpaycoData($data); // 8. Log del webhook válido LogService::info('webhook_received', 'Webhook ePayco válido', [ 'ref_payco' => $data['x_ref_payco'], 'response' => $data['x_response'], 'amount' => $data['x_amount'], 'transaction_id' => $data['x_transaction_id'], 'ip' => $clientIp ]); // 9. Procesar el callback $result = $this->paymentService->processCallback($data); return $this->success($result, 'Webhook procesado'); } /** * Validar firma SHA256 de ePayco * Formato: hash('sha256', $p_cust_id_cliente.$p_key.$x_ref_payco.$x_transaction_id.$x_amount.$x_currency_code) */ private function validateEpaycoSignature(array $data): bool { // Si no hay P_KEY, omitir validación (solo desarrollo) if (empty(EPAYCO_CONFIG['p_key'])) { return true; } $signature = $data['x_signature'] ?? ''; if (empty($signature) || strlen($signature) !== 64) { return false; } // Obtener componentes $custId = $data['x_cust_id_cliente'] ?? ''; $pKey = EPAYCO_CONFIG['p_key']; $refPayco = $data['x_ref_payco'] ?? ''; $transactionId = $data['x_transaction_id'] ?? ''; $amount = $data['x_amount'] ?? ''; $currency = $data['x_currency_code'] ?? 'COP'; // Formato de firma ePayco $signatureString = $custId . '^' . $pKey . '^' . $refPayco . '^' . $transactionId . '^' . $amount . '^' . $currency; $expectedSignature = hash('sha256', $signatureString); return hash_equals($expectedSignature, $signature); } /** * Validar que la IP sea de ePayco (AWS) */ private function isValidEpaycoIp(string $ip): bool { // Permitir localhost en desarrollo if (in_array($ip, ['127.0.0.1', '::1', 'localhost'])) { return EPAYCO_CONFIG['test_mode']; } // Verificar rangos de AWS donde opera ePayco foreach (self::EPAYCO_IP_RANGES as $range) { if (strpos($ip, $range) === 0) { return true; } } return false; } /** * Sanitizar datos de ePayco con lista blanca de campos */ private function sanitizeEpaycoData(array $data): array { // Lista blanca de campos permitidos de ePayco $allowedFields = [ 'x_cust_id_cliente', 'x_ref_payco', 'x_id_factura', 'x_id_invoice', 'x_description', 'x_amount', 'x_amount_country', 'x_amount_ok', 'x_tax', 'x_amount_base', 'x_currency_code', 'x_bank_name', 'x_cardnumber', 'x_quotas', 'x_respuesta', 'x_response', 'x_approval_code', 'x_transaction_id', 'x_fecha_transaccion', 'x_transaction_date', 'x_cod_respuesta', 'x_cod_response', 'x_response_reason_text', 'x_errorcode', 'x_cod_transaction_state', 'x_transaction_state', 'x_franchise', 'x_business', 'x_customer_doctype', 'x_customer_document', 'x_customer_name', 'x_customer_lastname', 'x_customer_email', 'x_customer_phone', 'x_customer_movil', 'x_customer_ind_pais', 'x_customer_country', 'x_customer_city', 'x_customer_address', 'x_customer_ip', 'x_test_request', 'x_extra1', 'x_extra2', 'x_extra3', 'x_extra4', 'x_extra5', 'x_extra6', 'x_extra7', 'x_extra8', 'x_extra9', 'x_extra10', 'x_tax_ico', 'x_payment_date', 'x_signature', 'x_transaction_cycle', 'is_processable' ]; $sanitized = []; foreach ($data as $key => $value) { // Solo campos en lista blanca if (!in_array($key, $allowedFields)) { continue; } if (is_string($value)) { // Eliminar null bytes $value = str_replace(chr(0), '', $value); // Limitar longitud (prevenir DoS) $value = substr($value, 0, 500); // Trim espacios $value = trim($value); } $sanitized[$key] = $value; } return $sanitized; } /** * GET /api/v1/health - Health check */ public function healthCheck(): array { return $this->success([ 'status' => 'healthy', 'service' => APP_NAME, 'version' => APP_VERSION, 'timestamp' => date('Y-m-d H:i:s'), 'environment' => APP_ENV ]); } private function getJsonInput(): array { $input = file_get_contents('php://input'); if (empty($input)) return []; $data = json_decode($input, true); if (json_last_error() !== JSON_ERROR_NONE) { return []; } return $data ?? []; } private function success($data, string $message = 'OK', int $code = 200): array { return [ 'success' => true, 'message' => $message, 'data' => $data, 'meta' => [ 'timestamp' => date('Y-m-d\TH:i:s.uP'), 'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8)) ] ]; } }
Save
cmd:
run