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
- Zig 0.13+ instalado (guia de instalação)
- Conhecimento de HTTP (servidor de arquivos)
- Familiaridade com redes TCP
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
- Explore a stdlib de redes
- Veja o Load Balancer para distribuição
- Construa o URL Shortener para mais HTTP
- Consulte o Servidor de Arquivos HTTP