Wer ein wenig mehr über die Kommunikation zwischen Webbrowser und HTTP-Server erfahren möchte, der baut sich mit einfachen Mitteln einen eigenen Webserver und schaut diesem genau über die Schulter.
Einen Webserver selbst zu programmieren, mag erst einmal als ein ebenso schweres wie überflüssiges Unterfangen erscheinen. Tatsächlich gibt es unter Linux zahlreiche frei verfügbare Webserver, von bekannten Allroundern wie Apache oder Nginx bis hin zu leichtgewichtigen Alternativen wie Cherokee oder Lighttpd (“Lighty”).
Allerdings erfordert nicht jede Situation einen ausgefeilten Webserver. Will man einfach nur lokal im eigenen Netzwerk ein paar HTML-Seiten teilen oder die Möglichkeit schaffen, Dateien hochzuladen, dann genügen dazu Linux-Bordmittel. Als passendes Rahmenkonstrukt eignet sich ein schlichtes Shell-Skript, das auf jedem System vorhandene Tools aus dem GNU-Fundus steuert. Die Netzwerkkommunikation übernimmt dabei das oft als Schweizer Taschenmesser für TCP/IP apostrophierte Netcat [1].
Bei so einem Projekt ist es am besten, an der Wurzel zu beginnen. Da ein Webserver eben ein Server ist, muss er ständig an einem bestimmten Port mithören und bei Anfragen passende Antworten geben. In der Regel lauschen Webserver bei normalen Anfragen am Port 80, wobei dieser Port im Allgemeinen nur HTTP-Anfragen ohne Verschlüsselung entgegennimmt. Der Webserver, den wir im Folgenden erstellen, wird auf den Ports 8080 und 8081 mithören und nur unverschlüsselt kommunizieren. Falls Sie eine Firewall nutzen und den Server im lokalen Netzwerk testen wollen, sollten Sie daran denken, diese beiden Ports für den Webserver freizuschalten.
Eigenbau
Ein Webserver braucht einen Root-Ordner, aus dem er die auszugebenden HTML-Dateien lädt. Zudem benötigt er noch ein Verzeichnis, in dem er hochgeladene Dateien ablegen kann. Die entsprechende Konfiguration definieren Sie zunächst über eine Reihe einfacher Variablen zu Beginn des Server-Skripts (Listing 1). Darüber hinaus müssen Sie die Verzeichnisse und auch die FIFO-Datei entweder manuell oder mithilfe des Test-Builtins der Bash erstellen. Wie das gelingt, demonstriert die fertige Lösung server6.sh, die Sie im Download-Bereich zu diesem Artikel finden.
Listing 1
Konfiguration
HTTP_HOME=http_home
HTTP_UPLOAD=${HTTP_HOME}/upload
CACHE_DATEI=${HTTP_UPLOAD}/dateizumbearbeiten
FIFO_GET=fifo_get
HTTP_GET_PORT=8080
HTTP_POST_PORT=8081
MEINE_IP=$(ip addr show enp2s0 | grep -Eo "([0-9]{1,3}\.){3}[0-9]+" | sed 1q)
In der letzten Zeile von Listing 1 sehen Sie dann noch, dass auch die eigene IP-Adresse wichtig ist. Die Angabe des Netzwerkgeräts (im Beispiel das Ethernet-Interface enp2s0 müssen Sie gegebenenfalls für Ihr System anpassen. Wenn ein Webbrowser eine Datei über ein Webformular abschicken will, braucht er eine Zieladresse. GET-Anfragen sind die einfachere Variante. Wenn ein Browser eine solche Anfrage stellt, dann erwartet er als Antwort einfach nur den Inhalt einer Webseite, die er dann im Browser-Fenster darstellt.
Passende Dateien für das Testen des Webservers lassen sich skriptgesteuert erstellen. Die Funktion aus Listing 2 durchläuft dazu sieben Mal eine For-Schleife. Darin verwendet die Routine ein Here-Dokument (dritte Zeile), um HTML-Code fast 1:1 eingeben zu können. In solchen Heredocs kann man Bezug auf die in der For-Anweisung gesetzte Variable nehmen, die dann einfach nur die laufende Nummer enthält.
Listing 2
Beispieldateien
function create_files () {
for x in {1..7}; do
cat <<-FILE > ${HTTP_HOME}/datei${x}.html
<html><head><meta charset="utf-8">
<title>Seite ${x}</title>
</head><body>
<p> $( date ) </p>
<p> Seite ${x} </p>
</body></html>
FILE
done
}
Auf diesem Weg erzeugen Sie mit wenigen Zeilen Code so viele HTML-Dateien, wie Sie brauchen. Optional integrieren Sie weitere dynamische Inhalte, die Sie per Skript innerhalb des Here-Dokuments erzeugen. Auch die HTML-Datei, die das Upload-Formular enthält, wird auf diesem Weg in einer anderen Funktion erzeugt.
Am besten testen Sie das Erstellen der HTML-Dateien gleich einmal. Nach einem erfolgreichen Test haben Sie den ersten Meilenstein zur Entwicklung eines Webservers geschafft und können an die Entwicklung weiterer Komponenten gehen. Das Skript im Download-Bereich enthält auch noch eine Funktion, die alle im Root-Ordner enthaltenen HTML-Dateien mit CSS-Code [2] im Header ausschmückt.
GET-Anfragen
Stellt ein Webbrowser eine GET-Anfrage, will er einfach nur Daten zurückbekommen. Wie das aussieht, lässt sich stark vereinfacht austesten: Um etwa zu sehen, was ein Browser macht, wenn man sich nicht an das HTTP-Protokoll hält, führen Sie auf der Kommandozeile echo "hallo Firefox" | netcat -l 8080 aus. Dann geben Sie in der Adresszeile des Webbrowsers die URL localhost:8080 ein. Daraufhin sehen Sie, dass Firefox wartet, wartet und weiter wartet – es fehlt offenbar etwas.
Nichtsdestotrotz können Sie aber im Terminalfenster prüfen, wie die Anfrage von Firefox im Klartext aussieht, da der Browser sie bereits an Netcat geschickt hat. Er wartet nun darauf, einen Header und auch HTML als Antwort zu erhalten. Netcat liefert jedoch nichts dergleichen. Mit [Strg]+[C] kappen Sie die Verbindung zu Firefox. Im Browser-Fenster sollte dann der Text aus dem Echo-Kommando erscheinen (Abbildung 1).
Möchten Sie jetzt wissen, wie eine Antwort von einem echten Webserver aussieht, dann werfen Sie einmal einen Blick in die Ausgabe des Befehls wget --spider -S "https://www.zeit.de/index". Das ist ein Spider-Request von Wget, bei dem es nur darum geht, die Antwort eines Servers herauszufiltern. Der informiert in der ersten Zeile mit HTTP/1.1 200 OK, dass er mit einer HTTP-Anforderung zufrieden ist. Mit weiteren Zeilen in Form von Wertepaaren (etwa Connection: keep-alive, Content-Length:) sendet er zusätzliche Informationen oder Anweisungen zurück.
Es scheint sich bei diesem Dienst auch um einen gut abgesicherten Webserver zu handeln, denn er gibt nicht preis, um was für ein Server-Programm es sich genau handelt. Viele Server outen sich an dieser Stelle zum Beispiel als Server: nginx – nicht ratsam, weil es Angreifern die Arbeit erleichtert. Rein technisch gesehen ist die Information auch gar nicht notwendig.
FIFO-Datei
Netcat steht auf nahezu jedem Linux-System zur Verfügung und lässt sich mit etwas Kreativität für viele Zwecke verwenden, wobei es auch seine Grenzen hat. Basisoperationen im Netzwerk lassen sich damit gut emulieren, komplexe Interaktionen dagegen nur schwer oder gar nicht. Den Plan, Apache oder Nginx mit Netcat Konkurrenz machen zu wollen, muss man definitiv aufgeben.
Wenn Sie möchten, dass Netcat dauerhaft an einem Port lauscht und auch verschiedene Antworten versenden kann, müssen Sie es in einer Schleife mit einer FIFO-Datei kombinieren. Das Kürzel FIFO steht für das Prinzip “first in, first out”. Die Informationen kommen also in derselben Reihenfolge wieder aus der Datei, in der sie hineingeschrieben wurden [3]. Listing 3 stellt das exemplarisch dar. Später zeigen wir noch eine Möglichkeit, diese Schleife per Log-Funktion zu optimieren.
Listing 3
Netcat-Antwort
while true; do antworten < $FIFO_GET | netcat -l $HTTP_GET_PORT > $FIFO_GET done
Die FIFO-Datei sorgt dabei für eine bessere Kommunikation zwischen Netcat und der Funktion antworten, die Listing 4 zeigt. Netcat lauscht am angegebenen Port und schreibt in die FIFO-Datei. Auf der linken Seite der Pipe sehen Sie den Aufruf der Funktion, die die Anfrage des Browsers aus der FIFO-Datei herausliest. Sie wertet sie aus und schickt dann durch die Pipe hindurch eine passende Antwort zu Netcat zurück, die einen HTML-Header und HTML-Daten enthält. Die Funktion antworten entscheidet, was an den Browser zurückgegeben wird.
Listing 4
FIFO-Datei
function antworten () {
read get_or_post adresse httpversion
if [ ${#adresse} = 1 ]; then
list_dir
elif [ ${#adresse} -gt 1 ]; then
return_file $adresse
fi
}
Diese Variante stellt bereits eine ziemlich leistungsfähige Lösung dar. Beträgt die Länge der Browser-Anfrage 1 (Zeile 3), dann lautet sie /, falls nicht (Zeile 5), dann soll eine Datei aus dem Root-Verzeichnis wiedergegeben werden. Um den Webserver dazu zu veranlassen, dass er die im Root-Ordner enthaltenen Dateien wiedergibt, genügt erst einmal ein ganz schlichtes ls Verzeichnis. Das Resultat gilt es dann aber noch in passenden HTML-Code einzubetten, damit der Browser etwas damit anfangen kann und auch Links funktionieren (Abbildung 2). Anderenfalls käme keine richtige Interaktion zwischen dem Server und dem Browser zustande. Um eine Auflistung eines Verzeichnisses in HTML-Code umzuwandeln, empfiehlt sich der Einsatz des Stream-Editors Sed [4].

Abbildung 2: Der selbst gebaute Webserver liefert jetzt den Inhalt des Root-Verzeichnisses als Listing aus.
Listing 5 zeigt die Funktionen, die für die Rückgabe des Inhalts des Root-Ordners und auch einzelner Seiten darin sorgen. Die Ausgabe des Verzeichnisinhalts übernimmt in der Funktion list_dir (ab Zeile 1) ein einfacher Ls-Befehl, dessen Resultat Sed dann in lupenreines HTML verwandelt. Die durch die Funktion aus Listing 2 erzeugten Dateien, die im Root-Ordner liegen, enthalten bereits HTML-Code. Der Server gibt sie über die Funktion return_file ab Zeile 19 mit einem passenden Header an den Browser zurück.
Listing 5
Ausgaben
function list_dir () {
local ausgabe=$( ls --hide=upload -1 $HTTP_HOME | sed -r '
1 i<html><head><meta charset="utf-8"><title>Inhalt</title></head>\
<body style="margin: 45px; font-family: sans-serif">
s#(.*)#<li><a href="http://1">\1</a></li>#
$ a</body></html>
' )
local content_length="Content-Length: $( cat <<<$ausgabe | wc --bytes )"
cat <<<$ausgabe | sed '
1 i HTTP/1.1 200 OK
1 i Server: Dein GET-SERVER
1 i Connection: close
1 i '"$content_length"'\n
'
}
function return_file () {
inhalt=$( cat ${HTTP_HOME}/${1:1} )
if [[ $? -eq 0 ]]; then
laenge=$( cat <<<${inhalt} | wc --bytes )
cat <<<${inhalt} | sed -r '
1 i HTTP/1.1 200 OK
1 i Server: Dein GET-SERVER
1 i Connection: close
1 i Content-Length: '"$laenge"'\n'
else
cat <<-FEHLER
HTTP/1.1 404 Not Found
Connection: close
Content-Length: 44
Die angeforderte Seite gibt es leider nicht!
FEHLER
fi
}
Dadurch, dass Netcat in der Schleife fortlaufend für Anfragen zur Verfügung steht und einen Header sowie das passende HTML dazu versendet, meint ein Browser im lokalen Netzwerk es mit einem echten Webserver zu tun zu haben.
Es kann aber auch der Fall auftreten, dass der Anwender im Browser manuell eine Seite anfordert, die es gar nicht gibt. Das führt zum berüchtigten Fehler 404, den Sie bestimmt schon einmal im Web gesehen haben [5]. Auch der Eigenbau-Webserver kann mit diesem Feature aufwarten. Falls der Cat-Befehl in der ersten Zeile der Funktion return_file (Zeile 20) einen Fehler auslöst, wird der Else-Zweig ab Zeile 28 ausgeführt. Im Webbrowser erscheint dann die Meldung, dass die angefragte Seite nicht existiert.
POST-Anfragen
Im Gegensatz zu GET-Anfragen, bei denen der Webbrowser Dateien heruntergeladen möchte, gibt es auch POST-Anfragen, die ein Browser an einen Webserver senden kann. Stellen Sie sich das so vor, als wollten Sie etwas auf einem Social-Media-Portal posten. Sie geben Text, Bilder oder auch Videos in eine dafür vorgesehene Box ein und drücken anschließend auf Posten. Der jeweilige Inhalt wird dann zum Server hochgeladen und erscheint anschließend auf Ihrem Profil. Unser einfacher Server verfolgt erst einmal nur den Zweck, Dateien von einem Browser aus hochzuladen und im Ordner uploads/ zu speichern.
Auch hier sendet der Browser einen Header, in dem er angibt, dass er einen Post machen will. Sie können ganz einfach selber testen, wie so eine Post-Anfrage aussieht, indem Sie das Kommando aus Listing 6 ausführen. Anschließend rufen Sie im Browser das Webformular im Root-Ordner auf und senden eine Datei (Abbildung 3). Nach ein paar Sekunden unterbrechen Sie das Netcat-Kommando mit [Strg]+[C]. Im Browser erscheint die Meldung Datei angekommen, und die Datei befindet sich dort, wohin Sie umgeleitet wurde. Allerdings handelt es sich noch um keine anzeigbare JPEG-Datei, da das gespeicherte File zum Beispiel noch den Header enthält (Abbildung 4).
Listing 6
POST-Simulation
$ echo "Datei angekommen" | netcat -l 8081 > upload/dateix.jpg

Abbildung 4: Die Upload-Anfrage enthält noch zahlreiche Daten, die nicht zur hochgeladenen Datei gehören.
Listing 7 zeigt, wie Sed überschüssige Daten wegschneidet, die nicht zu der hochgeladenen Datei gehören. Diese Aufgabe übernimmt der in der While-Schleife ab Zeile 9 enthaltene Code. Er entfernt den Header, Boundary-Angaben, den Dateinamen und Ähnliches. Um zu vergleichen, wie die Daten ursprünglich ausgesehen haben, werfen Sie einen Blick in die Cache-Datei, die sich ebenfalls im Upload-Ordner befindet. Würde Sed nicht alles Unnötige wegschneiden, könnte das Betriebssystem die empfangenen Dateien nicht korrekt darstellen.
Listing 7
Ausfiltern
function run_post_server () {
message_for_post='HTTP/1.1 200 OK
Content-Length: 17
Connection: close
Datei angekommen'
while true; do
cat <<< $message_for_post | netcat -l $HTTP_POST_PORT > "${CACHE_DATEI}"
neuer_name=$( sed -r -n '/filename/{ s/(.*)(filename=")(.+)(".*)/\3/; p}' ${CACHE_DATEI} )
upload_path="${HTTP_UPLOAD}/${neuer_name}"
sed '1,/filename/d;/Content-Type/{N;d};$d' ${CACHE_DATEI} > "${upload_path}"
done
}
run_post_server &
Die Routine ruft im Hintergrund zudem die Funktion run_post_server auf (letzte Zeile). Sie enthält eine passende Antwort für POST-Anfragen, die die Länge des Inhalts in Bytes angibt sowie die Informationen enthält, dass nach dem Lesen die Verbindung gekappt werden soll. Ohne diese Angaben würde Firefox die Verbindung einfach halten, obwohl die Daten schon gesendet wurden. Die Funktion startet im Hintergrund (&), damit sie nicht alles blockiert, sobald Dateien gesendet werden.
Ungeprüft
Auch wenn ein Webbrowser explizit nach dem Root-Verzeichnis oder einer anderen Datei fragt, kann der Webserver im Prinzip zurückschicken, was auch immer Sie wollen – Sie müssen es für den Browser nur dementsprechend deklarieren. Ein Beispiel dafür zeigt Listing 8. Der Browser zeigt in diesem Fall beim Aufruf von localhost:8080 oder IP-Adresse:8080 sofort eine JPEG-Datei an, ohne sich darüber zu beschweren.
Listing 8
JPEG-Datei senden
#!/bin/bash
header="HTTP/1.1 200 OK"
datei="http_home/upload/IMG-20220213-WA0002.jpg"
content_length="Content-Length: $( cat $datei | wc --bytes )"
content_type="Content-Type: image/jpeg"
cat $datei | sed -r -e "1 i $header" -e \
"1 i $content_length" -e \
"1 i $content_type" -e \
"1 i Connection: close\n" | netcat -l 8080
Interessant daran ist nicht nur, dass das funktioniert: Es stellt auch eine potenzielle Sicherheitslücke dar. Offenbar überprüfen die meisten Webbrowser überhaupt nicht, ob die Anforderung im GET-Request und die zurückgelieferte Seite zusammenpassen. Im diesem Fall hat der Browser nach der Indexseite des Webservers verlangt und stattdessen eine JPEG-Datei erhalten. Das ist ungefähr so, als würde ein Tennisspieler von seinem Gegenspieler auf einmal mit einem Basketball beworfen.
Erfahrungsgemäß sind solche Eigenheiten und auch andere Features, die man möglicherweise implementieren möchte, im Web nur unzureichend oder auch gar nicht dokumentiert [6]. Deswegen hilft es, mitzuschneiden, was Firefox und andere Browser anfragen. Die Funktion aus Listing 9 ist für den Start des Servers verantwortlich. Sie sehen dort zwei tee-Umleitungen, die alle Daten zum Debuggen in eine Log-Datei weiterleiten. In diesem Protokoll finden Sie dann mit Datum und Uhrzeit versehen, was jeweils der Webbrowser als Anfrage gesendet und der Server als Antwort darauf zurückgeschickt hat (Abbildung 5). So können Sie jede Anfrage und Antwort analysieren und nachvollziehen, was genau während der Kommunikation zwischen Client und Server geschieht.
Listing 9
Webserver aufrufen
function run_server () {
while true; do
date | sed -r 's/^|$/\n/g' >> debug
antworten < $FIFO_GET | tee --append debug |
netcat -l $HTTP_GET_PORT |
tee --append debug > $FIFO_GET
done
}

Abbildung 5: Ein Ausschnitt aus der Debug-Datei für den Port 8080. Genauer lässt sich die Kommunikation zwischen Webserver und Webbrowser nicht mitverfolgen.
Viele Browser fragen zum Beispiel nach dem berühmten favicon.ico, wenn sie etwas länger mit einem Server kommuniziert haben. Dabei handelt es sich um das Icon, das Sie meist oben in den Tabs der Browser sehen. Es liegt normalerweise im Root-Ordner des Webservers.
Falls Sie möchten, dass auch Ihr Eigenbau-Server ein Favicon zur Verfügung stellt, müssen Sie erst herausfinden, wie eine entsprechende Anfrage des Browsers aussieht, um dann den Server passend reagieren zu lassen. Dass der Webbrowser des Öfteren nach dieser Datei fragt, können Sie an der Fehlermeldung cat: http_home/favicon.ico: Datei oder Verzeichnis nicht gefunden in der Protokolldatei erkennen. Der Browser fordert die Icon-Datei also eigenständig an.
Fazit
Wie Sie sehen, lässt sich mit einfachen Mitteln schnell ein rudimentärer Webserver zusammenbauen. Der hier vorgestellte Webserver ist zwar schlicht und einfach konstruiert, soll aber auch gar nicht mit Boliden wie dem Apache-Webserver oder Nginx konkurrieren. Er besitzt jedoch Fähigkeiten, die ein typischer Webserver nicht aufweist: Er kann auf das ganze Repertoire von Shell-Befehlen zugreifen und auf diesem Weg mit minimalem Aufwand Informationen lokal im eigenen Netzwerk darstellen. Auch die Ressourcen, die das kleine Skript verbraucht, halten sich in engen Grenzen. Als Info-Server im eigenen Netzwerk kann sich der Eigenbau also sehen lassen, und er taugt auch dazu, Dateien von einem Rechner auf den anderen Rechner zu befördern – insgesamt sicher keine schlechte Lösung für das heimische Netz. (cla)
Glossar
-
Here-Dokument
-
Heredocs dienen in vielen Programmiersprachen zur Definition von Textabschnitten. Im Unterschied zur herkömmlichen Ausgabe per
echooderprintfbleiben Zeilenumbrüche, Einzüge und einige Sonderzeichen im Text erhalten. Die Bash erlaubt zudem die Verwendung von Variablen innerhalb des Heredocs.
Infos
-
Warteschlange: https://de.wikipedia.org/wiki/Warteschlange_(Datenstruktur)
-
HTTP-Statuscodes: https://de.wikipedia.org/wiki/HTTP-Statuscode
-
MDN Web Docs – HTTP: https://developer.mozilla.org/de/docs/Web/HTTP







