4. Einen Webserver programmieren

4.4 Einfacher Webserver mit express.js

Im letzten Abschnitt haben wir mehreres "von Hand" gemacht:
  1. Angeforderte Dateien haben wir direkt mit data = fs.readFileSync(filename, { encoding: 'utf8', flag: 'r' }); aus dem Dateisystem des Servers eingelesen und dann per response.write(data); den Response geschrieben.
  2. Wenn wir HTML-Seiten dynamisch erstellt haben, z.B. aus Daten aus einer .json-Datei eine HTML-Tabelle erstellt haben, haben wir im Server-Quelltext dargestellt, wie unser HTML-Text zusammengeschnippelt werden soll.

Punkt 2 ist mühsam und schlechte Praxis: wir haben Inhalt (HTML-Schnipsel und Formatierung) nicht von der Server-Logik (wie und wann unser Server die Daten einliest) getrennt. In der Praxis hieße das wohl auch, dass verschiedene Entwickler nicht unabhängig und parallel an Server und Webseitendesign arbeiten könnten. Auch müssen wir immer, wenn wir das Format der Webseite ändern

Punkt 1 hat uns direkt in eine potentiell gravierende Sicherheitslücke geführt. Hätten wir nicht darauf geachtet, Pfade, die den Substring .. enthalten, explizit zu verbieten, dann hätte ein Angreifer von außen das gesamte Dateisystem unseres Rechners ausspionieren können.

Deshalb: Verwenden Sie, wo es nur geht, Bibliotheken. Gerade im Web-Engineering gibt die zu Hauf, gerne auch mal Frameworks genannt. Da dies eine Lehrveranstaltung ist, die Ihnen die Grundprinzipien beibringen soll, und weil sich die Bibliotheken und Frameworks auch alle paar Jahre ändern, habe ich beschlossen, anfangs aber gerne so weit wie möglich auf ausgefeilte Frameworks zu verzichten. Nun aber, da wir ein paar Webserver geschrieben haben und sie per Hand Requests haben beantworten lassen, können wir eine Abstraktionsstufe nach oben steigen und alles nochmal mit dem Framework express.js machen.

express.js installieren

Erschaffen Sie ein Verzeichnis simple-express-server, gehen in das Verzeichnis, legen eine (gerne noch leere) Datei express-server.js an und geben dann ein:
npm init
npm führt Sie dann durch eine Folge von Konfigurationsschritten. Sie dürfen gerne bei allem einfac ENTER drücken, dann vergibt npm init einfach die Werte, die es für sinnvoll hält. Wenn es fertig ist, hat npm init eine Konfigurationsdatei package.json erstellt. Rufen Sie jetzt auf
npm install express
Öffnen Sie die Datei express-server.js und kopieren folgenden Code hinein:
const express = require('express');
const path = require("path");
const portnumber = 4009;
const server = express();

server.use(express.static("."));

server.listen(portnumber, function ()  {
    console.log('listening at port ' + portnumber);
});
Kopieren Sie ein paar andere Dateien in den gleichen Order, eine index.html und gerne auch Bilder oder pdfs, einfach um zu testen, ob der Server funktioniert. Starten Sie den Server:
node express-server.js                         
listening at port 4009
Testen Sie jetzt den Server, in dem Sie in Ihrem Browser die Adresse http://localhost:4009 eingeben. Der Server express-server.js bedient das Verzeichnis, in dem sein eigener Quelltext liegt, daher ist er auch bereit, angefragte Dateien in diesem Ordner an Ihrem Browser zu senden. Das Verzeichnis, dass der Server bedient, legen wir in Zeile 6 fest:
server.use(express.static(".")); 

Sie können "." durch anderes ersetzen. Üblicherweise würde man nicht das Verzeichnis bedienen lassen, in dem der eigene Quelltext liegt, sondern ein Unterverzeichnis public_html oder public oder www anlegen, und Zeile 6 in server.use(express.static("public_html")); oder dementsprechend ändern.

Auch die Konvention, dass bei einem Request des URLs / die Datei /index.html geschickt wird, versteht express.js. Wir müssen dieses Verhalten also nicht explizit programmieren.

Sicherheitslücken? Probieren Sie, ob Sie den Server austricksen können, indem Sie einen URL mit .. im Pfad anfragen:
nc localhost 2000                            
GET /express-server.js HTTP/1.1 

                        
Der Server sollte jetzt mit HTTP/1.1 200 OK, dem Header und dem Inhalt von express-server.js antworten. Stellen Sie jetzt sicher, dass ein es im Verzeichnis oberhalb von express-simple-server eine Datei warning.txt mit einem kurzen Text als Inhalt gibt. Versuchen Sie, express-server.js dazu zu bringen, Ihnen warning.txt zu liefern:
nc localhost 2000                            
GET /../warning.txt HTTP/1.1 
                            
Sehen Sie, express-server.js fällt nicht drauf rein und liefert HTTP/1.1 404 Not Found zurück. Dies ist ein wichtiger Grund, lieber Bibliotheken zu verwenden, als selbst das Rad neu erfinden zu wollen: die Entwickler einer Bibliothek haben wahrscheinlich an mehr Sicherheitslücken gedacht als wir. Wenn Sie also das Package express verwenden, um einen Server zu schaffen, dann sind Sie auf der sicheren Seite. Sehen Sie nur zu, dass Sie auf keinen Fall
server.use(exrpess.static("/"));
in Ihren Code schreiben. Denn dann gibt der Server alles frei her, was auf Ihrem Rechner liegt (oder streng genommen alles, worauf der Benutzer, der den Prozess node express-server.js gestartet hat, Zugriff hat).
Geben wir unserem Server jetzt ein wenig mehr Funktionalität. Wir wollen nicht nur angefragte Dateien zurückzuliefern sondern für / und /index.hmtl ein besonderes Verhalten festlegen. Der Server soll mit einer einfachen Meldung, mit der genauen Uhrzeit und der nachgefragten URL antworten. Den veränderten Quelltext sehen Sie hier, wobei ich die Änderungen rot markiert habe:
const express = require('express');
const path = require("path");
const portnumber = 4010;
const server = express();

server.use(express.static("."));

server.get('/date', sendDate);
function sendDate(req, res) {
    let answer = 'This is express-server.js ';
    answer += 'This is a dynamic page that has been created on ' + (new Date()).toString() + '. ';
    answer += 'You have just requested the URL ' + req.url;
    res.send(answer);
}

server.listen(portnumber, function ()  {
    console.log('listening at port ' + portnumber);
});

In Zeile 8 legen wir fest, was der Server tun soll, falls er einen HTTP-GET-Requeste auf /date bekommt. Das zweite Argument von get ist eine Funktion, hier createAndSendIndexHtml, die beschreibt, wie mit Request und Response Sie umzugehen ist. Wir sehen hier wieder das funktionale Programmierparadigma. Kopieren den obigen Code in express-server.js, starten Sie den Server per node express-server.js und geben Sie im Browser in die Adresszeile

ein. Sie sollten jetzt die versprochene Begrüßungsmeldung des Servers sehen. Ein technisches Detail: wenn wir einen URL mit Query-String eingeben, zum Beispiel dann sehen Sie trotzdem die gleiche Meldung. Wir sehen also: server.get('/index.html', yourFunction) ruft yourFunction auf, falls der Pfad der angefragten URL /index.html ist; ein angehängter Query-String stört nicht weiter und kann natürlich in der Implementierung von yourFunction sinnvoll weiterverwendet werden.
Übung Legen Sie eine Kopie von express-server.js an und erweitern Sie diese, dass Sie, wenn der Pfad der URL /search ist, die Liste der Parameter-Wert-Paare im Query-String als Tabelle darstellt. Das Ergebnis soll in etwa so aussehen:

Tip. Sobald Sie den Query-String in ein Javascript-Objekt query umgewandelt haben, können Sie im Code darauf zugreifen, z.B. mit query.type. Allerdings wissen Sie ja nicht, ob query überhaupt den Schlüssel "type" besitzt (erinnern Sie sich: Javascript-Objekte sind Lookup-Tables, keine structs wie in C). Mit dem folgenden Code können Sie über alle Schlüssel iterieren:

    for (let key in query) {
        console.log("Schlüssel = " + key + ", Wert = " + query[key]);
    }

Tip. Wenn Sie größere Blöcke von HTML-Schnipseln in Ihrem Javascript-Quelltext verwenden wollen, dann wird es Ihnen entgegenkommen, dass Javascript multiline strings unterstützt. Diese markieren Sie mit je einem `backtick` vorne und hinten:

    var htmlTop = `
<!DOCTYPE html>
<html>    
    <head>
      <title>Wandelt Query-String in Tabelle um</title>
      <link rel="stylesheet" href="styles-dominik.css">
      <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>   
    <body>    
      <div class="container">
        <h1>Parameter Ihrer URL-Anfrage auf ` 

Für diese Aufgabe müssen Sie kein HTML-Formular mit Texteingabefeldern schreiben. Geben Sie den URL wie im obigen Screenshot einfach per Hand in die Adresszeile ein und denken Sie sich einen schönen Query-String aus. Beachten Sie, dass URLs im Browser keine Leerzeichen enthalten dürfen und durch percent encoding mit %20 dargestellt werden (Ihr express-Server ersetzt die %20 dann wieder automatisch in Leerzeichen, wie oben beim flying unicorn; Sie müssen sich server-seitig also nicht drum kümmern.)