[linux-x86] Fazendo um servidor HTTP
Na parte anterior conseguimos aceitar uma conexão TCP e mandar uma mensagem simples. Mas o objetivo desse tutorial é fazer um servidor web! Então chegou a hora de falarmos o protocolo HTTP e transformarmos nosso programa em um servidor que um navegador consegue entender.
Como funciona o HTTP?
HTTP (HyperText Transfer Protocol) é um protocolo de texto que roda sobre o TCP. A comunicação acontece assim:
- O cliente (navegador) conecta no servidor
- O cliente envia uma requisição em texto puro
- O servidor processa e envia uma resposta em texto puro
- A conexão é fechada (ou mantida, no caso de keep-alive)
Uma requisição HTTP típica tem esse formato:
GET / HTTP/1.1\r\n
Host: localhost:8081\r\n
User-Agent: curl/7.68.0\r\n
Accept: */*\r\n
\r\n
A primeira linha é a request line: <método> <caminho> <versão>. Depois vêm os headers (um por linha, formato Chave: Valor), e uma linha em branco (\r\n) indica o fim da requisição.
A resposta HTTP tem estrutura parecida:
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 123\r\n
Connection: close\r\n
\r\n
<html>...</html>
Primeiro vem a status line (versão código mensagem), depois os headers de resposta, uma linha em branco, e por fim o corpo da resposta (o HTML).
Lendo a requisição do cliente
A primeira novidade: precisamos ler o que o cliente enviou. No Linux, usamos a syscall read (id 0):
- RDI = fd de onde ler
- RSI = ponteiro para o buffer onde guardar os dados
- RDX = quantos bytes no máximo queremos ler
- Retorno em RAX = quantos bytes foram lidos (ou negativo se erro)
Vamos criar um buffer na seção .bss (Block Started by Symbol), que é uma seção para dados não inicializados — ocupa espaço na memória mas não no executável:
section .bss
buffer: resb 4096 ; reserva 4096 bytes (4 KB)
E então chamar o read:
mov rax, 0 ; syscall read
mov rdi, r13 ; fd do cliente
mov rsi, buffer ; ponteiro do buffer
mov rdx, 4096 ; até 4096 bytes
syscall
; RAX = número de bytes lidos
Por enquanto, vamos apenas ler a requisição para tirar os dados do socket. Na sequência usaremos os primeiros bytes desse buffer para decidir qual resposta enviar.
Escrevendo a resposta HTTP
Para responder ao navegador, precisamos enviar os headers HTTP seguidos do conteúdo HTML. Vamos criar tudo como strings na seção .rodata:
section .rodata
headers_200:
db "HTTP/1.1 200 OK", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db "Content-Length: 251", 0x0D, 0x0A
db 0x0D, 0x0A ; linha em branco = fim dos headers
; O corpo HTML vem logo em seguida!
db "<html>", 0x0D, 0x0A
db "<head><title>Baixaria HTTP</title></head>", 0x0D, 0x0A
db '<body bgcolor="black">', 0x0D, 0x0A
db '<center><h1 style="color: #00ff00">Ola mundo da baixaria!</h1></center>', 0x0D, 0x0A
db '<hr>', 0x0D, 0x0A
db '<center style="color: #00ff00">asm4noobs — feito em assembly puro!</center>', 0x0D, 0x0A
db '</body>', 0x0D, 0x0A
db '</html>', 0x0D, 0x0A
headers_200_len equ $ - headers_200 ; calcula o tamanho total enviado pelo write
Repare em alguns detalhes importantes:
- As quebras de linha no HTTP são
\r\n(0x0D + 0x0A, ou CR+LF). Não use apenas\n! - O
Content-Lengthdiz quantos bytes tem o corpo da resposta (depois da linha em branco). Tem que estar correto, senão o navegador vai ficar esperando mais dados ou cortar o HTML. - Usei a diretiva
equpara calcular automaticamente quantos bytes serão enviados pelowrite.$significa “posição atual” e subtraindo o label inicial, temos o tamanho exato da resposta inteira (status line + headers + corpo).
Com isso, enviar a resposta completa é trivial:
mov rdi, r13 ; fd do cliente
mov rsi, headers_200
mov rdx, headers_200_len
call enviar_tudo
Precisamos de uma variação da nossa enviar que aceite um tamanho explícito (já que a resposta HTTP não tem terminador nulo):
Em um servidor robusto, precisaríamos repetir o
writeaté todos os bytes serem enviados, porque sockets podem fazer escritas parciais. Para este exemplo pequeno em localhost, uma única chamada é suficiente para manter o código simples.
enviar_tudo:
; enviar_tudo(fd, buffer, tamanho) => RDI=fd, RSI=buffer, RDX=tamanho
push rdi
push rsi
push rdx
mov rax, 1 ; syscall write
syscall
pop rdx
pop rsi
pop rdi
ret
Juntando tudo: o servidor HTTP
section .text
global _start
enviar_tudo:
; enviar_tudo(fd, buffer, tamanho)
push rdi
push rsi
push rdx
mov rax, 1 ; syscall write
syscall
pop rdx
pop rsi
pop rdi
ret
_start:
; 1. socket(AF_INET, SOCK_STREAM, 0)
mov rax, 41
mov rdi, 2
mov rsi, 1
mov rdx, 0
syscall
mov r12, rax
; 2. bind(socket_fd, &addr, 16)
mov rax, 49
mov rdi, r12
mov rsi, sockaddr_in
mov rdx, 16
syscall
; 3. listen(socket_fd, 10)
mov rax, 50
mov rdi, r12
mov rsi, 10
syscall
; 4. accept(socket_fd, NULL, NULL)
mov rax, 43
mov rdi, r12
mov rsi, 0
mov rdx, 0
syscall
mov r13, rax ; fd do cliente
; 5. Ler requisição do cliente
mov rax, 0 ; syscall read
mov rdi, r13 ; fd do cliente
mov rsi, buffer
mov rdx, 4096
syscall
; Ignoramos o conteúdo da requisição por enquanto
; 6. Enviar resposta HTTP
mov rdi, r13
mov rsi, headers_200
mov rdx, headers_200_len
call enviar_tudo
; 7. Fechar conexão com o cliente
mov rax, 3
mov rdi, r13
syscall
; 8. Fechar socket servidor
mov rax, 3
mov rdi, r12
syscall
; 9. Sair
mov rax, 60
mov rdi, 0
syscall
section .data
sockaddr_in:
dw 2 ; AF_INET
dw 0x911F ; htons(8081)
dd 0 ; INADDR_ANY
dq 0 ; sin_zero
section .bss
buffer: resb 4096
section .rodata
headers_200:
db "HTTP/1.1 200 OK", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db "Content-Length: 251", 0x0D, 0x0A
db 0x0D, 0x0A
db "<html>", 0x0D, 0x0A
db "<head><title>Baixaria HTTP</title></head>", 0x0D, 0x0A
db '<body bgcolor="black">', 0x0D, 0x0A
db '<center><h1 style="color: #00ff00">Ola mundo da baixaria!</h1></center>', 0x0D, 0x0A
db '<hr>', 0x0D, 0x0A
db '<center style="color: #00ff00">asm4noobs — feito em assembly puro!</center>', 0x0D, 0x0A
db '</body>', 0x0D, 0x0A
db '</html>', 0x0D, 0x0A
headers_200_len equ $ - headers_200
IMPORTANTE: O
Content-Length: 251foi calculado manualmente contando os bytes do HTML (do<html>até</html>incluindo os\r\n). Se você alterar o HTML, recalcule esse valor! Na próxima parte vamos simplificar isso usandoConnection: closesemContent-Length.
Testando com o navegador
Compile e execute:
make clean
make
./helloworld
Agora abra seu navegador e acesse:
http://localhost:8081
Você deverá ver uma página preta com o texto verde “Ola mundo da baixaria!” — exatamente o HTML que escrevemos! Dá pra testar com curl também:
curl -v http://localhost:8081
O -v (verbose) mostra os headers da resposta:
< HTTP/1.1 200 OK
< Content-Type: text/html
< Connection: close
< Content-Length: 251
<
<html>
<head><title>Baixaria HTTP</title></head>
...
Adicionando suporte a rotas (404)
Vamos melhorar: ler o caminho da requisição e retornar 404 se não for /. Primeiro, precisamos verificar se a requisição começa com GET / :
; Depois do read, verificar o buffer
cmp dword [buffer], "GET " ; compara os primeiros 4 bytes com "GET "
jne .erro_400
cmp word [buffer + 4], "/ " ; compara bytes 4-5 com "/ "
je .rota_ok
.erro_400:
; Retornar 404
...
jmp .fim_resposta
.rota_ok:
; Retornar 200 com o HTML
...
jmp .fim_resposta
.fim_resposta:
Atenção com a ordem dos bytes! No NASM,
"GET "é armazenado como bytes ‘G’ ‘E’ ‘T’ ‘ ‘ na memória. Como o x86 é little-endian,dword [buffer]lê os bytes na ordem: buffer[0], buffer[1], buffer[2], buffer[3]. A comparaçãocmp dword [buffer], "GET "funciona porque o NASM inverte automaticamente strings em constantes de múltiplos bytes. Mágico, né?
Se você não confia nessa mágica, podemos fazer o jeito seguro byte a byte:
mov al, [buffer]
cmp al, 'G'
jne .erro_400
mov al, [buffer + 1]
cmp al, 'E'
jne .erro_400
mov al, [buffer + 2]
cmp al, 'T'
jne .erro_400
mov al, [buffer + 4]
cmp al, '/'
jne .erro_400
mov al, [buffer + 5]
cmp al, ' '
jne .erro_400
; É um GET / válido!
Implementação completa com rota 404
section .text
global _start
enviar_tudo:
push rdi
push rsi
push rdx
mov rax, 1
syscall
pop rdx
pop rsi
pop rdi
ret
_start:
mov rax, 41
mov rdi, 2
mov rsi, 1
mov rdx, 0
syscall
mov r12, rax
mov rax, 49
mov rdi, r12
mov rsi, sockaddr_in
mov rdx, 16
syscall
mov rax, 50
mov rdi, r12
mov rsi, 10
syscall
mov rax, 43
mov rdi, r12
mov rsi, 0
mov rdx, 0
syscall
mov r13, rax
; Ler requisição
mov rax, 0
mov rdi, r13
mov rsi, buffer
mov rdx, 4096
syscall
; Verificar se é "GET / " (rota raiz)
cmp byte [buffer], 'G'
jne .erro_404
cmp byte [buffer + 1], 'E'
jne .erro_404
cmp byte [buffer + 2], 'T'
jne .erro_404
cmp byte [buffer + 4], '/'
jne .erro_404
cmp byte [buffer + 5], ' '
jne .erro_404
.rota_ok:
mov rdi, r13
mov rsi, resposta_200
mov rdx, resposta_200_len
call enviar_tudo
jmp .finalizar
.erro_404:
mov rdi, r13
mov rsi, resposta_404
mov rdx, resposta_404_len
call enviar_tudo
.finalizar:
mov rax, 3
mov rdi, r13
syscall
mov rax, 3
mov rdi, r12
syscall
mov rax, 60
mov rdi, 0
syscall
section .data
sockaddr_in:
dw 2
dw 0x911F
dd 0
dq 0
section .bss
buffer: resb 4096
section .rodata
resposta_404:
db "HTTP/1.1 404 Not Found", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db "Content-Length: 155", 0x0D, 0x0A
db 0x0D, 0x0A
db "<html><head><title>404</title></head>", 0x0D, 0x0A
db "<body bgcolor='black'><center>", 0x0D, 0x0A
db "<h1 style='color: red'>404 — Nao achei a baixaria!</h1>", 0x0D, 0x0A
db "</center></body></html>", 0x0D, 0x0A
resposta_404_len equ $ - resposta_404
resposta_200:
db "HTTP/1.1 200 OK", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db "Content-Length: 251", 0x0D, 0x0A
db 0x0D, 0x0A
db "<html>", 0x0D, 0x0A
db "<head><title>Baixaria HTTP</title></head>", 0x0D, 0x0A
db '<body bgcolor="black">', 0x0D, 0x0A
db '<center><h1 style="color: #00ff00">Ola mundo da baixaria!</h1></center>', 0x0D, 0x0A
db '<hr>', 0x0D, 0x0A
db '<center style="color: #00ff00">asm4noobs — feito em assembly puro!</center>', 0x0D, 0x0A
db '</body>', 0x0D, 0x0A
db '</html>', 0x0D, 0x0A
resposta_200_len equ $ - resposta_200
Testando as rotas
Compile, execute e teste:
make clean
make
./helloworld
No navegador:
-
http://localhost:8081/→ página verde “Ola mundo da baixaria!” -
http://localhost:8081/qualquercoisa→ página vermelha “404 — Nao achei a baixaria!”
Com curl:
$ curl -v http://localhost:8081/
< HTTP/1.1 200 OK
$ curl -v http://localhost:8081/naoexiste
< HTTP/1.1 404 Not Found
Entendendo as seções
Nesse ponto do tutorial usamos todas as seções principais de um programa:
| Seção | Permissão | O que colocamos |
|---|---|---|
.text |
R+X (ler e executar) | Código executável: funções, _start |
.data |
R+W (ler e escrever) | Dados inicializados mutáveis: sockaddr_in |
.rodata |
R (somente leitura) | Constantes: respostas HTTP |
.bss |
R+W (ler e escrever) | Dados não inicializados: buffer |
Salvando o progresso
git add helloworld.asm
git commit -am "Servidor HTTP funcionando com rotas 200 e 404. A baixaria agora tem web!"
Conclusão da terceira parte
Conseguimos! Nosso programa agora é um servidor HTTP completo (ainda que minimalista): recebe requisições, faz roteamento básico, retorna headers e HTML apropriados. Aprendemos:
- A syscall
readpara receber dados - O formato das requisições e respostas HTTP
- A seção
.bsspara buffers não inicializados - A diretiva
equpara calcular tamanhos automaticamente - Como comparar strings em assembly para fazer roteamento
Na próxima (e última) parte do tutorial Linux x86, vamos deixar nosso servidor atendendo várias conexões em sequência, melhorar a reinicialização do servidor, e fazer um fechamento do que aprendemos!