Lançado no HN: Hospedando um site com um servidor web em C
(github.com/cozis)Tecnologia do Meu Blog
Este servidor web é um servidor mínimo projetado para hospedar meu blog. Foi construído desde o início para ser robusto o suficiente para aguentar a internet pública. Não precisa de proxy reverso. Dá para ver funcionando de verdade em http://playin.coz.is/index.html. Pedi no Reddit para tentarem hackear e coletei gigabytes de logs de requisições divertidas e maliciosas. Salvei algumas em attempts.txt e pretendo fuçar mais depois por diversão.
Mas... por quê?
Eu gosto de criar minhas próprias ferramentas e cansei de ouvir que tudo precisa ser "testado em batalha". E se der crash? Bugs podem ser corrigidos.
Especificações
- Apenas Linux
- Implementa HTTP/1.1, pipelining e conexões keep-alive
- Suporte a HTTPS (usando BearSSL até TLS 1.2)
- Dependências mínimas (libc e BearSSL ao usar HTTPS)
- Timeouts configuráveis
- Logs de acesso, logs de crash, rotação de logs, limite de uso de disco
- Sem
Transfer-Encoding: Chunked(responde com411 Length Requiredpara induzir o cliente a reenviar comContent-Length) - Núcleo único (deve mudar quando eu conseguir um VPS melhor)
- Sem cache de arquivos estáticos (ainda)
Benchmark
Embora o foco deste projeto seja robustez, ele não é lento de forma alguma. Uma comparação simples com nginx (endpoint estático, ambos single-thread, limite de 1K conexões):
-
(blogtech)
$ wrk -c 500 -d 5s http://127.0.0.1:80/hello- Latência média: 6.66ms
- Requisições/seg: 76974.24
- Transferência/seg: 6.09MB
-
(nginx)
$ wrk -c 500 -d 5s http://127.0.0.1:8080/hello- Latência média: 149.11ms
- Requisições/seg: 44227.78
- Transferência/seg: 8.27MB
Configuração do nginx:
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location /hello {
add_header Content-Type text/plain;
return 200 "Hello, world!";
}
}
}
Build e execução
Por padrão, o build do servidor é somente HTTP:
$ make
Esse comando gera os executáveis serve (build de release), serve_cov (build de cobertura) e serve_debug (build de debug). O build de release escuta na porta 80, e o build de debug na porta 8080.
Para ativar HTTPS, é preciso clonar e compilar o BearSSL:
$ mkdir 3p
$ cd 3p
$ git clone https://www.bearssl.org/git/BearSSL
$ cd BearSSL
$ make -j
$ cd ../../
$ make -B HTTPS=1
Os mesmos executáveis são gerados, mas com conexões seguras nas portas 443 (release) ou 8081 (debug). Você precisa colocar os arquivos cert.pem e key.pem no mesmo diretório do executável. Para mudar nome e localização, altere:
#define HTTPS_KEY_FILE "key.pem"
#define HTTPS_CERT_FILE "cert.pem"
Para testar HTTPS localmente, gere um certificado autoassinado (e a chave privada):
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
openssl req -new -x509 -key key.pem -out cert.pem -days 365
Uso
O servidor serve conteúdo estático da pasta docroot/. Para mudar isso, altere a função respond:
typedef struct {
Method method;
string path;
int major;
int minor;
int nheaders;
Header headers[MAX_HEADERS];
string content;
} Request;
void respond(Request request, ResponseBuilder *b) {
if (request.major != 1 || request.minor > 1) {
status_line(b, 505); // HTTP Version Not Supported
return;
}
if (request.method != M_GET) {
status_line(b, 405); // Method Not Allowed
return;
}
if (string_match_case_insensitive(request.path, LIT("/hello"))) {
status_line(b, 200);
append_content_s(b, LIT("Hello, world!"));
return;
}
if (serve_file_or_dir(b, LIT("/"), LIT("docroot/"), request.path, NULLSTR, false))
return;
status_line(b, 404);
append_content_s(b, LIT("Nothing here :|"));
}
Aqui você pode adicionar endpoints tratando o campo request.path. O caminho é apenas um slice do buffer da requisição. A URI não é analisada.
Testes
Eu executo o servidor regularmente com valgrind e sanitizers (address, undefined) e faço testes com wrk. Também estou adicionando testes automatizados em tests/test.py para verificar conformidade com a especificação HTTP/1.1. Hospedo meu site com ele e posto links aqui e ali para manter a pressão. Todos os bots que escaneiam sites vulneráveis na internet viram ótimos fuzzers.
Problemas conhecidos
- O servidor responde a clientes HTTP/1.0 com HTTP/1.1
Contribuição
Eu trabalho principalmente na branch DEV e ocasionalmente faço merge para a MAIN. Ao abrir um pull request, vai ser mais fácil se ele tiver como alvo a DEV.
Resumo do GN⁺
- Este projeto é um servidor web com foco em dependências mínimas e robustez.
- Suporta HTTP/1.1 e HTTPS, além de oferecer vários recursos de logging e timeouts configuráveis.
- Os resultados de benchmark mostram tempos de resposta mais rápidos que o nginx.
- Foi projetado para que desenvolvedores possam curtir criar suas próprias ferramentas e corrigir bugs no processo.
- Projetos com funcionalidades parecidas incluem Nginx e Apache HTTP Server.
1 comentários
Comentários do Hacker News
Sem necessidade de proxy reverso: usou Jetty para colocar apps na internet sem proxy reverso e não teve problemas
Servidor web em C feito por conta própria: criou um servidor web em C que chegou a operar sites comerciais
Satisfação de construir serviços: é muito satisfatório criar serviços básicos usando APIs do sistema
poll()entregue desempenho tão altoApresentação de um pequeno projeto: apresenta um projeto interessante iniciado no tempo livre
Recomendação do framework Kore: se for incômodo escrever a parte exposta ao público de apps em C, recomenda o framework Kore
Compartilhamento de link interessante: a instância de althttpd do sqlite.org processa mais de 500 mil requisições HTTP por dia
A diversão de criar as próprias ferramentas: está cansado da opinião de que tudo precisa ser "testado em batalha"
Palestra no Chaos Communication Congress: relembra uma palestra sobre um blog/servidor web escrito em C com recursos de segurança
Site estável: um site que não trava nem quando aparece na primeira página
De volta ao básico: gosta da abordagem de voltar ao básico usando apenas o necessário