2 puntos por GN⁺ 2024-09-26 | 1 comentarios | Compartir por WhatsApp

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 con 411 Length Required para hacer que el cliente reenvíe con Content-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

 
GN⁺ 2024-09-26
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

    • Hay muchas opiniones que recomiendan usar un proxy inverso sin razones concretas de seguridad o rendimiento
    • Se pregunta si de verdad hace falta un proxy inverso
  • Servidor web en C hecho por él mismo: Creó un servidor web en C con el que operó sitios comerciales

    • Manejaba mucho tráfico con 128MB de RAM y 1 CPU
    • Menciona que internet era menos hostil hace 20 años
    • Los bots cumplen una gran función como fuzzer, pero aun así hace falta fuzzing real
  • La satisfacción de construir servicios: Es muy satisfactorio construir servicios básicos usando la API del sistema

    • Le sorprende el alto rendimiento de la función poll()
    • Las funciones por conexión, las estructuras relacionadas y los arreglos son similares a nginx, redis y memcached
    • Gran trabajo
  • 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

    • Tiene integradas funciones como gestión de certificados ACME, Pgsql, curl y WebSocket
    • Permite compilar y ejecutar módulos mezclando Lua/Python con C
  • Comparte un enlace interesante: La instancia de althttpd de sqlite.org maneja más de 500 mil solicitudes HTTP al día

    • Sirve 200GB de contenido desde un Linode de $40 al mes
    • El 19% de las solicitudes HTTP acceden al repositorio de código fuente Fossil mediante CGI
  • La diversión de crear tus propias herramientas: Está cansado de la idea de que todo tiene que estar "probado en batalla"

    • Los errores se pueden corregir
  • Charla del Chaos Communication Congress: Esto le recordó una charla sobre un blog/servidor web escrito en C con funciones de seguridad incluidas

    • Incluye funciones como almacenamiento inmutable, reducción de privilegios e imposibilidad de acceso a certificados TLS
  • 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

    • Se pregunta cómo afectan al rendimiento las funciones innecesarias del software
    • Felicita al desarrollador