Voltar para blog

Como criar um proxy inteligente para comparar servidores durante a migração

02/03/2026 · 4 min · Infraestrutura

Compartilhar

Quando você migra um site entre servidores, o maior risco não é o rsync. O maior risco é homologar o servidor novo sem trocar DNS público e sem depender que cada pessoa do time edite hosts manualmente.

Neste case, implementei uma camada de proxy em PHP para comparar ambiente legado (andamento) e ambiente novo (fortis) lado a lado, usando o mesmo host lógico da aplicação. O objetivo foi validar comportamento real de VirtualHost, sessão, formulário e recursos estáticos antes do cutover.

1) Problema real de migração que eu precisava resolver

URL de produção estava funcionando, mas o ambiente novo ainda não podia receber tráfego público. Eu precisava:

Esse é o ponto onde o proxy inteligente entra: ele conecta no IP alvo, mas força Host: dominio.com no cabeçalho para o Apache responder o vhost correto.

2) Arquitetura aplicada (dev.php + proxy.php + config.php)

Estrutura implementada:

Fluxo:

  1. operador informa domínio + caminho;
  2. dev.php monta duas URLs para o proxy;
  3. proxy.php resolve servidor alvo por alias;
  4. cURL conecta por IP e envia Host do domínio informado;
  5. Apache do alvo escolhe o VirtualHost correto;
  6. conteúdo retorna no iframe correspondente.

3) Código completo de apoio

3.1 config.php

<?php
// Ambientes de comparação
const ANDAMENTO_IP = '10.10.10.11';
const FORTIS_IP    = '10.10.10.12';

// Opcional: portas diferentes por ambiente
const DEFAULT_HTTP_PORT  = 80;
const DEFAULT_HTTPS_PORT = 443;

3.2 dev.php (painel comparativo)

<?php
$uri = $_GET['uri'] ?? 'meudominio.com.br/';
$uri = trim($uri);
?>
<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Comparador de Migração</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 0; }
    .bar { padding: 12px; border-bottom: 1px solid #ddd; }
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: calc(100vh - 70px); }
    iframe { width: 100%; height: 100%; border: 0; }
    input { width: 70%; padding: 8px; }
    button { padding: 8px 12px; }
  </style>
</head>
<body>
  <div class="bar">
    <form method="get">
      <label>URI (domínio + caminho):</label>
      <input name="uri" value="<?= htmlspecialchars($uri, ENT_QUOTES, 'UTF-8') ?>">
      <button type="submit">Comparar</button>
    </form>
  </div>

  <div class="grid">
    <iframe src="proxy.php?server=andamento&uri=<?= urlencode($uri) ?>"></iframe>
    <iframe src="proxy.php?server=fortis&uri=<?= urlencode($uri) ?>"></iframe>
  </div>
</body>
</html>

3.3 proxy.php (versão de campo)

<?php
require 'config.php';

header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');

$server = $_GET['server'] ?? '';
$uri    = $_GET['uri'] ?? '';

$servers = [
    'andamento' => ['ip' => ANDAMENTO_IP, 'http_port' => DEFAULT_HTTP_PORT, 'https_port' => DEFAULT_HTTPS_PORT],
    'fortis'    => ['ip' => FORTIS_IP,    'http_port' => DEFAULT_HTTP_PORT, 'https_port' => DEFAULT_HTTPS_PORT],
];

if (!isset($servers[$server]) || !preg_match('/^[\w\-\.\/\?\%\=\&\#\:]+$/', $uri)) {
    http_response_code(400);
    die('Parâmetros inválidos');
}

$srv = $servers[$server];
$uri = trim($uri);

if (strpos($uri, '://') === false) {
    $uri = 'http://' . $uri;
}

$parsed = parse_url($uri);
$domain = $parsed['host'] ?? '';
$path   = $parsed['path'] ?? '/';

if (!empty($parsed['query'])) {
    $path .= '?' . $parsed['query'];
}

if ($domain === '') {
    http_response_code(400);
    die('Domínio inválido');
}

if ($path === '') {
    $path = '/';
}

function executeCurl(string $url, string $domain): array
{
    $ch = curl_init();

    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_USERAGENT => 'migration-proxy/1.0',
        CURLOPT_HTTPHEADER => [
            'Host: ' . $domain,
            'Accept: */*',
            'Accept-Encoding: gzip, deflate',
            'Connection: keep-alive',
            'Cache-Control: no-cache',
            'Pragma: no-cache',
        ],
        CURLOPT_ENCODING => 'gzip, deflate',
        CURLOPT_HEADER => false,
    ]);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    $errno    = curl_errno($ch);
    $code     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $type     = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'text/html';
    $info     = curl_getinfo($ch);

    curl_close($ch);

    return [
        'response' => $response,
        'error' => $error,
        'errno' => $errno,
        'code' => $code,
        'type' => $type,
        'info' => $info,
    ];
}

// 1) tenta HTTP primeiro
$target = 'http://' . $srv['ip'] . ':' . $srv['http_port'] . $path;
$result = executeCurl($target, $domain);

// 2) fallback HTTPS se erro ou HTTP >= 400
if ($result['errno'] !== 0 || $result['code'] >= 400) {
    $target = 'https://' . $srv['ip'] . ':' . $srv['https_port'] . $path;
    $result = executeCurl($target, $domain);
}

if ($result['response'] === false || $result['response'] === null) {
    http_response_code(502);
    header('Content-Type: text/plain; charset=utf-8');
    echo "Proxy falhou ao buscar conteúdo\n";
    echo "target: {$target}\n";
    echo "curl_errno: {$result['errno']}\n";
    echo "curl_error: {$result['error']}\n";
    exit;
}

$response    = $result['response'];
$contentType = $result['type'];

// texto puro (ex.: teste.txt)
if (stripos($contentType, 'text/plain') !== false) {
    header('Content-Type: text/plain; charset=utf-8');
    echo $response;
    exit;
}

// corrige assets relativos de CSS/JS/imagens
$base = 'http://' . $srv['ip'] . '/';
$response = str_replace('<head>', '<head><base href="' . $base . '">', $response);

header('Content-Type: text/html; charset=utf-8');
echo $response;

4) Comandos de validação que usei no troubleshooting

Antes de culpar código, validei virtual host e rota HTTP/HTTPS no backend.

4.1 Validar vhost Apache

apachectl -S
apachectl -t

4.2 Validar resposta do backend forçando Host no curl

# Testa servidor andamento
curl -sv "http://10.10.10.11/" -H "Host: meudominio.com.br" -o /dev/null

# Testa servidor fortis
curl -sv "http://10.10.10.12/" -H "Host: meudominio.com.br" -o /dev/null

4.3 Validar arquivo sentinela criado só no servidor novo

curl -sv "http://10.10.10.12/teste.txt" -H "Host: meudominio.com.br"

Se o teste.txt não aparece no painel do proxy para o novo host, o problema não é DNS público: é mapeamento de vhost, docroot ou regra local no backend.

5) Erros reais que peguei e como corrigi

5.1 "Webserver is functioning normally"

Sintoma: servidor novo devolvia página default do Apache.

Causa raiz: request chegou por IP, mas o Apache não encontrou ServerName/ ServerAlias compatível com domínio enviado no Host.

Correção:

5.2 Assets quebrados (CSS/JS/imagem)

Sintoma: HTML abriu, mas links de assets iam para domínio público e não para o backend de homologação.

Correção:

5.3 Texto puro com comportamento errado

Sintoma: endpoint /teste.txt retornava como HTML.

Correção:

6) Configuração Apache obrigatória no host de destino

<VirtualHost *:80>
    ServerName meudominio.com.br
    ServerAlias www.meudominio.com.br
    DocumentRoot /var/www/meudominio

    <Directory /var/www/meudominio>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog  /var/log/apache2/meudominio-error.log
    CustomLog /var/log/apache2/meudominio-access.log combined
</VirtualHost>

Comandos pós-ajuste:

apachectl -t && systemctl reload apache2
# em RHEL/CloudLinux
# apachectl -t && systemctl reload httpd

7) Hardening do painel de comparação (não deixar aberto)

Esse tipo de ferramenta é para janela de migração controlada.

Controles que apliquei:

CURLOPT_SSL_VERIFYPEER=false e CURLOPT_SSL_VERIFYHOST=false foram usados somente na homologação com TLS não finalizado. No ambiente final, o certo é ativar validação SSL completa.

8) Runbook de homologação antes do cutover DNS

  1. criar endpoint sentinela apenas no host novo (/teste.txt);
  2. validar carregamento legado vs novo no painel comparativo;
  3. validar login, sessão e logout;
  4. validar formulários críticos (cadastro, contato, checkout);
  5. validar uploads/downloads;
  6. validar redirecionamentos 301/302 e páginas 404;
  7. validar assets e dependências JS/CSS;
  8. registrar divergências e corrigir no host novo;
  9. repetir ciclo até paridade funcional;
  10. só então aprovar alteração DNS.

9) Resultado operacional

Com o proxy inteligente, a homologação deixou de depender de ajuste manual em estação de trabalho e passou a ser centralizada, reproduzível e auditável.

O cutover DNS foi executado com evidência técnica de paridade funcional, não por intuição. Esse é o ponto que reduz incidente pós-migração.

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.