4. Einen Webserver programmieren
4.2 Der Server baut die HTML-Seiten
Erstellen Sie ein neues Verzeichnis. Legen Sie in dieses Verzeichnis eine
HTML-Seite, die Sie in den letzten Wochen erstellt haben, vorzugsweise mit
Links auf weitere Seiten. Legen Sie auch eine Datei in einem anderen Format rein,
zum Beispiel README.txt
.
Speichern Sie im gleichen Ordner auch die Datei
web-server-static.js.
Gehen Sie mit dem Terminal in das Verzeichnis und starten Sie den Server:
node web-server-static.js
Now listening on port 3001
Jetzt öffnen Sie einen neun Tab im Browser und geben ein: localhost:3001/index.html. Sie müssten jetzt eine Webseite angezeigt bekommen. Schauen Sie auf das Terminal, wo Ihr Server läuft. Sie müssten jetzt ungefähr folgenden Output sehen:
node web-server-static.js
Now listening on port 3001
requested file: ./index.html
Sie sehen nun also eine erfolgreiche Kommunikation zwischen Client (Browser) und Server ( web-server-static.js). Lassen Sie sich nicht davon stören, dass Client und Server auf dem gleichen Rechner laufen. Etwas spannenderes ist aus Sicherheitsgründen im Rechnerraum leider nicht möglich. Werfen wir einen Blick auf den Quellcode von web-server-static.js. Ich erkläre jeden Abschnitt mit Kommentaren.
/* Als erstes binden wir ein paar Bibliotheken ein
Das Keyword require entspricht also in etwa
dem import in Java oder Python. */
var http = require('http');
var url = require('url');
var fs = require('fs');
// Das Paket http erledigt die schwere Arbeit, also das Parsen des HTTP-Request und erstellen des Response-Headers
myServer = http.createServer(handleRequests);
/* Diese einfache Zeile erschafft einen Webserver. Als Parameter
erwartet http.createServer eine Funktion. Wir übergeben hier die Funktion handleRequests.
Sie hat zwei Parameter, request und response.
Die Semantik ist die folgende: wenn der Server einen HTTP-Request erhält, was ja nur ein String in einem bestimmten Format ist,
dann baut die Bibliothek http aus diesem Request-String ein Javascript-Objekt request,
das alle Informationen schön aufbereitet enthält.
Mit diesen Informationen kann die Funktion handlerequests nun einen Response zusammenbauen
und das Ergebnis in das Argument response, wiederum eine Javascript-Objekt, schreiben.
*/
function handlerequests (request, response) {
var filename = "." + url.parse(request.url, true).pathname;
// HTTP-Pfade beginnen mit einem "/". Um dies in einen Dateisystem-Pfad umzuwandeln, müssen wir einfach noch "." davorhängen
console.log("requested file: " + filename);
if (filename == "./")
filename = "./index.html";
// die Konvention ist, dass bei einem leeren URL-Pfad, also "/", die Datei index.html geliefert wird.
if (filename.includes('..')) {
// wir erlauben keine Requests, die im Dateibaum nach oben gehen
response.writeHead(404, { 'Content-Type': 'text/html' });
return response.end("404 File " + filename + " not found");
// und schicken in dem Fall einen 404-Fehler
}
try {
data = fs.readFileSync(filename, { encoding: 'utf8', flag: 'r' }); // ignorieren sie das zweite Argument; das sind irgendwelche Optionen
response.writeHead(200, {});
// 200 steht für "Erfolg"
response.write(data);
return response.end();
} catch (err) {
// das wird zum Beispiel passieren, wenn eine Datei angefordert wird, die es gar nicht gibt,
// und fs.readFileSync eine Exception wirft.
response.writeHead(404, { 'Content-Type': 'text/html' });
return response.end("404 File " + filename + " not found");
}
}
var portnumber = 3001;
myServer.listen(portnumber);
console.log("Now listening on port " + portnumber);
Warum lassen wir nicht zu, dass der Dateiname den Substringif (filename.includes('..')) {
// wir erlauben keine Requests, die im Dateibaum nach oben gehen
response.writeHead(404, { 'Content-Type': 'text/html' });
return response.end("404 File " + filename + " not found");
// und schicken in dem Fall einen 404-Fehler
}
'..'
enthält?
Was würde passieren, wenn wir die Zeilen 11-14 löschen oder auskommentieren würden?
Vorsicht! Im HSZG-Netz sollten Sie zwar sicher sein. Schalten Sie dennoch lieber Ihre Netzwerkverbindung aus!
Entfernen Sie jetzt die Zeilen 30-35 bzw. kommentieren Sie sie aus, mit
/* ... */
.
Gehen Sie in das parent directory von dem, wo
web-server-static.js
liegt.
Liegt web-server-static.js
zum Beispiel im Verzeichnis
/Users/dominik/web-engineering/static-web-server/
, von nun an
PATH
genannt.
Erschaffen Sie ein Unterverzeichnis tutorial
und legen in diesem eine
Datei
help.txt
an,
öffnen diese, und schreiben in sie
Dies ist die Datei help.txt. Sie liegt eine Ebene unterhalb
web-server-static.js
,
nämlich im
Unterverzeichnis tutorial.
Gehen Sie nun ins Verzeichnis oberhalb von
web-server-static.js
,
dem
obigen
Beispiel folgend also
/Users/dominik/web-engineering/
, und legen dort
eine Datei warning.txt
an, öffnen Sie die und schreiben in sie
Dies ist die Datei warning.txt. Sie liegt eine Ebene oberhalb von
web-server-static.js
Schalten Sie Ihre Netzwerkverbindung aus und starten Sie
web-server-static.js
.
Versuchen wir nun, die Datei warning.txt
bei diesem Server anzufragen.
Der
Server
selbst läuft
in einem Verzeichnis. Wenn wir http://localhost:3001/tutorial/help.txt
in
die
Adresszeile
des Browsers eingeben, so zeigt er uns den Inhalt von help.txt
an.
Genau
so,
wie
wir es
erwarten. Der Server bedient alle Dateien in seinem Verzeichnis und den
Unterverzeichnissen.
Können wir auf warning.txt
zugreifen? Versuchen wir es und geben in die
Adresszeile
vom Browser
http://localhost:3001/../warning.txt
ein.
Was geschieht? Die Datei warning.txt
wird nicht geladen, und in der Tat
löscht
der Browser automatisch die ..
im URL und versucht, die Datei
http://localhost:3001/warning.txt
zu laden. Die gibt es nicht, weswegen
Sie
einen
Fehler
404
bekommen.
Versuchen wir es per Hand. Öffnen Sie ein Terminal und bauen Sie eine Verbindung zu
dem
Webserver
web-server-static.js
auf. Auf Windows:
telnet localhost 3001
und auf OSX
nc localhost 3001
Falls das sofort terminiert, lauscht auf Port 3001 kein Server und irgendwas ist
schiefgegangen.
Geben Sie jetzt den folgenden Befehl für nc
bzw. telnet
ein:
nc localhost 3001
GET /../warning.txt
Drücken Sie nach der Zeile GET /../warning.txt
zweimal ENTER.
Jetzt
sollten
Sie folgenden Response sehen:
HTTP/1.1 200 OK
Date: Tue, 20 Sep 2022 21:13:18 GMT
Connection: close
Dies ist die Datei warning.txt. Sie liegt eine Ebene oberhalb von web-server-static.js
Großartig! Wir konnten web-server-static.js
dazu bringen,
uns
die
Datei
warning.txt im Oberverzeichnis
zu bringen. Ist das jetzt gut oder schlecht? Es ist katastrophal! Es
bringt
nämlich
die Möglichkeit,
von außen über das Internet auf all jene Dateien zuzugreifen, auf die
auch
der
Benutzer Zugriff
hat, der web-server-static.js
gestartet hat. Dies ist als
Directory
Traversal Attack
bekannt. Hier der Wikipedia-Artikel
dazu.
Beenden Sie nun den Prozess web-server-static.js
und
stellen
Sie
Zeilen 30-35 wieder her.
/
oder
/index.html
angefordert wird, nicht versucht, die Datei
./index.html
einzulesen, sondern eine automatisch
generierte Tabelle enthält.
Kopieren Sie hierzu folgende Zeilen in die Datei web-server-static.js:
france = {
"name": "France",
"capital": "Paris",
"continent": "Europe",
"currency": "Euro",
"population": 67897000
};
germany = {
"name": "Germany",
"capital": "Berlin",
"continent": "Europe",
"currency": "Euro",
"population": 83695430
};
japan = {
"name": "Japan",
"capital": "Tokyo",
"continent": "Asia",
"currency": "Yen",
"population": 125927902
}
countries = [germany, france, japan];
Die erzeugte Tabelle soll mehr oder weniger so aussehen:
Ein Lösungsvorschlag wäre zum Beispiel:- Teilen Sie die Datei index.html in zwei Teile: was vor dem
<table>
kommen soll und was danach. - Schreiben Sie eine Funktion
generateTable(countries)
, der über die Elemente voncountries
iteriert und für jeden Eintrag eine Tabellenzeile<tr>
erzeugt. - Der
head
Ihrer generierten HTML-Datei sollte folgende Zeilen enthalten:<head> <title>Diese Webseite wurde vom Server generiert</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> </head>
- Wenn Sie dann noch Ihrem
<table>
-tag die Attribute<table class="table table-striped">
geben, dann sieht die Tabelle einigermaßen gut aus.
In der vorherigen Übungsaufgabe lagen die Daten im Quelltext. Das ist ganz schlechte Praxis. Code und Daten sollten getrennt liegen. Das erreichen wir in der nächsten Übungsaufgabe.
- Lagern Sie die Daten aus der vorherigen Übung, also
[germany, france, japan]
etc., in eine externe Datei countries.json aus. - Schreiben Sie Code, der die Datei countries.json liest und in ein
Javaskript-Objekt
übersetzt,
analog zu dem Object
countries
in der vorherigen Übungsaufgabe. Hierzu müssen Sie natürlich recherchieren, wie man in Node.js eine Datei im JSON-Format einliest und in ein Javascript-Objekt übersetzt. Googeln Sie oder fragen Sie mich!
Experimentieren Sie: fügen Sie in der Datei countries.json
ein
weiteres
Land
hinzu und laden die Seite im Browser neu.
Überprüfen Sie, dass es in der erzeugten Tabelle erscheint, ohne dass Sie den
Quelltext Ihres Servers geändert haben.
Denken Sie über folgende Frage nach:
- Sollten Sie bei jedem empfangenen HTTP-Request der Datei index.html die Datei countries.json öffnen und lesen oder
- sollten Sie es einmal, beim Start des Servers tun, und dann die Daten einfach im Laufzeitspeicher halten?
Die Datei vocabulary.json
enthält zwei Lerneinheiten für fachspezifisches Vokabular auf Deutsch und Englisch.
Schreiben Sie, angelehnt an die zwei vorherigen Aufgaben, einen Server,
der vocabulary.json
einigermaßen attraktiv darstellt.
Die Seite index.html sollte für jede Lerneinheit einen Link enthalten, also in
etwa
<a href="unit/1">Rund ums Fahrrad</a>
.
Wenn der Link geklickt wird, soll als Response dynamisch eine HTML-Seite erstellt
werden,
die den Inhalt der Lerneinheit 1 als Tabelle darstellt.
Der Code Ihres Servers darf von dem Datenformat abhängen, also wissen,
dass die Eigenschaften in vocabulary.json
die Schlüssel "source", "target", "title", "content"
haben, muss
aber unabhängig vom Dateninhalt sein. Wenn ich also manuell eine
Lerneinheit erweitere oder eine neue Lerneinheit hinzufüge, muss Ihre Webseite das so
wiedergeben, ohne dass HTML-Seiten und Server-Quelltext verändert werden müssen.
Hinweis. Mit
können Sie einen Suffix eines Strings berechnen. Zum Beispiel auss = "beispielwort";
s.slice(5);
'ielwort'
/unit/2
den String 2
rausziehen.
Wir haben jetzt also bereits einige Fortschritte erzielt. Allerdings könnten wir alles auch mit statischen HTML-Seiten erreichen und müssten nicht extra einen Server programmieren.
index.html
, die jedes Mal, wenn sie geladen wird,
anders aussieht. Genauer gesagt, schreiben Sie einen Server, der
jedes Mal eine andere index.html
erzeugt. Die Datei
kann zum Beispiel Zeit und Datum enthalten oder eine Zufallszahl.
Übung
Schreiben Sie einen Server, der echt dynamischen Inhalt generiert.
Jedes Mal, wenn /
angefragt wird, soll er IP-Adresse und
Browser des Clients in einer Liste speichern und dann eine Html-Seite
mit einer Tabelle zurückgeben. Die Tabelle soll alle Http-Requests auflisten,
die der Server berarbeitet hat, und zwar wann,
von welcher IP und mit welchem Browser dieser Request geschickt wurde.