Rust-lernen.de

Sichere und leichtgewichtige Webanwendungen mit Rust CGI

Letzte Änderung: 09.05.2024

Du willst eine einfache Webseite mit etwas Serverfunktionalität erstellen und selbst hosten? Hier lernst du, wie man das recht einfach und sicher mit dem Apache HTTP Server und Rust realisieren kann.

Rust CGI

Ziel

Wenn du gerne selbst Webseiten und Dienste im Internet betreibst, hattest du vielleicht schon mal den Wunsch, eine kleine Website zu erstellen, die

Es geht hier nicht um einen großen Funktionsumfang, es soll vielmehr eine leichtgewichtige Anwendung werden. Wir befinden uns aber im Internet und sind damit unweigerlich jeder Menge (meist automatisiert ablaufender) Angriffsversuche ausgesetzt, die Anwendung soll also möglichst sicher sein.

Technischer Ansatz

Wir bauen im Wesentlichen eine statische Website mit ein wenig Serverfunktionalität. Dazu verwenden wir einen Apache HTTP Server zum Ausliefern der statischen Website und ein in Rust geschriebenes CGI-Programm für die Funktionalität auf Serverseite. Diese werden wie folgt miteinander interagieren:

Kommunikation zwischen Browser, Apache HTTP Server und CGI-Programm

Beides kann später auf einem selbstgehosteten Server (Virtual Private Server (VPS) oder Root-Server) installiert werden und wäre damit rund um die Uhr verfügbar.

Was ist CGI?

CGI ist die Abkürzung für Common Gateway Interface. Es handelt sich um eine Schnittstelle zum Datenaustausch zwischem einem Webserver (hier der Apache HTTP Server) und einem anderen Programm. Den Standard gibt es schon seit den 1990er Jahren.

Der Webserver kann eine eingehende HTTP-Anfrage an ein CGI-Programm weitergeben und dessen Antwort als HTTP-Antwort zurücksenden, er fungiert dabei als Proxy. Die Datenübergabe an das CGI-Programm erfolgt über Umgebungsvariablen und dem Standardeingabestrom (stdin), die Antwort wird über den Standardausgabestrom (stdout) übermittelt.

Vorteile von CGI-Programmen sind, dass sie in einer beliebigen Programmiersprache geschrieben sein können, also auch in Rust, und dass das Programm nur dann ausgeführt wird, wenn eine entsprechende HTTP-Anfrage eingeht. Das macht den Ansatz sehr leichtgewichtig und ressourcenschonend.

Nachteile sind, dass bei jedem HTTP-Aufruf ein separater Betriebssystem-Prozess gestartet wird und sich das CGI-Programm jedes Mal initialisieren muss, also z.B. jedes Mal erst eine Datenbankverbindung aufbauen muss, was nicht sehr schnell ist. Daher eignet sich CGI nicht für hohe Benutzerzahlen. In unserem Fall handelt es sich aber nur um eine kleine Website und ist daher kein Problem.

Ein CGI-Programm mit Rust schreiben

Zum Schreiben eines CGI-Programms sind keine umfangreichen Rust-Kenntnisse nötig. Mithilfe des Crates rust-cgi geht das ganz einfach, wie nachfolgendes Beispiel zeigt.

Datei main.rs

use chrono;
use rust_cgi;
use sysinfo;

fn main() {
    rust_cgi::handle(|request: rust_cgi::Request| -> rust_cgi::Response {
        let body = request.body();
        let response_str = handle_intern(body);
        rust_cgi::text_response(200, response_str)
    });
}

fn handle_intern(command: &Vec<u8>) -> String {
    let command_str = match std::str::from_utf8(command) {
        Ok(command_str) => command_str,
        Err(_) => return format!("Error reading request body as UTF-8"),
    };

    match command_str {
        "time" => chrono::Local::now().format("Current time: %Y-%m-%d %H:%M:%S").to_string(),
        "memory" => format!("Free RAM: {} bytes", sysinfo::System::new_all().free_memory()),
        other => format!("Unknown command '{other}'"),
    }
}

Über die Funktion rust_cgi::handle() erhalten wir Zugriff auf die Anfrageparameter und können die Antwort zurückgeben. In unserem Fall müssen wir nur das übermittelte Kommando einlesen. Wenn wir das Kommando time erhalten, geben wir die aktuelle Serverzeit zurück. Beim Kommando memory antworten wir mit dem aktuell freien Arbeitsspeicher.

Fehlerbehandlung: Allgemein sollte man bei CGI-Programmen in Rust darauf achten, dass diese möglichst nicht abstürzen, d.h. mit panic enden, weil die Webseite dann keine Antwort bekommt (außer HTTP Status 500). Besser ist es, den Fehlerfall explizit mit zu berücksichtigen und eine entsprechende Antwort zurückzugeben. In unserem Fall ist die Antwort stets nur eine Zeichenkette, um das Programm möglichst einfach zu halten.

Abhängigkeiten in der Datei Cargo.toml

[package]
name = "server-status"
version = "0.1.0"
edition = "2021"

[dependencies]
chrono = "0.4.37"
rust-cgi = "0.7"
sysinfo = "0.30.12"

Das CGI-Programm testen

Das Tolle an der CGI-Schnittstelle ist, dass CGI-Programme direkt von der Konsole aus aufgerufen und getestet werden können.

Senden wir zunächst das Kommando time an unser Programm, indem wir unter Linux eine Konsole öffnen und folgendes eintippen:

echo time | CONTENT_LENGTH=4 cargo run

Beim CGI-Protokoll werden die HTTP-Anfrageheadereinträge als Umgebungsvariablen übertragen; in unserem Fall bedeutet CONTENT_LENGTH=4, dass der HTTP-Body vier Bytes lang ist. Der Inhalt des HTTP-Body wird über den Standardeingabestrom (stdin) übertragen, was wir mit echo time | erreichen.

Die komplette HTTP-Antwort wird über den Standardausgabestrom (stdout) zurückgegeben und in der Konsole angezeigt:

Status: 200 OK
content-length: 33
content-type: text/plain; charset=utf-8

Current time: 2024-05-09 08:15:34

Die ersten drei Zeilen bilden den HTTP-Antwortheader. Dieser enthält u.a. den HTTP-Statuscode 200, die Anzahl der Bytes der Antwort (33) sowie die Kodierungsart der Antwort, in unserem Fall schlichter Text in UTF-8-Kodierung. Nach der Leerzeile kommt der HTTP-Body mit der Serverzeit.

Wiederholen wir das Ganze mit dem Kommando memory:

echo memory | CONTENT_LENGTH=6 cargo run

Hier sieht die Antwort wie folgt aus:

Status: 200 OK
content-length: 27
content-type: text/plain; charset=utf-8

Free RAM: 1128456192 bytes

Die Antwort enthält die Größe des freien Arbeitsspeichers in Bytes, in diesem Fall etwa 1,1 GB.

Webseite erstellen

Als Nächstes erstellen wir eine HTML-Seite mit einer Auswahl für das zu sendende Kommando und eine Schaltfläche zum Senden des Kommandos. Das Ergebnis wird im DIV-Element darunter angezeigt. Auf ein CSS-Layout der Seite verzichten wir der Einfachheit halber.

Datei status.html

<html>
<head>
    <title>Status</title>
    <script type="text/javascript" src="script.js" async></script>
</head>
<body>
    <select id="command">
        <option value="time">Server time</option>
        <option value="memory">Free RAM</option>
    </select>

    <button id="button">Call CGI</button>

    <div id="response"></div>
</body>
</html>

Und nun das JavaScript, mit dem wir das ausgewählte Kommando an das CGI-Programm schicken und die Antwort anzeigen:

Datei script.js

const commandSelect = document.getElementById('command');
const button = document.getElementById('button');
const responseDiv = document.getElementById('response');

button.addEventListener('click', function(event) {
    const command = commandSelect.value;
    const options = {
        method: 'POST',
        body: command
    };
    fetch('/cgi-bin/server-status', options)
        .then(response => response.text())
        .then(text => {
            responseDiv.innerText = text;
        });
});

Das Kommando wird über die Fetch API des Browsers an den Server geschickt. Wir wählen die HTTP-Methode POST, damit wir das Kommando im HTTP-Body übermitteln können. Der URL-Pfad /cgi-bin/server-status signalisiert dem Webserver, unser CGI-Programm aufzurufen.

Apache HTTP Server konfigurieren

Nun kommt der Apache HTTP Server ins Spiel. Damit dieser die Website ausliefert und über eine spezielle URL unser CGI-Programm ausführt, müssen wir ihn entsprechend konfigurieren. Nachfolgend eine Beispielkonfiguration:

Datei httpd.conf

# ... Grundeinstellungen wie Logging usw.

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Options None
    Require all granted
</Directory>

LoadModule cgid_module modules/mod_cgid.so
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
<Directory "/usr/local/apache2/cgi-bin">
    AllowOverride None
    Options ExecCGI
    Require all granted
</Directory>

DocumentRoot gibt das Verzeichnis mit den statischen Dateien der Website an. Mit LoadModule wird die Unterstützung für die CGI-Schnittstelle aktiviert. ScriptAlias gibt an, wo nach den CGI-Programmen gesucht werden soll, die über den URL-Pfad /cgi-bin/ angesprochen werden. Der Abschnitt Directory setzt Sicherheitseinstellungen und aktiviert die Ausführung von CGI-Programmen im Verzeichnis cgi-bin.

Statische Webseite und CGI-Programm deployen

Die Dateien index.html und script.js kopieren wir nun in das Verzeichnis /usr/local/apache2/htdocs.

Wir wollen eine möglichst kleine Binärdatei bekommen, daher kompilieren wir unser Rust-Programm mit cargo build --release. Dadurch erhalten wir eine Binärdatei von etwa 1 MB Größe anstatt 15 MB inkl. Debugging-Symbole. Die Binärdatei server-status kopieren wird dann in das oben konfigurierte Verzeichnis /usr/local/apache2/cgi-bin.

Webseite testen

Nun testen wir die Webseite, indem wir im Browser die URL http://localhost:8080/status.html öffnen und darüber unser CGI-Programm aufrufen:

Uhrzeit vom CGI-Programm Freier Speicher vom CGI-Programm

Wunderbar, es funktioniert!

Fazit

Mit der Kombination Apache HTTP Server und Rust lassen sich Webanwendungen mit einfacher Server-Funktionalität schnell, sicher und leichtgewichtig realisieren, ohne dafür Java oder PHP zu benötigen.

Obiges Programmbeispiel lässt sich leicht um weitere Funktionen wie den Versand einer E-Mail oder den Zugriff auf eine Datenbank erweitern, ohne dass man sich dazu in ein umfangreiches Webframework einarbeiten muss.

Fragen und Antworten

Warum implementieren wir nicht alles in Rust?

Prinzipiell könnten wir anstelle des Apache HTTP Servers auch ein Rust-Webframework wie Actix, Axum, Rocket und warp verwenden, um die statischen Dateien auszuliefern, und bräuchten dann auch nicht über die CGI-Schnittstelle nachzudenken. Wer schon viel mit Rust gemacht hat, geht möglicherweise diesen Weg. In diesem Blog steht aber ein möglichst leichtgewichtiger Ansatz im Vordergrund, der bewährte Basistechnologien nutzt und mit einfachen Rust-Kenntnissen gut und sicher zu realisieren ist.

Kann ich auch nginx verwendet?

Nein, nginx unterstützt kein CGI. Der Webserver nginx unterstützt zwar FastCGI, was allerdings deutlich komplizierter und damit für unseren Einsatzzweck wenig geeignet ist.

Quellen