Servidor HTTP de Arquivos em Zig — Tutorial Passo a Passo

Servidor HTTP de Arquivos em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um servidor HTTP que serve arquivos estáticos — semelhante ao python -m http.server, mas em Zig. O servidor lista diretórios, serve arquivos com MIME types corretos e lida com múltiplas conexões.

O Que Vamos Construir

Nosso servidor vai:

  • Servir arquivos estáticos de um diretório raiz
  • Gerar listagem HTML de diretórios automaticamente
  • Detectar MIME types baseado em extensão de arquivo
  • Retornar códigos de status HTTP corretos (200, 404, 403, 500)
  • Logar requisições no console
  • Suportar múltiplas conexões sequenciais

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir servidor-arquivos-http
cd servidor-arquivos-http
zig init

Passo 2: MIME Types e Utilidades

const std = @import("std");
const net = std.net;
const fs = std.fs;
const mem = std.mem;
const posix = std.posix;
const Allocator = std.mem.Allocator;

/// Detecta o MIME type baseado na extensão do arquivo.
/// Usamos uma função simples em vez de HashMap porque o número
/// de extensões é pequeno e conhecido em comptime.
fn detectarMimeType(caminho: []const u8) []const u8 {
    const extensoes = .{
        .{ ".html", "text/html; charset=utf-8" },
        .{ ".htm", "text/html; charset=utf-8" },
        .{ ".css", "text/css" },
        .{ ".js", "application/javascript" },
        .{ ".json", "application/json" },
        .{ ".xml", "application/xml" },
        .{ ".txt", "text/plain; charset=utf-8" },
        .{ ".md", "text/markdown; charset=utf-8" },
        .{ ".png", "image/png" },
        .{ ".jpg", "image/jpeg" },
        .{ ".jpeg", "image/jpeg" },
        .{ ".gif", "image/gif" },
        .{ ".svg", "image/svg+xml" },
        .{ ".ico", "image/x-icon" },
        .{ ".pdf", "application/pdf" },
        .{ ".zip", "application/zip" },
        .{ ".wasm", "application/wasm" },
        .{ ".zig", "text/plain; charset=utf-8" },
    };

    inline for (extensoes) |par| {
        if (mem.endsWith(u8, caminho, par[0])) return par[1];
    }

    return "application/octet-stream";
}

/// Formata tamanho de arquivo para exibição legível.
fn formatarTamanho(tamanho: u64, buf: []u8) []const u8 {
    if (tamanho < 1024) {
        return std.fmt.bufPrint(buf, "{d} B", .{tamanho}) catch "?";
    } else if (tamanho < 1024 * 1024) {
        return std.fmt.bufPrint(buf, "{d:.1} KB", .{@as(f64, @floatFromInt(tamanho)) / 1024.0}) catch "?";
    } else {
        return std.fmt.bufPrint(buf, "{d:.1} MB", .{@as(f64, @floatFromInt(tamanho)) / (1024.0 * 1024.0)}) catch "?";
    }
}

Passo 3: Parser de Requisição HTTP

/// Requisição HTTP parseada.
const Requisicao = struct {
    metodo: []const u8,
    caminho: []const u8,
    versao: []const u8,
};

/// Parseia a primeira linha de uma requisição HTTP.
/// Formato: "GET /caminho HTTP/1.1"
fn parsearRequisicao(raw: []const u8) ?Requisicao {
    const primeira_linha = if (mem.indexOf(u8, raw, "\r\n")) |idx|
        raw[0..idx]
    else if (mem.indexOf(u8, raw, "\n")) |idx|
        raw[0..idx]
    else
        raw;

    // Split por espaço
    var iter = mem.splitScalar(u8, primeira_linha, ' ');
    const metodo = iter.next() orelse return null;
    const caminho = iter.next() orelse return null;
    const versao = iter.next() orelse "HTTP/1.0";

    return Requisicao{
        .metodo = metodo,
        .caminho = caminho,
        .versao = versao,
    };
}

/// Decodifica URL encoding (%20 -> espaço, etc.).
fn decodificarURL(encoded: []const u8, buf: []u8) []const u8 {
    var pos: usize = 0;
    var i: usize = 0;

    while (i < encoded.len and pos < buf.len) {
        if (encoded[i] == '%' and i + 2 < encoded.len) {
            const hex = [2]u8{ encoded[i + 1], encoded[i + 2] };
            if (std.fmt.parseInt(u8, &hex, 16)) |val| {
                buf[pos] = val;
                pos += 1;
                i += 3;
                continue;
            } else |_| {}
        }
        buf[pos] = encoded[i];
        pos += 1;
        i += 1;
    }

    return buf[0..pos];
}

Passo 4: Geração de Respostas

/// Envia uma resposta HTTP completa.
fn enviarResposta(
    socket: posix.socket_t,
    status: u16,
    status_text: []const u8,
    content_type: []const u8,
    corpo: []const u8,
) void {
    var header_buf: [1024]u8 = undefined;
    const header = std.fmt.bufPrint(&header_buf,
        "HTTP/1.1 {d} {s}\r\n" ++
            "Content-Type: {s}\r\n" ++
            "Content-Length: {d}\r\n" ++
            "Connection: close\r\n" ++
            "Server: ZigHTTP/1.0\r\n" ++
            "\r\n",
        .{ status, status_text, content_type, corpo.len },
    ) catch return;

    _ = posix.write(socket, header) catch {};
    _ = posix.write(socket, corpo) catch {};
}

/// Gera uma página HTML de listagem de diretório.
fn gerarListagemDiretorio(
    allocator: Allocator,
    dir_path: []const u8,
    url_path: []const u8,
) ![]const u8 {
    var html = std.ArrayList(u8).init(allocator);
    errdefer html.deinit();
    const w = html.writer();

    try w.print(
        \\<!DOCTYPE html>
        \\<html><head>
        \\<meta charset="utf-8">
        \\<title>Indice de {s}</title>
        \\<style>
        \\body {{ font-family: monospace; margin: 2em; }}
        \\table {{ border-collapse: collapse; }}
        \\td {{ padding: 4px 16px; }}
        \\a {{ text-decoration: none; color: #0066cc; }}
        \\a:hover {{ text-decoration: underline; }}
        \\.dir {{ font-weight: bold; }}
        \\</style>
        \\</head><body>
        \\<h1>Indice de {s}</h1>
        \\<table>
    , .{ url_path, url_path });

    // Link para diretório pai
    if (!mem.eql(u8, url_path, "/")) {
        try w.print("<tr><td class=\"dir\"><a href=\"..\">..</a></td><td>-</td></tr>\n", .{});
    }

    var dir = try fs.cwd().openDir(dir_path, .{ .iterate = true });
    defer dir.close();

    var iter = dir.iterate();
    while (try iter.next()) |entry| {
        var size_buf: [32]u8 = undefined;
        switch (entry.kind) {
            .directory => {
                try w.print("<tr><td class=\"dir\"><a href=\"{s}/\">{s}/</a></td><td>-</td></tr>\n", .{
                    entry.name, entry.name,
                });
            },
            .file => {
                const stat = dir.statFile(entry.name) catch continue;
                const tamanho = formatarTamanho(stat.size, &size_buf);
                try w.print("<tr><td><a href=\"{s}\">{s}</a></td><td>{s}</td></tr>\n", .{
                    entry.name, entry.name, tamanho,
                });
            },
            else => {},
        }
    }

    try w.print("</table><hr><p>ZigHTTP/1.0</p></body></html>", .{});

    return html.toOwnedSlice();
}

Passo 5: Handler de Requisições

/// Processa uma requisição HTTP.
fn handleRequisicao(
    allocator: Allocator,
    socket: posix.socket_t,
    raiz: []const u8,
    writer_log: anytype,
) !void {
    var buf: [4096]u8 = undefined;
    const n = posix.read(socket, &buf) catch return;
    if (n == 0) return;

    const req = parsearRequisicao(buf[0..n]) orelse {
        enviarResposta(socket, 400, "Bad Request", "text/plain", "Requisicao invalida");
        return;
    };

    // Decodificar URL
    var url_buf: [512]u8 = undefined;
    const caminho_decodificado = decodificarURL(req.caminho, &url_buf);

    // Segurança: prevenir path traversal
    if (mem.indexOf(u8, caminho_decodificado, "..") != null) {
        enviarResposta(socket, 403, "Forbidden", "text/plain", "Acesso negado");
        try writer_log.print("  [{s}] {s} {s} -> 403\n", .{ req.metodo, req.caminho, "path traversal" });
        return;
    }

    // Construir caminho no filesystem
    var path_buf: [1024]u8 = undefined;
    const caminho_rel = if (caminho_decodificado.len > 1) caminho_decodificado[1..] else "";
    const caminho_fs = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ raiz, caminho_rel }) catch {
        enviarResposta(socket, 500, "Internal Error", "text/plain", "Erro interno");
        return;
    };

    // Verificar se é diretório
    if (fs.cwd().openDir(caminho_fs, .{ .iterate = true })) |*dir| {
        dir.close();
        const html = gerarListagemDiretorio(allocator, caminho_fs, caminho_decodificado) catch {
            enviarResposta(socket, 500, "Internal Error", "text/plain", "Erro ao listar diretorio");
            return;
        };
        defer allocator.free(html);

        enviarResposta(socket, 200, "OK", "text/html; charset=utf-8", html);
        try writer_log.print("  [GET] {s} -> 200 (dir)\n", .{req.caminho});
        return;
    } else |_| {}

    // Tentar servir arquivo
    const conteudo = fs.cwd().readFileAlloc(allocator, caminho_fs, 50 * 1024 * 1024) catch {
        enviarResposta(socket, 404, "Not Found", "text/html",
            "<!DOCTYPE html><html><body><h1>404 - Arquivo nao encontrado</h1></body></html>");
        try writer_log.print("  [GET] {s} -> 404\n", .{req.caminho});
        return;
    };
    defer allocator.free(conteudo);

    const mime = detectarMimeType(caminho_fs);
    enviarResposta(socket, 200, "OK", mime, conteudo);
    try writer_log.print("  [GET] {s} -> 200 ({d} bytes)\n", .{ req.caminho, conteudo.len });
}

Passo 6: Loop Principal do Servidor

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const stdout = std.io.getStdOut().writer();

    const porta: u16 = 8080;
    const raiz = ".";

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

    const optval: i32 = 1;
    try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, mem.asBytes(&optval));

    try posix.bind(socket, &endereco.any, endereco.getOsSockLen());
    try posix.listen(socket, 10);

    try stdout.print(
        \\
        \\  ==========================================
        \\    SERVIDOR HTTP DE ARQUIVOS - Zig
        \\  ==========================================
        \\  Servindo: {s}
        \\  URL: http://localhost:{d}
        \\  Ctrl+C para parar
        \\
    , .{ raiz, porta });

    while (true) {
        const cliente = posix.accept(socket, null, null) catch continue;
        defer posix.close(cliente);

        handleRequisicao(allocator, cliente, raiz, stdout) catch |err| {
            try stdout.print("  Erro: {any}\n", .{err});
        };
    }
}

Testes

test "detectar mime type" {
    try std.testing.expectEqualStrings("text/html; charset=utf-8", detectarMimeType("index.html"));
    try std.testing.expectEqualStrings("image/png", detectarMimeType("foto.png"));
    try std.testing.expectEqualStrings("application/json", detectarMimeType("data.json"));
    try std.testing.expectEqualStrings("application/octet-stream", detectarMimeType("arquivo.xyz"));
}

test "parsear requisicao" {
    const req = parsearRequisicao("GET /index.html HTTP/1.1\r\nHost: localhost\r\n").?;
    try std.testing.expectEqualStrings("GET", req.metodo);
    try std.testing.expectEqualStrings("/index.html", req.caminho);
}

test "decodificar url" {
    var buf: [512]u8 = undefined;
    try std.testing.expectEqualStrings("hello world", decodificarURL("hello%20world", &buf));
}

test "formatar tamanho" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("512 B", formatarTamanho(512, &buf));
}

Compilando e Executando

zig build test
zig build run
# Acesse http://localhost:8080 no navegador

Conceitos Aprendidos

  • Sockets TCP e protocolo HTTP básico
  • Servir arquivos estáticos com MIME types corretos
  • Geração de HTML dinâmico (listagem de diretórios)
  • URL decoding e prevenção de path traversal
  • inline for para lookup em comptime
  • Gerenciamento de memória com allocator

Próximos Passos

Continue aprendendo Zig

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