Mostrado en HN: Hospedar un sitio web usando un servidor web en C
(github.com/cozis)Tecnología de mi blog
Este servidor web es un servidor web minimalista diseñado para alojar mi blog. Fue construido desde cero para ser lo suficientemente robusto como para soportar la internet pública. No necesita un proxy inverso. Puedes ver una instancia real funcionando en http://playin.coz.is/index.html. Pedí en Reddit que lo intentaran hackear y reuní gigabytes de logs con solicitudes divertidas y maliciosas. Guardé algunas en attempts.txt y después revisaré más por diversión.
Pero... ¿por qué?
Disfruto crear mis propias herramientas y estoy cansado de escuchar que todo tiene que estar “probado en batalla”. ¿Y si ocurre un fallo? Los bugs se pueden corregir.
Especificaciones
- Solo para Linux
- Implementa HTTP/1.1, pipelining y conexiones keep-alive
- Soporte para HTTPS (usa BearSSL, hasta TLS 1.2)
- Dependencias mínimas (libc y BearSSL si se usa HTTPS)
- Timeouts configurables
- Logs de acceso, logs de fallos, rotación de logs y límite de uso de disco
- No hay
Transfer-Encoding: Chunked(responde con411 Length Requiredpara hacer que el cliente reenvíe conContent-Length) - Un solo núcleo (esto cambiará cuando consiga un mejor VPS)
- Sin caché de archivos estáticos (todavía)
Benchmarks
Aunque el enfoque de este proyecto es la robustez, para nada es lento. Comparación simple con nginx (endpoint estático, ambos de un solo hilo, límite de 1K conexiones):
-
(blogtech)
$ wrk -c 500 -d 5s http://127.0.0.1:80/hello- Latencia promedio: 6.66ms
- Solicitudes/seg: 76974.24
- Transferencia/seg: 6.09MB
-
(nginx)
$ wrk -c 500 -d 5s http://127.0.0.1:8080/hello- Latencia promedio: 149.11ms
- Solicitudes/seg: 44227.78
- Transferencia/seg: 8.27MB
Configuración de nginx:
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location /hello {
add_header Content-Type text/plain;
return 200 "Hello, world!";
}
}
}
Compilación y ejecución
Por defecto, la compilación del servidor es solo para HTTP:
$ make
Este comando genera los ejecutables serve (build de release), serve_cov (build con cobertura) y serve_debug (build de depuración). La build de release escucha en el puerto 80 y la build de depuración en el puerto 8080.
Para habilitar HTTPS, hay que clonar y compilar BearSSL:
$ mkdir 3p
$ cd 3p
$ git clone https://www.bearssl.org/git/BearSSL
$ cd BearSSL
$ make -j
$ cd ../../
$ make -B HTTPS=1
Se generan los mismos ejecutables, pero con conexiones seguras disponibles en el puerto 443 (release) o 8081 (depuración). Debes colocar los archivos cert.pem y key.pem en el mismo directorio que el ejecutable. Para cambiar el nombre y la ubicación, modifica:
#define HTTPS_KEY_FILE "key.pem"
#define HTTPS_CERT_FILE "cert.pem"
Para probar HTTPS localmente, genera un certificado autofirmado (y su clave 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
El servidor entrega contenido estático desde la carpeta docroot/. Para cambiar esto, modifica la función 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 :|"));
}
Aquí puedes agregar endpoints haciendo switch sobre el campo request.path. La ruta es solo un slice del buffer de la solicitud. El URI no se parsea.
Pruebas
Ejecuto regularmente el servidor con valgrind y sanitizers (address, undefined) y le hago pruebas con wrk. También estoy agregando pruebas automatizadas en tests/test.py para verificar el cumplimiento de la especificación HTTP/1.1. Mantengo el estrés alojando mi sitio web y publicándolo en varios lugares. Todos los bots que escanean sitios web vulnerables en internet terminan siendo excelentes fuzzers.
Problemas conocidos
- El servidor responde con HTTP/1.1 a clientes HTTP/1.0
Contribuciones
Trabajo principalmente en la rama DEV y de vez en cuando fusiono a MAIN. Si abres un pull request apuntando a DEV, será más fácil.
Resumen de GN⁺
- Este proyecto es un servidor web orientado a la robustez y a las dependencias mínimas.
- Soporta HTTP/1.1 y HTTPS, y ofrece varias funciones de logging y timeouts configurables.
- Los benchmarks muestran tiempos de respuesta más rápidos que nginx.
- Está diseñado para que los desarrolladores disfruten creando sus propias herramientas y corrigiendo bugs.
- Proyectos con funciones similares incluyen Nginx y Apache HTTP Server.
1 comentarios
Comentarios de Hacker News
No hace falta un proxy inverso: Usó Jetty para desplegar una app en internet sin proxy inverso y no tuvo problemas
Servidor web en C hecho por él mismo: Creó un servidor web en C con el que operó sitios comerciales
La satisfacción de construir servicios: Es muy satisfactorio construir servicios básicos usando la API del sistema
poll()Presentación de un proyecto pequeño: Presenta un proyecto divertido que empezó en su tiempo libre
Recomendación del framework Kore: Si al escribir una app en C resulta incómodo implementar la parte expuesta al público, recomienda el framework Kore
Comparte un enlace interesante: La instancia de althttpd de sqlite.org maneja más de 500 mil solicitudes HTTP al día
La diversión de crear tus propias herramientas: Está cansado de la idea de que todo tiene que estar "probado en batalla"
Charla del Chaos Communication Congress: Esto le recordó una charla sobre un blog/servidor web escrito en C con funciones de seguridad incluidas
Sitio web estable: Un sitio web que no se cae aunque aparezca en la portada
Volver a lo básico: Le gusta el enfoque de volver a lo básico usando solo lo necesario