Introducción: ¿Por qué usar proxies en PHP?
Cuando desarrollas aplicaciones PHP que realizan scraping, consumen APIs de terceros o automatizan tareas, inevitablemente te enfrentarás a bloqueos de IP, rate limits y restricciones geográficas. Los proxies HTTP resuelven estos problemas actuando como intermediarios entre tu aplicación y los servidores destino.
Como desarrollador PHP, tienes múltiples herramientas a tu disposición: desde la extensión cURL nativa hasta clientes HTTP modernos como Guzzle y Symfony HTTP Client. Esta guía te mostrará cómo implementar proxies en cada uno de estos escenarios, con código listo para producción.
Los casos de uso más comunes incluyen:
- Web scraping: distribuir solicitudes entre múltiples IPs para evitar bloqueos
- Integraciones de APIs: mantener sesiones estables con IPs confiables
- Testing geolocalizado: simular usuarios desde diferentes países o ciudades
- Automatización: ejecutar jobs en cola con rotación automática de proxies
cURL nativo: configuración básica de proxy
La extensión cURL de PHP es la forma más directa de realizar solicitudes HTTP con soporte para proxies. Los parámetros clave son CURLOPT_PROXY para el hostname y CURLOPT_PROXYUSERPWD para las credenciales de autenticación.
Veamos un ejemplo completo con manejo de errores y timeouts:
<?php
function fetchWithProxy(string $url, string $username, string $password): ?string
{
$ch = curl_init();
// Configuración del proxy ProxyHat
$proxyHost = 'gate.proxyhat.com';
$proxyPort = 8080;
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $proxyHost,
CURLOPT_PROXYPORT => $proxyPort,
CURLOPT_PROXYUSERPWD => "{$username}:{$password}",
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
// Opciones de respuesta
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
// Timeouts críticos para producción
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 30,
// TLS/SSL
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
// Headers para parecer un navegador real
CURLOPT_HTTPHEADER => [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: es-ES,es;q=0.8,en;q=0.5',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($error) {
error_log("cURL Error [{$errno}]: {$error}");
return null;
}
if ($httpCode >= 400) {
error_log("HTTP Error: {$httpCode} para URL {$url}");
return null;
}
return $response;
}
// Uso con proxy residencial de ProxyHat
$username = 'user-country-US-sid-abc123';
$password = 'tu_password';
$html = fetchWithProxy('https://httpbin.org/ip', $username, $password);
if ($html) {
echo "Respuesta recibida: " . $html . PHP_EOL;
}
Este patrón es ideal para scripts CLI, tareas cron o integraciones simples. La función devuelve null en caso de error, permitiendo reintentos con diferentes proxies.
Geo-targeting con cURL
Para simular solicitudes desde países específicos, incluye el código de país en el nombre de usuario:
<?php
function createGeoProxyUrl(string $country, string $region = null, string $city = null, string $session = null): array
{
$username = 'user';
if ($country) {
$username .= "-country-{$country}";
}
if ($region && $city) {
$username .= "-region-{$region}-city-{$city}";
} elseif ($region) {
$username .= "-region-{$region}";
}
if ($session) {
$username .= "-sid-{$session}";
}
return [
'host' => 'gate.proxyhat.com',
'port' => 8080,
'user' => $username,
'pass' => 'tu_password',
];
}
// Solicitar desde Berlín, Alemania
$proxy = createGeoProxyUrl('DE', 'state_of_berlin', 'berlin', 'xyz789');
echo "Proxy configurado para: {$proxy['user']}@{$proxy['host']}:{$proxy['port']}" . PHP_EOL;
Guzzle: cliente HTTP moderno con soporte de proxy
Guzzle es el cliente HTTP más popular en el ecosistema PHP. Ofrece una API limpia, manejo de promesas y middleware para casos avanzados. La configuración de proxy se pasa en el array de opciones de cada solicitud.
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
class ProxyScraper
{
private Client $client;
private array $proxyConfig;
private int $retryCount = 3;
public function __construct(string $username, string $password)
{
$this->proxyConfig = [
'proxy' => [
'http' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
'https' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
],
'timeout' => 30,
'connect_timeout' => 15,
'verify' => '/etc/ssl/certs/ca-certificates.crt',
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
],
];
$this->client = new Client($this->proxyConfig);
}
public function fetch(string $url, array $options = []): ?string
{
$attempts = 0;
$lastException = null;
while ($attempts < $this->retryCount) {
try {
$response = $this->client->get($url, $options);
return (string) $response->getBody();
} catch (RequestException $e) {
$lastException = $e;
$attempts++;
// Rotar proxy en cada reintento
$newSession = bin2hex(random_bytes(8));
$this->rotateProxy($newSession);
usleep(500000 * $attempts); // Backoff exponencial
}
}
error_log("Guzzle error después de {$attempts} intentos: " . $lastException?->getMessage());
return null;
}
private function rotateProxy(string $sessionId): void
{
// Reconstruir cliente con nueva sesión
$username = "user-country-US-sid-{$sessionId}";
$this->proxyConfig['proxy'] = [
'http' => "http://{$username}:tu_password@gate.proxyhat.com:8080",
'https' => "http://{$username}:tu_password@gate.proxyhat.com:8080",
];
$this->client = new Client($this->proxyConfig);
}
public function fetchJson(string $url): ?array
{
$body = $this->fetch($url, [
'headers' => ['Accept' => 'application/json'],
]);
return $body ? json_decode($body, true) : null;
}
}
// Uso del scraper
$scraper = new ProxyScraper('user-country-ES', 'tu_password');
// Obtener IP visible
$ipData = $scraper->fetchJson('https://httpbin.org/ip');
if ($ipData) {
echo "IP visible: " . $ipData['origin'] . PHP_EOL;
}
// Scraping de página HTML
$html = $scraper->fetch('https://example.com/product/123');
if ($html) {
// Procesar con DOMDocument o librería similar
$dom = new DOMDocument();
@$dom->loadHTML($html);
echo "Página cargada correctamente" . PHP_EOL;
}
Middleware para logging y métricas
<?php
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\MessageFormatter;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
function createLoggedClient(array $proxyConfig): Client
{
$logger = new Logger('proxy-requests');
$logger->pushHandler(new StreamHandler('logs/proxy.log', Logger::DEBUG));
$stack = HandlerStack::create();
// Middleware de logging
$stack->push(
Middleware::log(
$logger,
new MessageFormatter('{method} {uri} - {code} - {req_header_User-Agent}')
)
);
// Middleware para métricas de tiempo
$stack->push(Middleware::timer());
return new Client(array_merge($proxyConfig, [
'handler' => $stack,
'on_stats' => function (GuzzleHttp\TransferStats $stats) {
error_log(sprintf(
'Tiempo total: %.2fs - DNS: %.2fs - Connect: %.2fs',
$stats->getTransferTime(),
$stats->getHandlerStat('namelookup_time'),
$stats->getHandlerStat('connect_time')
));
},
]));
}
Symfony HTTP Client: asíncrono y alto rendimiento
Symfony HTTP Client ofrece soporte nativo para solicitudes asíncronas y concurrentes, ideal para scraping a escala. Su API de responses asíncronas permite procesar múltiples solicitudes en paralelo sin bloquear.
<?php
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class AsyncProxyClient
{
private $client;
private string $proxyUrl;
public function __construct(string $username, string $password)
{
$this->proxyUrl = "http://{$username}:{$password}@gate.proxyhat.com:8080";
$this->client = HttpClient::create([
'proxy' => $this->proxyUrl,
'timeout' => 30,
'max_redirects' => 5,
'verify_peer' => true,
'verify_host' => true,
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
}
public function fetchConcurrent(array $urls, callable $onSuccess, callable $onError): void
{
$responses = [];
// Iniciar todas las solicitudes sin bloquear
foreach ($urls as $url) {
try {
$responses[$url] = $this->client->request('GET', $url);
} catch (TransportExceptionInterface $e) {
$onError($url, $e->getMessage());
}
}
// Procesar respuestas a medida que llegan
foreach ($responses as $url => $response) {
try {
$statusCode = $response->getStatusCode();
if ($statusCode >= 200 && $statusCode < 300) {
$content = $response->getContent();
$onSuccess($url, $content);
} else {
$onError($url, "HTTP {$statusCode}");
}
} catch (TransportExceptionInterface $e) {
$onError($url, $e->getMessage());
}
}
}
public function fetchWithRetry(string $url, int $maxRetries = 3): ?string
{
$attempt = 0;
while ($attempt < $maxRetries) {
try {
$response = $this->client->request('GET', $url);
if ($response->getStatusCode() === 200) {
return $response->getContent();
}
} catch (TransportExceptionInterface $e) {
$attempt++;
usleep(1000000 * $attempt); // Backoff
}
}
return null;
}
}
// Uso con procesamiento concurrente
$asyncClient = new AsyncProxyClient('user-country-FR', 'tu_password');
$urls = [
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/user-agent',
];
$asyncClient->fetchConcurrent(
$urls,
function (string $url, string $content) {
echo "✓ {$url} - " . strlen($content) . " bytes" . PHP_EOL;
},
function (string $url, string $error) {
echo "✗ {$url} - Error: {$error}" . PHP_EOL;
}
);
Laravel: servicio de proxy para jobs en cola
En aplicaciones Laravel, es común ejecutar scraping como jobs en cola. Un servicio dedicado encapsula la lógica del proxy y permite inyección de dependencias.
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class ResidentialProxyService
{
private Client $client;
private string $baseUsername;
private string $password;
private string $gateway = 'gate.proxyhat.com';
private int $port = 8080;
// Pool de países disponibles
private array $countryPool = ['US', 'ES', 'DE', 'FR', 'GB', 'IT'];
public function __construct()
{
$this->baseUsername = config('proxyhat.username', 'user');
$this->password = config('proxyhat.password', '');
$this->client = new Client([
'timeout' => config('proxyhat.timeout', 30),
'connect_timeout' => config('proxyhat.connect_timeout', 15),
'verify' => config('proxyhat.ca_bundle', '/etc/ssl/certs/ca-certificates.crt'),
]);
}
/**
* Construye URL de proxy con opciones de geo-targeting
*/
private function buildProxyUrl(?string $country = null, ?string $session = null): string
{
$username = $this->baseUsername;
if ($country) {
$username .= "-country-{$country}";
}
if ($session) {
$username .= "-sid-{$session}";
}
return "http://{$username}:{$this->password}@{$this->gateway}:{$this->port}";
}
/**
* Obtiene una sesión sticky (misma IP para múltiples requests)
*/
public function getStickySession(?string $country = null): array
{
$sessionId = bin2hex(random_bytes(8));
$proxyUrl = $this->buildProxyUrl($country, $sessionId);
return [
'session_id' => $sessionId,
'proxy_url' => $proxyUrl,
'expires_at' => now()->addMinutes(30),
];
}
/**
* Realiza solicitud con rotación automática en caso de error
*/
public function request(string $method, string $url, array $options = [], int $maxRetries = 3): ?string
{
$attempt = 0;
$lastError = null;
while ($attempt < $maxRetries) {
$country = $this->countryPool[array_rand($this->countryPool)];
$proxyUrl = $this->buildProxyUrl($country);
try {
$response = $this->client->request($method, $url, array_merge($options, [
'proxy' => $proxyUrl,
'headers' => array_merge([
'User-Agent' => config('proxyhat.user_agent', 'ProxyHat-Scraper/1.0'),
], $options['headers'] ?? []),
]));
Log::info('Proxy request successful', [
'url' => $url,
'country' => $country,
'status' => $response->getStatusCode(),
]);
return (string) $response->getBody();
} catch (RequestException $e) {
$lastError = $e->getMessage();
$attempt++;
Log::warning('Proxy request failed, retrying', [
'url' => $url,
'attempt' => $attempt,
'error' => $lastError,
]);
usleep(500000 * $attempt);
}
}
Log::error('Proxy request failed after retries', [
'url' => $url,
'attempts' => $attempt,
'error' => $lastError,
]);
return null;
}
/**
* Verifica disponibilidad del proxy
*/
public function healthCheck(): bool
{
try {
$response = $this->client->get('https://httpbin.org/status/200', [
'proxy' => $this->buildProxyUrl(),
'timeout' => 10,
]);
return $response->getStatusCode() === 200;
} catch (\Exception $e) {
return false;
}
}
}
Ahora el Job que utiliza este servicio:
<?php
namespace App\Jobs;
use App\Services\ResidentialProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ScrapeProductJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
private string $productUrl;
private ?string $country;
public function __construct(string $productUrl, ?string $country = null)
{
$this->productUrl = $productUrl;
$this->country = $country;
}
public function handle(ResidentialProxyService $proxyService): void
{
// Obtener sesión sticky para mantener IP consistente
$session = $proxyService->getStickySession($this->country);
Log::info('Starting scrape job', [
'url' => $this->productUrl,
'session' => $session['session_id'],
]);
// Primera solicitud: obtener página del producto
$html = $proxyService->request('GET', $this->productUrl, [
'proxy' => $session['proxy_url'],
]);
if (!$html) {
$this->fail(new \Exception('Failed to fetch product page'));
return;
}
// Parsear HTML y extraer datos
$productData = $this->parseProduct($html);
// Segunda solicitud: verificar stock (usando misma sesión)
if (isset($productData['stockUrl'])) {
$stockData = $proxyService->request('GET', $productData['stockUrl'], [
'proxy' => $session['proxy_url'],
]);
$productData['stock'] = $this->parseStock($stockData);
}
// Guardar o procesar datos
$this->processProductData($productData);
Log::info('Scrape job completed', [
'product' => $productData['name'] ?? 'unknown',
]);
}
private function parseProduct(string $html): array
{
// Implementar parsing según estructura del sitio
$dom = new \DOMDocument();
@$dom->loadHTML($html);
$xpath = new \DOMXPath($dom);
return [
'name' => $xpath->query('//h1[@class="product-title"]')?->item(0)?->textContent,
'price' => $xpath->query('//span[@class="price"]')?->item(0)?->textContent,
'stockUrl' => $xpath->query('//a[@data-stock]')?->item(0)?->getAttribute('href'),
];
}
private function parseStock(?string $json): array
{
if (!$json) return [];
$data = json_decode($json, true);
return [
'available' => $data['available'] ?? false,
'quantity' => $data['quantity'] ?? 0,
];
}
private function processProductData(array $data): void
{
// Guardar en base de datos o enviar a otro servicio
// \App\Models\Product::updateOrCreate(['url' => $this->productUrl], $data);
}
}
// Despachar el job
ScrapeProductJob::dispatch('https://example.com/product/123', 'US')
->onQueue('scraping');
multi_curl: concurrencia máxima para scraping
Cuando necesitas realizar cientos de solicitudes simultáneas, curl_multi_* ofrece el mejor rendimiento. Este enfoque gestiona múltiples handles cURL en paralelo sin bloquear.
<?php
class ConcurrentProxyScraper
{
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
private string $username;
private string $password;
private int $maxConnections = 50;
private int $timeout = 30;
public function __construct(string $username, string $password)
{
$this->username = $username;
$this->password = $password;
}
/**
* Fetch múltiple URLs concurrentemente
*/
public function fetchMultiple(array $urls, ?callable $processor = null): array
{
$results = [];
$handles = [];
$mh = curl_multi_init();
// Crear handles para cada URL (hasta maxConnections)
$chunks = array_chunk($urls, $this->maxConnections);
foreach ($chunks as $chunk) {
$results = array_merge($results, $this->processChunk($chunk, $processor));
}
return $results;
}
private function processChunk(array $urls, ?callable $processor): array
{
$results = [];
$handles = [];
$mh = curl_multi_init();
// Configurar cada handle
foreach ($urls as $i => $url) {
$ch = curl_init();
// Generar sesión única para cada solicitud (rotación)
$session = bin2hex(random_bytes(4));
$userWithSession = "{$this->username}-sid-{$session}";
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $this->proxyHost,
CURLOPT_PROXYPORT => $this->proxyPort,
CURLOPT_PROXYUSERPWD => "{$userWithSession}:{$this->password}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
CURLOPT_HTTPHEADER => [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
curl_multi_add_handle($mh, $ch);
$handles[$i] = ['handle' => $ch, 'url' => $url];
}
// Ejecutar todas las solicitudes
$active = null;
do {
$status = curl_multi_exec($mh, $active);
if ($status === CURLM_CALL_MULTI_PERFORM) {
continue;
}
// Esperar actividad
if ($status === CURLM_OK) {
curl_multi_select($mh, 1.0);
}
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
// Recoger resultados
foreach ($handles as $i => $data) {
$ch = $data['handle'];
$url = $data['url'];
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$content = curl_multi_getcontent($ch);
if ($error) {
$results[$url] = [
'success' => false,
'error' => $error,
'code' => $httpCode,
];
} else {
$results[$url] = [
'success' => true,
'content' => $processor ? $processor($content) : $content,
'code' => $httpCode,
];
}
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
}
// Uso para scraping masivo
$scraper = new ConcurrentProxyScraper('user-country-US', 'tu_password');
$urls = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
// ... cientos de URLs
];
$results = $scraper->fetchMultiple($urls, function ($html) {
// Procesar HTML extraído
$dom = new DOMDocument();
@$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
$titles = [];
foreach ($xpath->query('//h2[@class="title"]') as $node) {
$titles[] = trim($node->textContent);
}
return $titles;
});
// Contar éxitos y fallos
$successCount = count(array_filter($results, fn($r) => $r['success']));
echo "Exitosos: {$successCount} / " . count($results) . PHP_EOL;
TLS/SSL y manejo de certificados CA
Las conexiones HTTPS a través de proxies requieren verificación correcta de certificados. Configurar incorrectamente SSL puede exponer tu aplicación a ataques man-in-the-middle.
Configuración correcta de CA bundle
<?php
class TlsProxyClient
{
private string $caBundlePath;
private array $proxyConfig;
public function __construct(array $proxyConfig)
{
$this->proxyConfig = $proxyConfig;
$this->caBundlePath = $this->detectCaBundle();
}
/**
* Detecta el path correcto del CA bundle según el sistema
*/
private function detectCaBundle(): string
{
// Opciones comunes según sistema operativo
$candidates = [
// Linux (Debian/Ubuntu)
'/etc/ssl/certs/ca-certificates.crt',
// Linux (RHEL/CentOS)
'/etc/pki/tls/certs/ca-bundle.crt',
// macOS (Homebrew)
'/usr/local/etc/openssl/cert.pem',
'/usr/local/etc/openssl@1.1/cert.pem',
// Windows (XAMPP)
'C:\xampp\apache\conf\extra\cacert.pem',
// Composer CA bundle (fallback)
__DIR__ . '/../../../composer/ca-bundle/res/cacert.pem',
];
foreach ($candidates as $path) {
if (file_exists($path) && is_readable($path)) {
return $path;
}
}
// Fallback: usar el bundle de Composer
$composerBundle = $this->downloadComposerCaBundle();
return $composerBundle;
}
private function downloadComposerCaBundle(): string
{
$cachePath = sys_get_temp_dir() . '/cacert.pem';
if (!file_exists($cachePath) || filemtime($cachePath) < strtotime('-30 days')) {
$url = 'https://curl.se/ca/cacert.pem';
$content = file_get_contents($url);
if ($content === false) {
throw new RuntimeException('Cannot download CA bundle');
}
file_put_contents($cachePath, $content);
}
return $cachePath;
}
/**
* Verifica la validez del certificado del servidor destino
*/
public function verifyPeer(string $url): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $this->proxyConfig['host'],
CURLOPT_PROXYPORT => $this->proxyConfig['port'],
CURLOPT_PROXYUSERPWD => $this->proxyConfig['auth'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $this->caBundlePath,
CURLOPT_CERTINFO => true,
CURLOPT_VERBOSE => true,
]);
$result = curl_exec($ch);
$info = curl_getinfo($ch);
$error = curl_error($ch);
curl_close($ch);
return [
'success' => $result !== false,
'ssl_verify_result' => $info['ssl_verify_result'],
'certinfo' => $info['certinfo'] ?? [],
'error' => $error,
'ca_bundle' => $this->caBundlePath,
];
}
/**
* Cliente HTTP con TLS estricto
*/
public function createSecureClient(): GuzzleHttp\Client
{
return new GuzzleHttp\Client([
'proxy' => $this->buildProxyUrl(),
'timeout' => 30,
'verify' => $this->caBundlePath,
'version' => 2.0, // HTTP/2 si está disponible
'config' => [
'curl' => [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
],
],
]);
}
private function buildProxyUrl(): string
{
return sprintf(
'http://%s:%s@%s:%d',
$this->proxyConfig['username'],
$this->proxyConfig['password'],
$this->proxyConfig['host'],
$this->proxyConfig['port']
);
}
}
// Verificar configuración TLS
$client = new TlsProxyClient([
'host' => 'gate.proxyhat.com',
'port' => 8080,
'username' => 'user-country-US',
'password' => 'tu_password',
'auth' => 'user-country-US:tu_password',
]);
$verification = $client->verifyPeer('https://example.com');
echo "CA Bundle: " . $verification['ca_bundle'] . PHP_EOL;
echo "SSL Verify Result: " . $verification['ssl_verify_result'] . PHP_EOL;
echo "Success: " . ($verification['success'] ? 'Yes' : 'No') . PHP_EOL;
if (!$verification['success']) {
echo "Error: " . $verification['error'] . PHP_EOL;
}
Comparativa de clientes HTTP para proxies
| Característica | cURL nativo | Guzzle | Symfony HTTP Client |
|---|---|---|---|
| Complejidad | Alta | Media | Media |
| Concurrencia | multi_curl (manual) | Promise-based | Nativa asíncrona |
| Integración Laravel | Manual | Excelente | Buena |
| Middleware/Plugins | No | Sí (PSR-15) | Sí |
| Curva de aprendizaje | Baja | Media | Media |
| Mejor uso | Scripts simples | Aplicaciones completas | Scraping concurrente |
Buenas prácticas para producción
Regla de oro: Nunca desactives la verificación SSL (
CURLOPT_SSL_VERIFYPEER = false) en producción. Esto expone tu aplicación a ataques MITM y compromete la seguridad de los datos transmitidos.
- Rotación de sesiones: Genera IDs de sesión únicos para cada solicitud o grupo de solicitudes relacionadas.
- Backoff exponencial: Implementa reintentos con delays crecientes (1s, 2s, 4s...) para evitar sobrecargar el proxy.
- Logging detallado: Registra tiempo de respuesta, código de estado y proxy usado para cada solicitud.
- Circuit breaker: Si un proxy falla repetidamente, márcalo como no disponible temporalmente.
- Rate limiting: Respeta los límites del sitio destino con delays entre solicitudes.






