CGI (Common Gateway Interface)

Configuration Apache

S'assurer que le module est chargé

Dans Apache/conf/hhtpd.conf Charger le module CGI (ligne 103)
LoadModule cgi_module modules/mod_cgi.so

Indiquer le dossier des CGI

Ce n'est pas forcément bon pour la sécurité de pouvoir lancer des exécutables depuis n'importe où. Mieux vaut garder tous les exécutables en dehors du répertoire de publication.
	Apache/htdocs
	Apache/cgi-bin

C'est pourquoi on restreint les exes au dossier cgi-bin en utilisant un scriptAlias.

Par exemple cette URL
http://www.domaine.fr/cgi-bin/script
cherchera le fichier local
/usr/local/apache2/cgi-bin/script
Pour changer ce dossier éditer Apache/conf/httpd.conf ligne 375
ScriptAlias /cgi-bin/ "${SRVROOT}/cgi-bin/"
Apache est livré avec un script Perl /cgi-bin/printenv.pl


En dehors de dossiers avec alias

# To use CGI scripts outside of ScriptAliased directories:
# (You will also need to add "ExecCGI" to the "Options" directive.)
#
#AddHandler cgi-script .cgi .pl

# For type maps (negotiated resources):
#AddHandler type-map var

<Directory "/usr/local/apache2/htdocs/somedir">
    Options +ExecCGI
</Directory>

#Fichier HTACCESS
Si pas accès à httpd.conf, un .htaccess peut activer un CGI sur un répertoire:

<Directory "/home/*/public_html/cgi-bin">
    Options ExecCGI
    SetHandler cgi-script
</Directory>

Premier programme CGI

1. Conformément au protocole HTTP, le programme doit envoyer un champ HTTP Content-Type pour indiquer au client le type de contenu qu'il va recevoir

2. Séparer l'en-tête HTTP du corps avec deux lignes vides. En langage C
#include <stdio.h>

int main(int argc, char** argv)
{
    printf("Content-type: text/plain\n\n");
    printf("Hello, World.");
    return 0;
}
Compiler et tester le programme CGI en ligne de commande:
	D:\Lab\Langages\C>gcc -Wall -s -o CGI.exe CGI.c

	D:\Lab\Langages\C>CGI
	Content-type: text/html

	Hello, World.
	D:\Lab\Langages\C>
	
Copier l'exécutable dans le dossier cgi-bin et tester:
http://localhost/cgi-bin/CGI.exe
Bien entendu on aurait pu lui donner l'extension .cgi, ou l'enlever
http://localhost/cgi-bin/prog.cgi
http://localhost/cgi-bin/prog

La plupart du temps le serveur tourne sous Linux.
La démarche est la même, sauf que l'exécutable n'a pas d'extension, et l'interpréteur Perl est installé. Aussi l'exécutable doit être compilé sous Linux. Le code est le même.
Si le script produit un erreur, Apache sortira une 500 server internal error.
Le script doit avoir les bons droit de fichier (Exec +X).
chmod a+x hello.pl
(Et l'interpréteur Perl doit être installé!)
Si besoin regarder le log d'erreurs Apache.


Traiter une requête GET

Lorsqu'Apache reçoit une requête HTTP, il crée des variables d'environnement. Printenv.pl livré avec Apache les affiche. phpinfo() les indique. La valeur d'une variable d'environnement s'obtient en invoquant getenv().
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    char* data;
    data = getenv("QUERY_STRING");
    printf("Content-type: text/html\n\n");
    printf("query string reçu:[%s]", data);
    return 0;
}
Tester http://localhost/cgi-bin/CGI?a=test&b=2

Il faut parser la chaîne. Avec une boucle ou sscanf()

Les valeurs sont transmises au programme via des variables d'environnement:
D:\Lab\Langages\C>CGI?a=test&b=1
'CGI?a' n’est pas reconnu en tant que commande interne
ou externe, un programme exécutable ou un fichier de commandes.
'b' n’est pas reconnu en tant que commande interne
ou externe, un programme exécutable ou un fichier de commandes.
On aurait pu simuler en créant deux variables d'environnement, puis appeler le CGI le tout dans une console, sans serveur web. Un formulaire GET ou un lien envoie deux champs:
<form action="cgi-bin/viewdata.cgi">
	nom: <input type="text" name="a" value="test"/>
	âge: <input type="text" name="b" value="1"/>
	<input type="submit" value="Valider"/>
</form>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    char* data;
    char str[200];
    int i = 0;

    printf("Content-type: text/html\n\n");

    data = getenv("QUERY_STRING");

    if(data == NULL)
        printf("<b>Error in passing data from form to script.</b>");

    // La partie %[^&] du spécificateur de format va extraire chaque caractère qui
    // n'est pas un &. Il s'arrètera d'extraire dès qu'il rencontrera un &.

    else if(sscanf(data,"a=%[^&]&b=%d",str,&i)!=2)
        printf("<b>Error! Invalid data. Must be ?a=str&b=int</b>");

    else
    printf("La valeur de a est %s et b %d.",str,i);
      
    return 0;
}
La longueur du tableau str dépend de la taille maximale d'une URL: c'est 255 octets en GET.

Traiter une requête POST

Pour développer le script et simuler Apache, définir une variable d'environnement
D:\Apache\cgi-bin>set a="<!DOCTYPE html>\r\n<html>\r\n <head>\r\n  <title>test</title>\r\n </head>\r\n<body>\r\n<form action="index.php">\r\n <input type=\"text\" name=\"test\" value=\"ok\" required/>\r\n</form>\r\n</body>\r\n</html>"
D:\Apache\cgi-bin>echo %a%
"<!DOCTYPE html>..."
<form action="/cgi-bin/CGI4.exe" method="post">
	Titre: <input type="TEXT" name="a" value="test"/>
	Article: <textarea name="b">un gros
message</textarea> <input type="submit"/> </form>
Titre:
Article:
Dans ce cas lire la valeur de la variable d'environnement CONTENT_LENGTH
pour savoir exactement combien d'octets le programme CGI va devoir lire sur stdin:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    char* lenstr = getenv("CONTENT_LENGTH");

    printf("Content-type: text/html\n\n");
    printf("La valeur de CONTENT_LENGTH est %s.",lenstr);
     
    return 0;
}
Lit CONTENT_LENGTH octets sur stdin et les renvoie au client
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    char* buffer;
    unsigned long len;

    len = strtoul (getenv("CONTENT_LENGTH"), NULL, 0);
    buffer = (char*) malloc (len+1);
    if (buffer==NULL) return 1;

    fgets(buffer, len+1, stdin);
    buffer[len] = 0;

    printf("Content-type: text/html\n\n");
    printf("La longueur est %ld.<br>\n",len);
    printf("POST DATA [%s]",buffer);

    free(buffer);
    return 0;
}
Ce qui donne
La longueur est 24.
POST DATA [a=test&b=un+gros%0D%0Amessage]
C'est une suite de paires nom=valeur séparées par &.
Les valeurs sont urlencodées: les espaces sont +. %0D%0A c'est 13 10 \r\n

On peut parser la chaîne avec sscanf(),
ou splitter la chaîne strtok() sur le caractère &,
(la chaîne est donc lue 2 fois)
// avec strtok()
char* pch= strtok (buffer,"&");
while (pch != NULL)
{
    printf ("[%s]<br>\n",pch);
    pch = strtok (NULL, "&");
}
ou à la main lire caractère par caractère et rechercher =, &.
(la chaîne lue une seule fois, ce qui peut s'avérer intéressant pour les très gros uploads)
// lit caractère par caractère à la recherche de = et &
char *mot;
unsigned long b = 0;
unsigned long n;

mot = (char*) malloc (len+1);
if (buffer==NULL) return 1;

for(n=0; n<len; n++)
{
    if(buffer[n]=='=')
    {
        // tient un nom
        mot[b] = 0; b=0;
        printf ("nom:[%s]<br>\n",mot);          
    }
    else if(buffer[n]=='&')
    {
        // tient une valeur
        mot[b] = 0; b=0;
        printf ("valeur:[%s]<br>\n",mot);          
    }
    else mot[b++] = buffer[n];
}
// le dernier
mot[b] = 0; b=0;
printf ("valeur:[%s]<br>\n",mot);
Ce qui donne:
nom:[a]
valeur:[test]
nom:[b]
valeur:[un+gros%0D%0Amessage]
A ce stade le parser peut transformer l'urlencodage
si c'est un + -> mettre un espace
si c'est un % -> les deux caractères suivants sont le chiffre hexa du caractère
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    char* buffer;
    unsigned long len;

    len = strtoul (getenv("CONTENT_LENGTH"), NULL, 0);
    buffer = (char*) malloc (len+1);
    if (buffer==NULL) return 2;

    fgets(buffer, len+1, stdin);
    buffer[len] = 0;

    printf("Content-type: text/plain\n\n");

    // chaîne reçue:[a=test&b=un+gros%0D%0Amessage]
    // lit caractère par caractère
    // à la recherche de = & %
    char *mot;
    unsigned long b = 0;
    unsigned long n;

    mot = (char*) malloc (len+1);
    if (buffer==NULL) return 1;

    for(n=0; n<len; n++)
    {
        if(buffer[n]=='=') // tient un nom
        {
          mot[b]=0; b=0;
          printf ("nom:[%s]\n",mot);
        }
        else if(buffer[n]=='&') // tient une valeur
        {
          mot[b]=0; b=0;
          printf ("valeur:[%s]\n",mot);
        }
        // remplace + par espace
        else if(buffer[n]=='+') mot[b++] = ' ';
        // remplace valeur %hexa en caractère ASCII
        else if(buffer[n]=='%') // '%0D' ou '%0A'
        {
          char hexStr[5];
          sprintf(hexStr, "0x%c%c", buffer[n+1],buffer[n+2]); // '0X0D'
          int c = (int)strtol(hexStr, NULL, 16); // donne 13
          mot[b++] = c;
          n += 2; // avance pour les survoler
        }
        else mot[b++] = buffer[n];
    }
    // le dernier
    mot[b]=0; b=0;
    printf ("valeur:[%s]\n",mot);

    free(mot);
    free(buffer);
    return 0;
}
Ce qui donne:
nom:[a]
valeur:[test]
nom:[b]
valeur:[un gros

message]
Voila les paires nom=valeur sont isolées et peuvent être traitées: insertion dans une base de données, balisage HTML, ou autre.