4. Einen Webserver programmieren

4.2 Der Server baut die HTML-Seiten

Übung

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);
Vorsicht, Sicherheitslücke! Lesen Sie bitte Zeilen 30-35 oben im Code.
    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    
        }   
Warum lassen wir nicht zu, dass der Dateiname den Substring '..' 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.
Wenn es so aussieht, ist etwas falsch.
Wenn es so aussieht, wurde der Server erfolgreich kontaktiert.

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.

Übung Ändern Sie den Quelltext web-server-static.js unseres Servers, sodass der Server, wenn / 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 von countries 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.

Übung Implementieren Sie folgende Änderungen zu Ihrer obigen Lösung:
  • 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?
Übung

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

    s = "beispielwort";
    s.slice(5);
    'ielwort'
können Sie einen Suffix eines Strings berechnen. Zum Beispiel aus /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.

Übung Schreiben Sie eine 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.