Einfache CGI-Programme in C
CGIs sind heute selten geworden, aber in kleinen Embedded Systems haben sie immer noch ihre Berechtigung. Dies weil sie wenig Resourcen wie RAM und und Platz auf dem Disk belegen. Im Vergleich zu anderen Ansätzen sind sie relativ langsam, aber noch auf einem Raspberry mit einem Apache brauchen sie nur wenige Millisekunden zum Antworten.
Natürlich ist es nicht sinnvoll, die Funktionen eines grossen Webframeworks mit CGIs nachzubilden. Das ist zwar im Prinzip möglich, nur ist der Aufwand für eine solche Lösung jenseits von gut und böse. Für Statusabfragen ganz ohne oder mit einem einzelnen Parameter sind sie dagegen sehr gut geeignet.
Gerade solche Abfragen gibt es bei Embedded Systems häufig, weil verlangt wird dass die HTML-Seite immer den aktuellen Status anzeigt. Das wird meistens mit AJAX realisiert. Ein Script im Browser pollt den Server periodisch und setzt die neusten Werte in die Seite ein.
Was ist ein CGI-Programm überhaupt?
Ein Programm das für einen Webserver Ausgaben produziert. Wenn der Webserver erkennt dass eine Anfrage ans CGI geht, dann startet er das Programm. Den Output vom CGI schickt er an den Client der die Anfrage gestellt hat.
Per Default interpretieren die meisten Webserver den Aufruf einer Seite im Pfad /cgi-bin/* als Request für ein CGI. Das kann selbstverständlich beim Server nach Belieben umkonfiguriert werden.
Bei meinem Apache 2.4.17 auf einem Debian-System liegen die CGIs im Filesystem unter /usr/lib/cgi-bin.
Das CGI in muss also in dieses Directory kopiert werden, und zudem für den User www-data ausführbar sein.
Hello CGI world!
CGIs sind viel einfacher zu schreiben als die meisten Leute denken. Der folgende Code ist eine Erweiterung des weltbekannten Programms von Kernighan und Ritchie:
#include <stdio.h>
int main() { printf( "Content-type: text/plain\r\n\r\n" ) ; printf( "hello, world\n" ) ;
return 0 ; }
Das einzig spezielle ist die Zeile mit dem Content-Type. Der Webserver erwartet diese Ausgabe von einem CGI, sonst meldet er einen Fehler. "text/plain" habe ich in diesem Fall gewählt damit der Browser den Text direkt anzeigt. Für AJAX nutze ich meistens "application/json".
Ebenfalls ungewöhnlich ist der Zeilenumbruch mit "\r\n". Der HTTP-Standard schreibt das eigentlich so vor, aber manche Webserver sind tolerant genug um auch einzelne Newlines zu akzeptieren.
Beachten Sie auch, dass es zwei aufeinander folgende Newlines sind. Die Leerzeile die dadurch entsteht, signalisiert dem Webserver das Ende des Headers. Nach der Leerzeile folgt der Inhalt der Meldung.
Im Erfolgsfall sollte ein CGI 0 zurückgeben damit der Webserver weiss, dass das Programm seine Aufgabe korrekt erfüllt hat.
Das Environment
Vor dem Start des Programmes setzt der Webserver das Environment für das CGI auf. Mit der Funktion getenv können diese Variablen abgefragt werden. Vor der Abfrage muss man aber erst mal wissen, welche Variablen des Webserver liefert. Einige Variablen sind durch eine Konvention festgeschrieben, andere sind serverspezifisch.
Das nächste Programm listet diese Variablen aus:
#include <stdio.h> #include <unistd.h> int main( ) { printf( "Content-type: text/plain\r\n\r\n" ) ; for( int i = 0; environ[i] != NULL; i++ ) printf( "%s\r\n", environ[i] ) ;
return 0 ; }
Beim oben erwähnten Apache gibt das folgende Ausgabe:
HTTP_HOST=192.168.5.69 HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 HTTP_ACCEPT_LANGUAGE=en-us HTTP_CONNECTION=keep-alive HTTP_ACCEPT_ENCODING=gzip, deflate HTTP_USER_AGENT=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SERVER_SIGNATURE=Apache/2.4.17 (Debian) Server at 192.168.1.34 Port 80 SERVER_SOFTWARE=Apache/2.4.17 (Debian) SERVER_NAME=192.168.5.69 SERVER_ADDR=192.168.5.69 SERVER_PORT=80 REMOTE_ADDR=192.168.5.79 DOCUMENT_ROOT=/var/www/html REQUEST_SCHEME=http CONTEXT_PREFIX=/cgi-bin/ CONTEXT_DOCUMENT_ROOT=/usr/lib/cgi-bin/ SERVER_ADMIN=webmaster@localhost SCRIPT_FILENAME=/usr/lib/cgi-bin/printenv REMOTE_PORT=49183 GATEWAY_INTERFACE=CGI/1.1 SERVER_PROTOCOL=HTTP/1.1 REQUEST_METHOD=GET QUERY_STRING= REQUEST_URI=/cgi-bin/printenv SCRIPT_NAME=/cgi-bin/printenv
Die Leerzeile ist tatsächlich so drin, sie gehört zur Server-Signature.
Parameter
Viele Status-CGIs benötigen gar keine Parameter, weil sie einfach einen bestimmten Wert (oder mehrere davon) zurückliefern.
Wenn es einen Parameter braucht, dann kann man ihn einfach ans URL anhängen, und der Server legt ihn im Environment als QUERY_STRING ab:
REQUEST_METHOD=GET QUERY_STRING=parameter REQUEST_URI=/cgi-bin/printenv?parameter SCRIPT_NAME=/cgi-bin/printenv
Parameter mit Werten sind ebenfalls möglich. Den Webserver kümmert das nicht, er legt den String so ab wie er kommt. Das CGI muss ihn also selber zerlegen.
REQUEST_METHOD=GET QUERY_STRING=parameter=fridolin REQUEST_URI=/cgi-bin/printenv?parameter=fridolin SCRIPT_NAME=/cgi-bin/printenv
Wenn man weitere Elemente an die URL anhängt, dann erkennt der Server das Programm immer noch und liefert die den Teil hinter dem CGI in PATH_INFO.
REQUEST_METHOD=GET QUERY_STRING= REQUEST_URI=/cgi-bin/printenv/with/options SCRIPT_NAME=/cgi-bin/printenv PATH_INFO=/with/options PATH_TRANSLATED=/var/www/html/with/options
Diese beiden Varianten kann man sogar kombinieren:
REQUEST_METHOD=GET QUERY_STRING=parameter REQUEST_URI=/cgi-bin/printenv/with/options?parameter SCRIPT_NAME=/cgi-bin/printenv PATH_INFO=/with/options PATH_TRANSLATED=/var/www/html/with/options
Bitte kein Caching
Besonders die älteren Versionen des Internet Explorers cachten die Ausgabe der CGI-Programme. Das führte dazu, dass der XMLHttpRequest nur einmal ausgeführt wurde, und der Browser bei der nächsten Statusabfrage einfach auf die Daten im Cache zugriff.
Durch zusätzliche Headerzeilen kann dieses Caching verhindert werden:
printf( "Content-type: application/json\r\n" ) ; printf( "Expires: Tue, 03 Jul 2001 09:00:00\r\n" ) ; printf( "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n" ) ; printf( "Pragma: no-cache\r\n\r\n" ) ;