Proxy HTTP em Zig — Tutorial Passo a Passo

Proxy HTTP em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um proxy HTTP forward em Zig que intercepta requisições dos clientes, as encaminha ao servidor destino e retorna a resposta. O proxy inclui logging, filtragem de domínios e reescrita de headers.

O Que Vamos Construir

Nosso proxy HTTP vai:

  • Aceitar conexões de clientes locais
  • Parsear requisições HTTP para extrair o host destino
  • Encaminhar a requisição ao servidor real
  • Retornar a resposta ao cliente
  • Logar todas as requisições (método, URL, status, tempo)
  • Suportar lista de bloqueio de domínios

Por Que Este Projeto?

Proxies são componentes fundamentais da infraestrutura web. Construir um nos ensina sobre o protocolo HTTP em profundidade, gerenciamento de múltiplas conexões simultâneas e relay de dados. Em Zig, temos controle total sobre buffers e conexões.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir proxy-http
cd proxy-http
zig init

Passo 2: Parser de Requisições HTTP

const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
const time = std.time;

const TAMANHO_BUF = 65536;

/// Informações extraídas de uma requisição HTTP.
const HttpRequest = struct {
    metodo: []const u8,
    url: []const u8,
    host: []const u8,
    porta: u16,
    versao: []const u8,
    headers_raw: []const u8,
    corpo_inicio: usize,
    content_length: usize,
};

/// Extrai informações de uma requisição HTTP raw.
fn parsearRequest(dados: []const u8) ?HttpRequest {
    // Request Line: METHOD URL HTTP/VERSION
    const fim_primeira_linha = mem.indexOf(u8, dados, "\r\n") orelse return null;
    const request_line = dados[0..fim_primeira_linha];

    var it = mem.splitScalar(u8, request_line, ' ');
    const metodo = it.next() orelse return null;
    const url = it.next() orelse return null;
    const versao = it.next() orelse "HTTP/1.1";

    // Extrair Host header
    var host: []const u8 = "";
    var porta: u16 = 80;

    if (mem.indexOf(u8, dados, "Host: ")) |pos| {
        const inicio_host = pos + 6;
        const fim_host = mem.indexOfPos(u8, dados, inicio_host, "\r\n") orelse dados.len;
        const host_completo = dados[inicio_host..fim_host];

        if (mem.indexOf(u8, host_completo, ":")) |pos_porta| {
            host = host_completo[0..pos_porta];
            porta = fmt.parseInt(u16, host_completo[pos_porta + 1 ..], 10) catch 80;
        } else {
            host = host_completo;
        }
    }

    // Content-Length
    var content_length: usize = 0;
    if (mem.indexOf(u8, dados, "Content-Length: ")) |pos| {
        const inicio = pos + 16;
        const fim = mem.indexOfPos(u8, dados, inicio, "\r\n") orelse dados.len;
        content_length = fmt.parseInt(usize, dados[inicio..fim], 10) catch 0;
    }

    // Encontrar fim dos headers
    const corpo_inicio = if (mem.indexOf(u8, dados, "\r\n\r\n")) |pos|
        pos + 4
    else
        dados.len;

    return .{
        .metodo = metodo,
        .url = url,
        .host = host,
        .porta = porta,
        .versao = versao,
        .headers_raw = dados[0..corpo_inicio],
        .corpo_inicio = corpo_inicio,
        .content_length = content_length,
    };
}

Passo 3: Filtragem e Logging

/// Configuração do proxy.
const ProxyConfig = struct {
    porta: u16,
    dominios_bloqueados: []const []const u8,
    log_habilitado: bool,
};

/// Registro de log de uma requisição.
const LogEntry = struct {
    metodo: [8]u8,
    metodo_len: usize,
    host: [256]u8,
    host_len: usize,
    url: [512]u8,
    url_len: usize,
    status: u16,
    tempo_ms: u64,
    bloqueado: bool,
};

/// Verifica se um domínio está na lista de bloqueio.
fn dominioBloqueado(host: []const u8, lista: []const []const u8) bool {
    for (lista) |bloqueado| {
        if (mem.eql(u8, host, bloqueado)) return true;
        // Verificar subdomínios
        if (host.len > bloqueado.len) {
            if (mem.endsWith(u8, host, bloqueado)) {
                const pos = host.len - bloqueado.len;
                if (pos > 0 and host[pos - 1] == '.') return true;
            }
        }
    }
    return false;
}

/// Gera a resposta de bloqueio.
fn respostaBloqueio(host: []const u8, buf: []u8) []const u8 {
    return fmt.bufPrint(buf,
        "HTTP/1.1 403 Forbidden\r\n" ++
            "Content-Type: text/html\r\n" ++
            "Connection: close\r\n" ++
            "\r\n" ++
            "<html><body><h1>Acesso Bloqueado</h1>" ++
            "<p>O dominio '{s}' esta bloqueado pelo proxy.</p></body></html>",
        .{host},
    ) catch "";
}

Passo 4: Lógica de Proxy

/// Encaminha uma requisição ao servidor destino e retorna a resposta.
fn encaminharRequisicao(
    request: HttpRequest,
    dados_request: []const u8,
    socket_cliente: posix.socket_t,
    writer_log: anytype,
) !void {
    const inicio = time.milliTimestamp();

    // Resolver endereço do servidor destino
    // Simplificação: usar IP direto via parsing simples
    const endereco = net.Address.resolveIp(request.host, request.porta) catch |err| {
        try writer_log.print("  [ERRO] Nao foi possivel resolver {s}: {}\n", .{ request.host, err });

        var buf_resp: [512]u8 = undefined;
        const resp = fmt.bufPrint(&buf_resp,
            "HTTP/1.1 502 Bad Gateway\r\n" ++
                "Content-Type: text/plain\r\n" ++
                "Connection: close\r\n\r\n" ++
                "Erro ao resolver host: {s}",
            .{request.host},
        ) catch return;
        _ = posix.write(socket_cliente, resp) catch {};
        return;
    };

    // Conectar ao servidor destino
    const socket_destino = posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0) catch {
        return;
    };
    defer posix.close(socket_destino);

    posix.connect(socket_destino, &endereco.any, endereco.getOsSockLen()) catch |err| {
        try writer_log.print("  [ERRO] Conexao recusada por {s}: {}\n", .{ request.host, err });
        const resp = "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
        _ = posix.write(socket_cliente, resp) catch {};
        return;
    };

    // Encaminhar a requisição original
    _ = posix.write(socket_destino, dados_request) catch return;

    // Relay: ler resposta do servidor e enviar ao cliente
    var buf_resposta: [TAMANHO_BUF]u8 = undefined;
    var status: u16 = 0;
    var total_bytes: usize = 0;

    while (true) {
        const n = posix.read(socket_destino, &buf_resposta) catch break;
        if (n == 0) break;

        // Extrair status code da primeira resposta
        if (total_bytes == 0) {
            if (mem.indexOf(u8, buf_resposta[0..n], " ")) |pos| {
                const fim_status = mem.indexOfPos(u8, buf_resposta[0..n], pos + 1, " ") orelse n;
                status = fmt.parseInt(u16, buf_resposta[pos + 1 .. fim_status], 10) catch 0;
            }
        }

        _ = posix.write(socket_cliente, buf_resposta[0..n]) catch break;
        total_bytes += n;
    }

    const duracao: u64 = @intCast(time.milliTimestamp() - inicio);

    try writer_log.print("  {s} {s}{s} -> {d} ({d}ms, {d} bytes)\n", .{
        request.metodo,
        request.host,
        request.url,
        status,
        duracao,
        total_bytes,
    });
}

Passo 5: Servidor Principal

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const porta: u16 = 8888;

    // Lista de domínios bloqueados
    const dominios_bloqueados = [_][]const u8{
        "ads.example.com",
        "tracker.example.com",
    };

    const config = ProxyConfig{
        .porta = porta,
        .dominios_bloqueados = &dominios_bloqueados,
        .log_habilitado = true,
    };

    // Criar socket do servidor
    const endereco = net.Address.initIp4(.{ 0, 0, 0, 0 }, config.porta);
    const socket_servidor = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
    defer posix.close(socket_servidor);

    const optval: i32 = 1;
    try posix.setsockopt(socket_servidor, posix.SOL.SOCKET, posix.SO.REUSEADDR, mem.asBytes(&optval));
    try posix.bind(socket_servidor, &endereco.any, endereco.getOsSockLen());
    try posix.listen(socket_servidor, 128);

    try stdout.print(
        \\
        \\  ==========================================
        \\     PROXY HTTP - Zig
        \\  ==========================================
        \\  Escutando na porta {d}
        \\
        \\  Configure seu navegador:
        \\    Proxy HTTP: localhost:{d}
        \\
        \\  Teste com curl:
        \\    curl -x http://localhost:{d} http://example.com
        \\  ==========================================
        \\
    , .{ porta, porta, porta });

    // Loop principal
    while (true) {
        const socket_cliente = posix.accept(socket_servidor, null, null) catch continue;

        // Ler requisição do cliente
        var buf: [TAMANHO_BUF]u8 = undefined;
        const n = posix.read(socket_cliente, &buf) catch {
            posix.close(socket_cliente);
            continue;
        };

        if (n == 0) {
            posix.close(socket_cliente);
            continue;
        }

        const dados = buf[0..n];

        if (parsearRequest(dados)) |request| {
            // Verificar bloqueio
            if (dominioBloqueado(request.host, config.dominios_bloqueados)) {
                try stdout.print("  [BLOQUEADO] {s}{s}\n", .{ request.host, request.url });
                var buf_bloq: [1024]u8 = undefined;
                const resp = respostaBloqueio(request.host, &buf_bloq);
                _ = posix.write(socket_cliente, resp) catch {};
            } else {
                encaminharRequisicao(request, dados, socket_cliente, stdout) catch {};
            }
        } else {
            const resp = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
            _ = posix.write(socket_cliente, resp) catch {};
        }

        posix.close(socket_cliente);
    }
}

Testes

test "parsear request GET" {
    const raw = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
    const req = parsearRequest(raw).?;
    try std.testing.expectEqualStrings("GET", req.metodo);
    try std.testing.expectEqualStrings("/", req.url);
    try std.testing.expectEqualStrings("example.com", req.host);
    try std.testing.expectEqual(@as(u16, 80), req.porta);
}

test "parsear request com porta" {
    const raw = "GET /path HTTP/1.1\r\nHost: example.com:8080\r\n\r\n";
    const req = parsearRequest(raw).?;
    try std.testing.expectEqual(@as(u16, 8080), req.porta);
}

test "dominio bloqueado - exato" {
    const lista = [_][]const u8{"ads.example.com"};
    try std.testing.expect(dominioBloqueado("ads.example.com", &lista));
    try std.testing.expect(!dominioBloqueado("example.com", &lista));
}

test "dominio bloqueado - subdominio" {
    const lista = [_][]const u8{"example.com"};
    try std.testing.expect(dominioBloqueado("sub.example.com", &lista));
    try std.testing.expect(!dominioBloqueado("notexample.com", &lista));
}

Compilando e Executando

zig build run

# Testar com curl:
curl -x http://localhost:8888 http://example.com

# Testar bloqueio:
curl -x http://localhost:8888 http://ads.example.com

Conceitos Aprendidos

  • Protocolo HTTP em profundidade (parsing manual)
  • Forward proxy com relay de dados
  • Filtragem de domínios para bloqueio de conteúdo
  • Gerenciamento de sockets com múltiplas conexões
  • Tratamento de erros de rede (timeouts, conexões recusadas)

Próximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.