Blog der Heimetli Software AG

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" ) ;