2 Punkte von GN⁺ 2024-09-26 | 1 Kommentare | Auf WhatsApp teilen

Die Technologie meines Blogs

Dieser Webserver ist ein minimalistischer Webserver, der dafür entwickelt wurde, meinen Blog zu hosten. Er wurde von Grund auf robust gebaut, sodass er dem öffentlichen Internet standhalten kann. Ein Reverse Proxy ist nicht erforderlich. Eine funktionierende Demo gibt es unter http://playin.coz.is/index.html. Ich habe auf Reddit dazu aufgerufen, ihn zu hacken, und dabei Gigabytes an unterhaltsamen und bösartigen Request-Logs gesammelt. Einen Teil davon habe ich in attempts.txt gespeichert und werde später aus Spaß noch mehr durchsehen.

Aber ... warum?

Ich baue gern meine eigenen Werkzeuge und habe es satt, ständig zu hören, alles müsse erst „battle-tested“ sein. Was, wenn es crasht? Bugs kann man beheben.

Spezifikationen

  • Nur für Linux
  • Implementiert HTTP/1.1, Pipelining und Keep-Alive-Verbindungen
  • HTTPS-Unterstützung (mit BearSSL bis TLS 1.2)
  • Minimale Abhängigkeiten (libc und BearSSL bei Verwendung von HTTPS)
  • Konfigurierbare Timeouts
  • Access-Logs, Crash-Logs, Log-Rotation und Begrenzung der Festplattennutzung
  • Kein Transfer-Encoding: Chunked (antwortet mit 411 Length Required, damit der Client erneut mit Content-Length sendet)
  • Single-Core (soll geändert werden, wenn ich einen besseren VPS bekomme)
  • Kein statisches Datei-Caching (noch nicht)

Benchmarks

Der Schwerpunkt dieses Projekts liegt auf Robustheit, aber langsam ist es keineswegs. Ein einfacher Vergleich mit nginx (statischer Endpoint, beide Single-Thread, Limit von 1K Verbindungen):

  • (blogtech)

    $ wrk -c 500 -d 5s http://127.0.0.1:80/hello
    
    • Durchschnittliche Latenz: 6.66ms
    • Requests/s: 76974.24
    • Transfer/s: 6.09MB
  • (nginx)

    $ wrk -c 500 -d 5s http://127.0.0.1:8080/hello
    
    • Durchschnittliche Latenz: 149.11ms
    • Requests/s: 44227.78
    • Transfer/s: 8.27MB

nginx-Konfiguration:

worker_processes 1;
events {
  worker_connections 1024;
}
http {
  server {
    listen 8080;
    location /hello {
      add_header Content-Type text/plain;
      return 200 "Hello, world!";
    }
  }
}

Build und Ausführung

Standardmäßig wird der Server nur mit HTTP gebaut:

$ make

Dieser Befehl erzeugt die ausführbaren Dateien serve (Release-Build), serve_cov (Coverage-Build) und serve_debug (Debug-Build). Der Release-Build lauscht auf Port 80, der Debug-Build auf Port 8080.

Um HTTPS zu aktivieren, muss BearSSL geklont und gebaut werden:

$ mkdir 3p
$ cd 3p
$ git clone https://www.bearssl.org/git/BearSSL
$ cd BearSSL
$ make -j
$ cd ../../
$ make -B HTTPS=1

Es werden dieselben Binärdateien erzeugt, aber sichere Verbindungen sind dann über Port 443 (Release) oder 8081 (Debug) möglich. Die Dateien cert.pem und key.pem müssen im selben Verzeichnis wie die Binärdatei liegen. Um Namen und Speicherort zu ändern, passe Folgendes an:

#define HTTPS_KEY_FILE "key.pem"
#define HTTPS_CERT_FILE "cert.pem"

Um HTTPS lokal zu testen, erstelle ein selbstsigniertes Zertifikat (und den privaten Schlüssel):

openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
openssl req -new -x509 -key key.pem -out cert.pem -days 365

Verwendung

Der Server liefert statische Inhalte aus dem Ordner docroot/ aus. Um das zu ändern, passe die Funktion respond an:

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 :|"));
}

Hier können Endpoints hinzugefügt werden, indem auf das Feld request.path verzweigt wird. Der Pfad ist lediglich ein Slice des Request-Buffers. Die URI wird nicht geparst.

Tests

Ich lasse den Server regelmäßig mit valgrind und Sanitizern (Address, Undefined) laufen und bearbeite ihn mit wrk. Außerdem ergänze ich in tests/test.py automatisierte Tests, um die Konformität mit der HTTP/1.1-Spezifikation zu prüfen. Ich hoste damit meine Website und poste sie hier und da, um weiter Last zu erzeugen. All die Bots im Internet, die verwundbare Websites scannen, sind hervorragende Fuzzer.

Bekannte Probleme

  • Der Server antwortet HTTP/1.0-Clients mit HTTP/1.1

Beiträge

Ich arbeite hauptsächlich im DEV-Branch und merge gelegentlich nach MAIN. Wenn du einen Pull Request öffnest, ist es einfacher, DEV als Ziel zu wählen.

Zusammenfassung von GN⁺

  • Dieses Projekt ist ein Webserver, der auf minimale Abhängigkeiten und Robustheit abzielt.
  • Er unterstützt HTTP/1.1 und HTTPS und bietet verschiedene Logging-Funktionen sowie konfigurierbare Timeouts.
  • Die Benchmark-Ergebnisse zeigen schnellere Antwortzeiten als nginx.
  • Er ist so konzipiert, dass Entwickler Freude daran haben, ihre eigenen Werkzeuge zu bauen und Bugs zu beheben.
  • Ähnliche Projekte mit vergleichbaren Funktionen sind Nginx und Apache HTTP Server.

1 Kommentare

 
GN⁺ 2024-09-26
Hacker-News-Kommentare
  • Kein Reverse Proxy nötig: Mit Jetty ließ sich die App ohne Reverse Proxy problemlos im Internet bereitstellen

    • Viele raten zum Einsatz eines Reverse Proxy, ohne konkrete Gründe in Bezug auf Sicherheit oder Performance zu nennen
    • Es wird infrage gestellt, ob ein Reverse Proxy wirklich nötig ist
  • Selbst entwickelter C-Webserver: Es wurde ein C-Webserver gebaut, der für kommerzielle Websites im Einsatz war

    • Mit 128 MB RAM und 1 CPU wurde viel Traffic bewältigt
    • Es wird erwähnt, dass das Internet vor 20 Jahren weniger feindselig war
    • Bots sind großartige Fuzzer, aber echtes Fuzzing ist trotzdem nötig
  • Zufriedenheit beim Aufbau von Services: Es ist sehr befriedigend, grundlegende Services mit System-APIs zu bauen

    • Es überrascht, wie leistungsfähig die Funktion poll() ist
    • Funktionen pro Verbindung sowie zugehörige Strukturen und Arrays ähneln nginx, redis und memcached
    • Tolle Arbeit
  • Vorstellung eines kleinen Projekts: Ein interessantes Projekt, das in der Freizeit begonnen wurde

  • Empfehlung für das Kore-Framework: Wenn es unangenehm ist, den öffentlich exponierten Teil einer C-App selbst zu schreiben, wird das Kore-Framework empfohlen

    • ACME-Zertifikatsverwaltung, Pgsql, curl, WebSockets und weitere Funktionen sind integriert
    • Module lassen sich mit Lua/Python und C gemischt bauen und ausführen
  • Teilen eines interessanten Links: Die althttpd-Instanz von sqlite.org verarbeitet täglich mehr als 500.000 HTTP-Anfragen

    • Auf einem Linode für 40 $/Monat werden 200 GB an Inhalten ausgeliefert
    • 19 % der HTTP-Anfragen greifen per CGI auf das Fossil-Source-Code-Repository zu
  • Freude am Bau eigener Tools: Es besteht Ermüdung gegenüber der Meinung, dass alles „battle-tested“ sein müsse

    • Bugs kann man beheben
  • Vortrag auf dem Chaos Communication Congress: Es wird an einen Vortrag über einen in C geschriebenen Blog-/Webserver mit Sicherheitsfunktionen erinnert

    • Enthalten sind Funktionen wie unveränderlicher Speicher, Privilegienreduktion und kein Zugriff auf TLS-Zertifikate
  • Stabile Website: Eine Website, die nicht abstürzt, selbst wenn sie auf der Startseite erscheint

  • Zurück zu den Grundlagen: Der Ansatz gefällt, zu den Grundlagen zurückzukehren und nur das Nötige zu verwenden

    • Es wird hinterfragt, wie sich unnötige Funktionen in Software auf die Performance auswirken
    • Glückwünsche an den Entwickler