<- SL: Intro - Copertina - SL: Postfix ->

Sistemi Liberi


Apache - Seconda puntata

di Umberto Salsi


L'articolo

Si illustra la tecnologia CGI di Apache attraverso una serie di esempi scritti nel linguaggio di shell Bash. Si spiega il formato MIME necessario, come decodificare le query HTTP, come salvare e recuperare dati da un semplice DB realizzato con un file di testo, come gestire le sessioni con i cookie, come eseguire il debugging dei CGI. Si presume che sia stato attivato il meccanismo SUEXEC di Apache come descritto nel precedente articolo.
Gli argomenti trattati sono di interesse per il sistemista che voglia approntare un ambiente di sviluppo WEB multiutente, per il programmatore di applicazioni WEB che cerchi una introduzione all'argomento, e per il WEB designer che voglia integrare aspetti dinamici nei suoi siti.

Indice

Introduzione
CGI in generale
Un primo esempio
Debugging dei CGI
CGI che restituiscono pagine HTML
CGI di test
Metodo GET
Metodo POST
Soluzione generale per GET e POST
Riconoscere i parametri della query HTTP con Bash
Gestire un DB
CGI che ritornano immagini
Sicurezza e diritti di accesso
CGI polimorfi
Sessioni con i cookie
Variabili d'ambiente
Conclusioni
Appendice
Bibliografia
L'autore

Introduzione

Nel precedente articolo abbiamo affrontato alcuni aspetti della configurazione del server WEB Apache. In particolare abbiamo visto come realizzare siti WEB su host virtuale e come realizzare un ambiente di programmazione WEB multiutente protetto.

Questo articolo affronta il protocollo HTTP e il suo interfacciamento con la tecnologia CGI: vedremo come si generano dinamicamente le pagine WEB e come è possibile gestire l'interazione tra il client WEB e il server.

L'articolo si rivolge soprattutto a coloro che hanno scarsa dimestichezza con i vari linguaggi di programmazione. Ecco perché i programmi di esempio sono realizzati in Bash, che dovrebbe essere un minimo comune denominatore tra tutti gli utilizzatori di GNU/Linux e degli altri ambienti simil-Unix sui quali Apache può essere installato, siano essi programmatori, sistemisti o semplici utilizzatori del sistema.

Per la programmazione avanzata di CGI si sono affermati linguaggi di programmazione specifici, ed altri sono stati estesi per supportare questa tecnologia. Perl, PHP, Java sono solo tra i più noti. Tuttavia noi non utilizzeremo questi strumenti specializzati, proprio perché vogliamo andare al cuore della tecnologia e vogliamo indagare sul meccanismo di funzionamento dei CGI. In particolare vedremo:

Il sistemista troverà che gli esempi presentati si prestano per realizzare semplici ma utili programmi di test che diagnosticano problemi di configurazione e comunicazione.
Il programmatore troverà spiegazione del funzionamento di tecnologie che spesso vengono mascherate dall'uso di strumenti di sviluppo specializzati.
L'utilizzatore e il WEB designer che vogliono aggiungere dinamicità alle loro realizzazioni possono trovare qui alcuni suggerimenti sulle possibilità e i limiti del mezzo.

CGI in generale

Per cominciare, una precisazione: la sigla CGI significa Common Gateway Interface, che di per sè non vuol dire molto... Comunque, si tratta delle specifiche di interfacciamento che permettono ai programmi di interagire con il WEB server. Attraverso il WEB server, il programma CGI acquisisce dati dal client remoto ed elabora le risposte appropriate, che poi vengono rispedite al client per la visualizzazione. Essenzialmente, il'interfaccia CGI coinvolge le variabili d'ambiente del processo CGI, il suo standard input e il suo standard output, tutti concetti molto comuni nell'ambiente Unix e affini. La figura qui sotto cerca di schematizzare il percorso dei dati come conseguenza della invocazione di un certo URL da parte del client dove gira il browser WEB (ho omesso le variabili d'ambiente del programma CGI, che non saprei come rappresentare):

                     **********                                       
                    *          *         +------------------------+   
                    * I        *         |      S e r v e r       |   
+----------+        *  n       *         |                        |   
|          |        *   t      *         |   +------+    +------+ |   
|          |------->*    e     *------------>|      |--->|Progr.| |   
|  Client  |        *     r    *         |   |Apache|    |      | |   
|          |<-------*      n   *<------------|      |<---|  CGI | |   
|          |   ^    *       e  *    ^    |   +------+  ^ +------+ |   
+----------+   :    *        t *    :    |             :          |   
               :    *          *    :    |             :          |   
               :     **********     :    +-------------:----------+   
               :                    :                  :              
               :                    :                  :                 
               :..Protocollo HTTP...:                  :..Interfaccia CGI

Il computer client invia la propria richiesta usando il protocollo HTTP, ed ottiene la risposta sempre secondo le specifiche di questo protocollo. Fintanto che il client e il server dialogano usando questo protocollo standard, il client non ha modo di sapere quale tecnologia di programmazione lato server è stata adottata, e neppure è tenuto a saperlo.

Un programma CGI può essere realizzato con un qualsiasi linguaggio di programmazione compilato o interpretato (script) in grado di leggere le variabili d'ambiente e di accettare dati dallo standard input e in grado di scrivere sullo standard output. Per i nostri esempi useremo il linguaggio di scripting Bash, la popolare shell di sistema di GNU/Linux.

Per chiudere il paragrafo, citiamo anche l'Active Server Pages (ASP) come soluzione di interfaccia alternativa alla CGI, adottata dall'Internet Information Server (IIS) di Microsoft. Siccome però il protocollo HTTP è lo stesso, si ritrovano anche in ASP gli stessi concetti che noi vedremo in questo articolo, come le variabili d'ambiente, i flussi dati da e verso il client, le sessioni, ecc.

Un primo esempio

Darò per scontato che Apache sia correttamente installato e configurato, e che sia già predisposto per eseguire programmi CGI sfruttando il meccanismo SUEXEC come descritto nel precedente articolo. Nei nostri programmi CGI di esempio questo ci permetterà di creare file nell'account di un utente. Se il meccanismo SUEXEC non è stato abilitato, la realizzazione delle funzionalità che descrivo richiede la corretta impostazione dei permessi ai file e alle directory per consentire l'accesso all'utente sotto la cui identità gira Apache, che tipicamente è nobody:nobody oppure apache:apache.

Facciamo subito la prova con un CGI minimale scritto in Bash che si limita a stampare la data corrente:

    #!/bin/bash
    echo "Content-Type: text/plain"
    echo
    echo "La data di oggi:"
    date

Scrivere questo testo e salvarlo come /home/pippo/public_html/prova1.cgi. Se il vostro nome utente è diverso da "pippo", apportare i dovuti cambiamenti. Ricordare di rendere eseguibile il programma settando il flag di esecuzione:

    $ chmod +x prova1.cgi

Aprire un browser WEB e puntare all'URL http://localhost/~pippo/prova1.cgi per vedere la pagina generata.

Osserviamo subito quanto segue:

Descriviamo brevemente il meccanismo di chiamata: il client richiede la pagina, Apache individua il file prova1.cgi, scopre che si tratta di un programma per via della estensione .cgi, quindi lo esegue tramite SUEXEC; il CGI viene quindi eseguito con l'identità pippo:users; l'output generato da questo programma viene restituito da Apache al client che ne ha fatto richiesta; il client riconosce l'intestazione MIME per un testo puro, e lo visualizza come tale sullo schermo.

Riguardo al formato MIME ci sono maggiori dettagli nell'articolo dedicato ai protocolli, seconda puntata (PLUTO Journal, marzo 2002, www.pluto.linux.it/journal/pj0203/protocolli2.html). Per quello che ci serve nei nostri esempi, basta sapere che la risposta del server al client deve rispettare una certa sintassi: prima viene l'intestazione MIME, poi una riga vuota, e quindi il corpo del documento; nei nostri esempi l'intestazione contiene il campo Content-Type dove si dichiara il tipo dei dati contenuti nel corpo. Il client farà affidamento su questa informazione per formattare adeguatamente il documento.

Nel seguito perfezioneremo la nostra conoscenza del CGI attraverso una serie di esempi, sempre utilizzando Bash. Purtroppo Bash non ha un supporto integrato per il CGI, per cui dovremo fare tutto "a mano", ma con grande vantaggio dal punto di vista didattico.

Debugging dei CGI

In tutta questa catena di chiamate e passaggi, c'è sempre qualcosa che può andare storto: ecco perché è importante sapersi orientare nel debugging dei programmi CGI.

Quando un programma CGI non funziona, la prima cosa da provare è di avviare il CGI da riga di comando in una sessione terminale:

    $ ./prova1.cgi

E' necessario specificare la directory corrente "./" o in alternativa il path completo del file /home/pippo/public_html/prova1.cgi. Sullo schermo dovrebbe apparire l'output corretto del CGI, che nel nostro caso sarà qualcosa del tipo:

    Content-Type: text/plain

    La data di oggi:
    Sat Feb  9 11:49:32 CET 2002

Conviene anche aprire una finestra terminale addizionale dove mostrare con continuità i messaggi di errore di Apache:

    $ tail -f /var/log/http/*error.log

Conviene lasciare sempre aperta questa finestra mentre si debugga un CGI.

Raccomando di scrivere sempre i programmi direttamente dalla shell dei comandi del computer, quindi dalla consolle, via telnet o attraverso i più moderni programmi come SSH e OpenSSH. Cercare di vincere la tentazione di scrivere i programmi in un altro sistema operativo, come Microsoft Windows o Apple MacOS per poi inviarli con FTP al server:

Ricordo che, oltre ai soliti editor in modalità testuale come vi, pico, jed, mcedit, emacs, ... ci sono anche text editor in modalità grafica X Window come nedit, xemacs, gnome-edit, ...

CGI che restituiscono pagine HTML

Abbiamo visto come un CGI può ritornare un testo puro. Per ritornare una pagina HTML si tratta semplicemente di indicare il tipo MIME corrispondente:

    #!/bin/bash
    echo "Content-Type: text/html"
    echo
    echo "<HTML><BODY>"
    echo "<CENTER>La data di oggi:</CENTER>"
    echo "<CENTER><B>"
    date
    echo "</B></CENTER>"
    echo "</BODY></HTML>"

Salvare questo testo col nome /home/pippo/public_html/prova2.cgi, settare il flag x con

    $ chmod +x prova2.cgi

e richiamarlo col browser con l'URL http://localhost/~pippo/prova2.cgi. Se qualcosa non funziona, fare riferimento alla sezione di debugging precedente. In questo modo possiamo generare dinamicamente una pagina HTML arbitraria.

Miglioramenti suggeriti: il comando date è ricco di funzionalità di formattazione delle date: consiglio di leggere attentamente il manuale on-line per i dettagli ("man date" e soprattutto "info date"). Per esempio,

    date "+%Y-%m-%d, %H:%M"

formatta la data secondo lo standard ISO-8601.

CGI di test

E' utile tenere sempre a disposizione un CGI di test come il seguente:

    #!/bin/bash
    echo "ContentType: text/plain"
    echo
    echo "---------------------------------------------------"
    echo "Identita' del processo:"
    id
    echo "---------------------------------------------------"
    echo "Ecco lo stdin:"
    cat
    echo "---------------------------------------------------"
    echo "Ecco l'env:"
    echo
    env | sort
    echo "---------------------------------------------------"

Salvare questo script col nome test.cgi, renderlo eseguibile e provarlo dal browser. Dovrebbe stampare nell'ordine:

  1. L'identità del processo, cioè il nome utente e il gruppo assegnati al processo CGI, che ne determinano anche i diritti di accesso al sistema. Il nome utente sarà pippo e il gruppo sarà users.
  2. Gli eventuali dati provenienti dallo standard input. Vedremo che Apache può fornire dati al CGI attraverso questo meccanismo.
  3. Le variabili di ambiente (environment) definite da Apache. Una scorsa veloce a queste variabili mostra che il CGI ha a disposizione un sacco di informazioni interessanti: l'indirizzo IP del client, il tipo di browser, ecc. Vedremo le più importanti e il loro uso.

Questo script ci sarà utile per sperimentare con il metodo GET (cioè invocandolo con l'URL dotato di opportuna query) e per sperimentare con il metodo POST (inserendo l'URL dello script come metodo action di un form HTML). Vediamo allora più nel dettaglio come funzionano i metodi GET e POST.

Metodo GET

Il client ha essenzialmente due meccanismi per inviare dati a un programma CGI: il metodo GET e il metodo POST.

Il metodo GET è quello primitivo, ma ancora oggi molto utile: con questo metodo il client invia al CGI alcuni parametri inserendoli dentro all'URL. Questa sequenza di uno o più parametri viene chiamata query perché permette al client di interrogare il CGI. Ricordiamo che l'URL completo di query può apparire ovunque sia consentito di inserire un URL, come per esempio nella entry box del browser, in un'ancora di una pagine HTML, come SRC di una immagine, ecc.
Proviamo il nostro script di test invocandolo con questo URL:

    http://localhost/~pippo/test.cgi?alfa=123&beta=456

La query HTTP è la stringa che segue il carattere speciale "?", ed è costituita da una sequenza di assegnamenti del tipo

        nome=valore

separati tra di loro dal carattere speciale "&" (ampersand). All'inizio della query c'è il carattere ? che la separa dal resto dell'URL.

Notiamo che Apache definisce una variabile d'ambiente:
    REQUEST_METHOD=GET

che ci dice che il metodo è GET, ed inoltre definisce un'altra variabile d'ambiente:

    QUERY_STRING=alfa=123&beta=456

che contiene la query HTTP. Questo, nella sostanza, è il meccanismo di passaggio di dati dal client verso il CGI usando il metodo GET. Sarà compito del programma CGI riconoscere ed estrarre in modo appropriato questi dati. Bash non dispone di un meccanismo automatico per fare questo: vedremo negli esempi seguenti come fare.

Il limite del metodo GET sta nella massima lunghezza dell'URL che possono gestire il client e il server. Generalmente l'URL completo della query non dovrebbe superare poche centinaia di caratteri, sicchè il metodo GET non è adatto per trasportare grandi quantità di dati.

Metodo POST

Il metodo POST differisce dal metodo GET come segue:

Vediamo nel prossimo paragrafo come utilizzare in pratica tutto ciò.

Con il metodo POST non esiste limite alla lunghezza dei dati che si possono inviare, e inoltre questi dati non appaiono mai nella barra degli indirizzi del browser, evitando di confondere il navigante. Non è invece possibile usare il metodo POST per creare URL ``parametrizzati'', come invece abbiamo visto è possibile fare con GET.

Soluzione generale per GET e POST

Creiamo un CGI sotto forma di script Bash in grado di individuare la query HTTP, indipendentemente dal fatto che essa sia stata ritornata col metodo GET o POST. Per riconoscere il meccanismo di passaggio dei parametri con il quale il programma CGI è stato invocato, basta leggere la variabile d'ambiente REQUEST_METHOD. Nel caso del Bash si accede alle variabili d'ambiente del processo semplicemente come se fossero normali variabili. Con altri linguaggi bisogna invece usare apposite funzioni (come getenv() in C e in PHP, o l'array associativo $ENV{} in Perl). Ecco dunque il programma che stampa (nella finestra del browser) la query passata:

    #!/bin/bash
    if [ "$REQUEST_METHOD" = POST ]; then
            query=$( head --bytes="$CONTENT_LENGTH" )
    else
            query="$QUERY_STRING"
    fi
    echo "Content-Type: text/plain"
    echo
    echo "Query=$query"

Due commenti su questo codice:
- se il metodo è POST, ho sfruttato il comando head per ritornare esattamente CONTENT_LENGTH byte dallo standard input;
- altrimenti presumo che il metodo sia GET, e allora la query è contenuta nella variabile d'ambiente QUERY_STRING;
- comunqe sia, salvo la query nella variabile query da dove in seguito il CGI potrà andare ad estrarre di volta in volta i parametri che gli servono.

Salviamo questo programma col nome prova3.cgi, rendiamolo eseguibile e quindi invochiamolo dal client nel solito modo.

Per provare il metodo GET basta invocare il CGI con questo URL:

        http://localhost/~pippo/prova3cgi?alfa=123&beta=456

Per provare il metodo POST bisogna prima creare una pagina HTML con un form:

    <HTML><BODY>
    <FORM method=POST action="prova3.cgi">
    Alfa=<input type=text name=alfa><br>
    Beta=<input type=text name=beta><br>
    <input type=submit name=bottone value="INVIA!">
    </FORM>
    </BODY></HTML>

Salvare questo testo col nome prova3.html nella stessa directory del CGI prova3.cgi, e quindi dal browser invocare l'URL http://localhost/~pippo/prova3.html

Riconoscere i parametri della query HTTP con Bash

Bash non fornisce alcuno strumento integrato per interpretare la query HTTP. Esistono librerie di funzioni già fatte per questo, ma noi ci svilupperemo gli strumenti necessari da soli! Vediamo come si può risolvere il problema perfezionando il CGI prova3.cgi di prima:

    #!/bin/bash

    function getkey ()
    {
            echo "$query" | tr '&' '\n' | grep "^$1=" | head -1 \
                    | sed "s/.*=//" | urldecode
    }

    if [ "$REQUEST_METHOD" = POST ]; then
            query=$( head --bytes="$CONTENT_LENGTH" )
    else
            query="$QUERY_STRING"
    fi
    echo "Content-Type: text/plain"
    echo
    echo "Query=$query"
    alfa=$( getkey alfa )
    beta=$( getkey beta )
    gamma=$( getkey gamma )
    echo "Alfa=$alfa"
    echo "Beta=$beta"
    echo "Gamma=$gamma"

Per prima cosa, il programma individua la query, indipendentemente dal fatto che essa sia stata ritornata col metodo GET o col metodo POST. La query così ottenuta viene salvata nella variabile globale $query. La funzione getkey deve essere chiamata passando per argomento il nome della variabile della query che ci interessa; la funzione analizza la query e ritorna il valore del parametro indicato; se il parametro non esiste ritorna la stringa nulla, come sarà il caso della gamma.

La funzione getkey sfrutta alcuni programmi filtro tradizionalmente disponibili sui sistemi UNIX, quali tr, grep e sed: rimando alle relative man pages per i dettagli.

L'ultimo filtro urldecode è invece un programmino scritto in C che riporto in appendice: questo programma converte la codifica esadecimale dei caratteri riservati nella loro rappresentazione ASCII, secondo quanto richiesto dalle specifiche RFC 1738. In pratica si tratta di questo: visto che alcuni caratteri sono riservati per la sintassi della query HTTP, come ad esempio ? = &, questi caratteri non possono comparire direttamente come valori delle variabili della query. Per superare questo problema, i caratteri "vietati" vengono invece rappresentati dal loro valore esadecimale preceduto dal carattere %. Il programma-filtro urldecode, e la sua controparte urlencode, assolvono proprio a questa funzione. Per il momento urlencode non ci serve, ma verrà anche il suo momento.

Gestire un DB

Tanto per fare un esempio concreto che coinvolge tutti i concetti di prima, vediamo come estendere ulteriormente il nostro programma prova3.cgi: adesso vogliamo salvare i dati impostati in un DB e, per rendere le cose ancora più interessanti, invieremo anche gli stessi dati ad un certo indirizzo email. Il codice da aggiungere è il seguente:

    (qui il codice di prova3.cgi come prima)
    db=/home/pippo/db
    email="pippo@localhost"
    dati="$alfa $beta"
	lockfile $db.lock
    echo "$dati" >> $db
	rm $db.lock
    echo "$dati" | mail -s "Dati dal form prova3.html" "$email"

Il DB viene implementato come file di testo, dove ogni riga è un record. I nuovi dati vengono accodati a quelli esistenti; viene anche creato un lock file per evitare che accessi concorrenti portino alla corruzione del DB. In questo articolo non ci interessa di studiare un data base vero e proprio; in certe applicazioni elementari sarebbe addirittura esagerato coinvolgere un DBMS, ed inoltre il nostro CGI diventa autonomo e non dipende dall'installazione di un determinato DBMS.

Per semplificare ulteriormente le cose, non faremo alcuna validazione dei dati introdotti, non controlleremo eventuali doppioni di dati inseriti, insomma non faremo nulla di tutto ciò che un vero programma CGI dovrebbe fare. Queste operazioni di controllo e validazione sono essenziali nelle applicazioni reali, e di solito una buona parte dei programmi CGI è costituita proprio da questo tipo di codice.

E' facile implementare una interfaccia di consultazione al DB così creato. Per cominciare, il form di interrogazione:

    <HTML><BODY>
    <FORM method=POST action=prova4.cgi>
    Chiave:<input type=text name=chiave>
    <input type=submit name=bottone value="CERCA!">
    </FORM>
    </BODY></HTML>

che dovrebbe mostrare il form seguente:

Chiave:

Salvare questa pagina HTML col nome prova4.html. Il CGI per l'interrogazione del DB è il seguente:

    #!/bin/bash

    function getkey ()
    {
            echo "$query" | tr '&' '\n' | grep "^$1=" | head -1 \
                    | sed "s/.*=//" | urldecode
    }

    if [ "$REQUEST_METHOD" = POST ]; then
            query=$( head --bytes="$CONTENT_LENGTH" )
    else
            query="$QUERY_STRING"
    fi
    echo "Content-Type: text/plain"
    echo
    chiave=$( getkey chiave )
    if [ -z "$chiave" ]; then
            echo "ERRORE: non hai inserito la chiave di ricerca!"
            exit
    fi
    echo "Esito della ricerca della chiave $chiave:"
    grep -e "$chiave" /home/pippo/db

Salvare questo script col nome prova4.cgi, impostare il flag che lo rende eseguibile, quindi dal browser puntare all'URL

    http://localhost/~pippo/prova4.html

Ogni volta che dal browser si invoca questo URL viene presentato il form di ricerca. Il navigatore lo compila inserendo una parola nel campo di input, e quindi preme il bottone ``CERCA!''; a questo punto il browser costruisce un documento MIME dove inserisce tutti i valori dei campi del form, cioè nel nostro caso il parametro chiave, e quindi invia il tutto al server; il server a sua volta individua il programma CGI richiesto nel form e gli passa tutti i dati; l'output generato dal CGI viene riconsegnato dal server al client per la visualizzazione del risultato. Buona ricerca!

CGI che ritornano immagini

In generale un CGI può ritornare al client qualunque tipo di dato per il quale sia previsto un corrispondente tipo MIME. Tra i tipi MIME più frequenti ci sono le immagini. Supponiamo allora di avere una immagine GIF in un file di nome figura.gif. Il tipo MIME da usare in questo caso è image/gif. Ecco un CGI che ritorna questa figura:

    #!/bin/bash
    echo "Content-Type: image/gif"
    echo
    cat figura.gif

Scriviamo questo programma nel file prova5.cgi, rendiamolo eseguibile nel solito modo, ed invochiamolo dal browser con l'URL http://localhost/~pippo/prova5.cgi: dovrebbe apparire la nostra figura.

Apparentemente, creare un programma CGI che ritorna una figura sembra del tutto inutile. Infatti basterebbe puntare l'URL del browser direttamente al file della figura stessa! Invece, ecco possibili applicazioni di un programma CGI che ritorna una figura:

Il programma CGI può essere anche invocato nel tag <IMG> completo di una query, come in questo esempio:

        <IMG SRC="prova5.cgi?alfa=123&beta=456">

Con una query come questa, il CGI ha la possibilità di ritornare immagini diverse in base ai valori passati; è ovvio che diventa necessario dotare il CGI della capacità di generare e manipolare file di immagine in modo più o meno sofisticato. Per inciso, a questo scopo esistono programmi facilmente interfacciabili anche con gli script. Un esempio di questo tipo di programmi è fly, col quale è facile costruire dinamicamente bottoni, grafici ed istogrammi (www.unimelb.edu.au/fly/fly.html).

Naturalmente non si è vincolati al formato GIF: se ne possono usare tanti altri, purché siano riconosciuti dai browser. Ecco alcuni formati MIME per le immagini comunemente riconosciuti dai browser:

        image/gif
        image/png
        image/jpeg

Gli stessi principi visti per le immagini valgono ovviamente anche per gli altri tipi di dato (filmati, suoni, ecc.).

Sicurezza e diritti di accesso

Abbiamo visto come i programmi CGI siano eseguiti con l'identità del loro autore. Al riguardo ci sono due aspetti da notare, uno positivo e uno negativo. Cominciamo dalle buone notizie.

Le buone notizie. Nell'esempio del data base che abbiamo costruito, il DB viene salvato nel file /home/pippo/db senza bisogno di particolari settaggi dei permessi. Questo file può essere reso leggibile al solo proprietario e illeggibile agli altri compreso il WEB server (chmod u=rx,o=); i dati in esso contenuti sono resi disponibili dal sig. Pippo esclusivamente attraverso l'interfaccia CGI che egli ha creato.

Le cattive notizie. Un programma CGI avviato col meccanismo del SUEXEC può fare nè più nè meno tutto quello che può fare l'utente stesso che lo ha scritto. Facciamo un esempio pratico: si vuole gestire un grosso archivio di file che contengono ciascuno un articolo del codice civile, e dando la possibilità al cliente di specificare il numero dell'articolo al quale è interessato. Il programma CGI recupera l'articolo e lo ritorna al client. Ecco una possibile implementazione del form:

    <FORM method=post action="articolo.cgi">
    Art. n. <INPUT type=text name=n>
    <INPUT type=submit name=x value="OK">
    </FORM>

Il cliente inserisce il numero dell'articolo che vuole leggere, per esempio "1234", schiaccia il bottone OK, e quindi il programma CGI di nome articolo.cgi ritorna il file di nome 1234. Ecco il CGI articolo.cgi:

    #!/bin/bash
    (qui ometto il solito codice per acquisire la query)
    n=$( getkey n )
    echo "Content-Type: text/plain"
    echo
    cat $n

Problema: cosa succede se un navigande malizioso invece di inserire un numero innocuo inserisce la stringa /etc/passwd ? Risposta: fedelmente, il comando cat ritornerà al client l'elenco degli utenti registrati nel sistema, il chè non è proprio una bella cosa...

Questo è uno di quei casi in cui un programma CGI mal scritto può incidere sulla sicurezza del sistema, o per lo meno mettere a repentaglio la riservatezza dei dati. Sebbene i diritti di accesso di un utente non consentano di violare la riservatezza degli account degli altri utenti, nè consentano di vedere o alterare informazioni vitali del sistema, questa svista può costituire comunque un ottimo appiglio per un cracker smaliziato.

In tutti casi, l'input da un form dovrebbe essere sempre vagliato dal programma CGI. Nel nostro caso basterebbe assicurare che la variabile $n sia costituita da una o più cifre:

    if echo "$n" | grep -q '^[0-9]\+$'; then
            cat $n
    else
            echo "ATTENZIONE! non hai inserito un numero valido!"
    fi

Qui ci siamo avvalsi di una espressione regolare per rappresentare sinteticamente la sintassi ammessa per $n. Tutti i linguaggi di scripting hanno un supporto per le espressioni regolari, che si dimostra versatile e potente anche in casi più articolati, ad es. per la validazione di un indirizzo email, di una data, di un codice fiscale, ecc.

Una descrizione dettagliata e sintetica delle espressioni regolari si trova nella relativa pagina del manuale in linea:

        $ man 7 regex

In generale i nostri programmi CGI dovranno sottoporre i dati in input a un controllo stringente di conformità, in modo da assicurare il corretto funzionamento del programma indipendentemente dagli input più o meno maliziosi coi quali potrebbe essere chiamato. Oltre alla questione sicurezza, naturalmente, ci sono anche ragioni di consistenza degli output generati e di preservazione dei dati quando è coinvolto anche un data base.

CGI polimorfi

Ebbene sì, lo ammetto: io sono un fan dei CGI monolitici e autosufficienti, cioè, in breve, dei CGI polimorfi. Con questo termine intendo un programma CGI capace di generare una varietà di output diversi in base alle informazioni di contesto, costituite dalla query e dai cookie (che vedremo nel prossimo paragrafo). Mi sembra interessante vedere questa tecnica di programmazione perché dovrebbe chiarire bene quanto sia versatile il meccanismo MIME adottato dal protocollo HTTP. Infatti molti credono che solo perché l'URL di una pagina WEB finisce con .html debba necessariamente essere una pagina HTML, oppure credono che un URL che finisce con .jpeg debba essere necessariamente una immagine. Niente di più sbagliato.

Ecco il sorgente di polimorfo.cgi: se invocato senza query, questo programma presenta una pagina HTML con alcuni link; ogni link richiama lo stesso CGI per generare altre pagine WEB. Lascio al lettore di scoprire nei dettagli come funziona.


#!/bin/bash
# polimorfo.cgi

function getkey ()
{
        echo "$query" | tr '&' '\n' | grep "^$1=" | head -1 \
                | sed "s/.*=//" | urldecode
}

function genera_errore()
{
    echo "Content-Type: text/html"
    echo
    echo "<html><body><h1>ERRORE!</h1>$@</body></html>"
}

function genera_menu()
{
    echo "Content-Type: text/html"
    echo
    echo "<html><body><h1>MENU</h1>"
    echo "<a href=\"$SELF?op=sommatore\">Sommatore</a><p>"
    echo "<a href=\"$SELF?op=figura\">Vedi figura</a><p>"
    echo "<a href=\"$SELF?op=dir\">Elenco dei file</a></p>"
    echo "</body></html>"
}

function genera_figura()
{
    echo "Content-Type: image/png"
    echo
    {
        echo "begin-base64 644 x.png"
        echo "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQAAAAA3iMLMAAAANUlEQVR42mMQ"
        echo "62YoW81QNptBYDXD//8MR9wZrl5nuBLOcD0cxAaK7JZimF3FsNqKYXM9kA0A"
        echo "vSoSfMoIiCAAAAAASUVORK5CYII="
    } | uudecode -o /dev/stdout

}

function genera_sommatore()
{
    echo "Content-Type: text/html"
    echo
    echo "<html><body><h1>SOMMATORE</h1>"
    echo "<form method=post action=$SELF>"
    echo "<input type=hidden name=op value=somma>"
    echo "Primo addendo: <input type=text name=a><p>"
    echo "Secondo addendo: <input type=text name=b><p>"
    echo "<input type=submit name=but value=SOMMA>"
    echo "</form>"
    echo "</body></html>"
}

function genera_somma()
{
    a=$( getkey a )
    b=$( getkey b )
    if ! echo "$a" | grep -q "^[0-9]\+$"; then
        genera_errore "Il primo termine non &egrave; un numero!"
        return;
    fi
    if ! echo "$b" | grep -q "^[0-9]\+$"; then
        genera_errore "Il secondo termine non &egrave; un numero!"
        return;
    fi
    echo "Content-Type: text/html"
    echo
    echo "<html><body><h1>RISULTATO</h1>"
    echo "$a + $b = $(( a+b ))"
    echo "</body></html>"
}

function genera_dir()
{
    echo "Content-Type: text/plain"
    echo
    ls -l
}

#
# main
#

SELF="$SCRIPT_NAME"

if [ "$REQUEST_METHOD" = POST ]; then
        query=$( head --bytes="$CONTENT_LENGTH" )
else
        query="$QUERY_STRING"
fi

op=$( getkey op )

case "$op" in
    figura)     genera_figura    ;;
    sommatore)  genera_sommatore ;;
    somma)      genera_somma     ;;
    dir)        genera_dir       ;;
    *)          genera_menu      ;;
esac

#### FINE!

Qualche breve spiegazione del suo funzionamento.
- Il parametro di query op (OPeration) codifica il comportamento che vogliamo far assumere al CGI. Al variare di questo parametro sui valori figura sommatore somma dir il programma chiama una funzione specifica che svolge una certa elaborazione e ritorna un determinato tipo di documento.
- Tanto per gradire, la funzione genera_figura ritorna una simpatica immaginetta GIF; l'immagine è codificata nel sorgente del programma nel formato Base64, e il comando uudecode provvede a rigenerarla nel formato binario.
Non mi dilungo in ulteriori spiegazioni perché mi sembra che il codice sia piuttosto chiaro. E' facile estendere alle estreme conseguenze questa soluzione e costruire CGI che realizzano un intero programma autosufficiente che comprende tante pagine WEB, la gestione delle maschere di input, la validazione dei dati, la generazione dei messaggi diagnostici, ecc.

Sessioni con i cookie

Un programma CGI non è in grado di stabilire se due richieste provengono dallo stesso client. L'indirizzo IP del client contenuto nella variabile d'ambiente REMOTE_ADDR non è utile a questo scopo, perchè macchine client diverse possono essere mascherate da un proxy server o da un NAT ed apparire tutte con lo stesso indirizzo; inoltre certe macchine, come i sistemi Unix, sono multiutente. Quello che manca al protocollo HTTP è un concetto di sessione che permetta di collegare tra di loro richieste distinte. Per superare questo problema senza pesanti rimaneggiamenti del protocollo, Netscape ha proposto il meccanismo dei cookie: vediamo di cosa si tratta.

L'intestazione MIME della risposta del WEB server può contenere il settaggio di un cookie nel browser attraverso il campo Set-Cookie. Ecco un esempio:

    Content-Type: text/plain
    Set-Cookie: tuocodice=1234567; expires=Mon 04-Feb-02 14:30:18 GMT

    Salve,
    il tuo accesso e' stato validato.
    In questo momento il tuo browser ha ricevuto il cookie di sessione.
    Il tuo codice di sessione e' 1234567.
    Buona navigazione!

Ho evidenziato la riga dove il server imposta il cookie. Il browser dovrà memorizzare la variabile tuocodice e assegnarle il valore 1234567; inoltre, il browser dovrà mantenere questo valore in memoria fino alla data indicata, dopo di che dovrà scartarlo. Nel frattempo, il browser dovrà ritornare al server il cookie ogni volta che invocherà lo stesso URL. Ad ogni invocazione, il CGI si ritroverà l'elenco dei cookie nella variabile d'ambiente HTTP_COOKIE sotto forma di una lista di assegnamenti del tipo nome=valore&nome=valore&... del tutto simile a una query HTTP.

Passiamo subito all'esempio: creeremo uno script CGI in Bash che imposta e mostra un cookie. Chiameremo questo programma prova6.cgi:

    #!/bin/bash


    function setcookie()
    #
    # $1 = nome variabile
    # $2 = valore variabile
    # $3 = durata (secondi)
    # $4 = path (opzionale)
    #
    {
            value=$( echo -n "$2" | urlencode )
            if [ -z "$4" ]; then
                    path=""
            else
                    path="; Path=$4"
            fi
            echo -n "Set-Cookie: $1=$value$path; expires="
            date -u --date="$3 seconds" "+%a, %d-%b-%y %H:%M:%S GMT"
    }

    # Intestazione MIME:
    echo "Content-Type: text/plain"
    setcookie tuocodice 1234567 60
    setcookie tuonome   "Mario" 60
    setcookie tuocogn   "Rossi" 60
    echo

    # Corpo del messaggio:
    echo "Cookie ritornato dal browser: $HTTP_COOKIE"

Gran parte del codice è occupato dalla funzione setcookie() perché ha un comportamento un po' articolato. La parte più difficile è la composizione corretta della data. I parametri della funzione setcookie() sono:

Un ultimo fatto da notare riguardo alla funzione setcookie() è piuttosto ovvio: questa funzione deve essere richiamata mentre si compone l'intestazione MIME della risposta: una direttiva Set-Cookie che apparisse nel corpo della risposta verrebbe trattata dal browser come testo qualsiasi e quindi ignorato ai fini dei cookie.

Il codice del nostro CGI d'esempio prosegue generando una risposta in formato MIME come al solito, salvo che nell'intestazione richiamiamo la funzione setcookie() tre volte per settare altrettante variabili con scadenza di 60 secondi (tempo sufficientemente breve per vedere cosa succede senza invecchiare troppo).

Finalmente passiamo alla fase della sperimentazione sul campo: sul nostro browser componiamo l'URL http://localhost/~pippo/prova6.cgi: alla prima invocazione il cookie non appare nella pagina scaricata, anche se è già nella pancia del browser. Facciamo un reload della pagina e, miracolosamente, ci appare il cookie con tutte le variabili settate. Aspettiamo più di 60 secondi, quindi reload di nuovo: il cookie è sparito! reload: il cookie è di nuovo qui!

Domande sui cookie:

Domanda 1. Spiegare il motivo di questi cookie che appaiono e scompaiono, e in particolare spiegare perché il cookie non viene presentato alla prima invocazione del CGI, o comunque dopo che è trascorso più di un minuto.

Domanda 2. Il cookie ritornato al CGI nella variabile HTTP_COOKIE può contenere diversi assegnamenti e diverse variabili: costruire una funzione che permetta di estrarre questi valori. Come modello si può seguire la funzione getkey().

Domanda 3. Cosa succede ai cookie se l'orologio del client è avanti? e se è indietro? L'RFC 2109 e il suo aggiornamento RFC 2965 propongono l'uso della direttiva Max-Age che dà la durata del cookie, al posto della direttiva expires che imposta la data assoluta come proposto da Netscape.

Arrivati a questo punto abbiamo tutti gli strumenti per realizzare quella soluzione nota come gestione della sessione. Si tratta di una serie di accorgimenti di programmazione che non interessano molto ai fini di questo articolo, per cui mi limiterò solo ad alcune osservazioni.

A questo punto il programmatore può sbizzarrirsi scegliendo di volta in volta la soluzione più adatta. Non insisto oltre sull'argomento perché esula dagli obiettivi di questo articolo.

Variabili d'ambiente

Ecco le principali variabili d'ambiente messe a disposizione da Apache e che potremo usare nei nostri programmi CGI. L'elenco completo e le descrizioni esaustive si trovano nella documentazione di Apache.

CONTENT_LENGTH
Lunghezza in bytes dei dati disponibili sullo standard input.
HTTP_HOST
Nome dell'host come indicato nell'URL:
    http://localhost.localdomain/~pippo/text.cgi?a=AA&b=BB

Per un server configurato con domini virtuali, questo potrebbe essere l'indirizzo più o meno completo del dominio virtuale richiesto. E' il contenuto del campo Host della intestazione MIME ritornata dal client.

SCRIPT_NAME
Parte di URL che contiene il pathfile virtuale del CGI all'interno dell'host:
    http://localhost.localdomain/~pippo/text.cgi?a=AA&b=BB

Utile perché consente al CGI di scoprire il proprio indirizzo URL locale: il CGI può allora generare link a sè stesso, indipendentemente dalla sua collocazione o ri-collocazione nel file system del server. Da non confondere con SCRIPT_FILENAME.

SCRIPT_FILENAME
Pathfile completo dello script. Suggerimento: magari in combinazione coi comandi dirname e basename, permette al CGI di individuare la propria collocazione, la directory in cui risiede, e il proprio nome. Da non confondere con SCRIPT_NAME.
QUERY_STRING
Contiene la stringa di interrogazione utilizzata nel metodo GET, cioè la parte di URL che viene dopo il punto interrogativo:
    http://localhost.localdomain/~pippo/text.cgi?a=AA&b=BB
REMOTE_ADDR
L'indirizzo IP del client che ha eseguito la richiesta.
REQUEST_METHOD
Metodo di richiesta HTTP. Può valere GET, POST, PUT, HEAD, DELETE, TRACE, CONNECT. In questo articolo abbiamo descritto solo i metodi GET e POST.
SERVER_NAME
Il nome di dominio del server, completo di nome host (FQDN). E' il nome principale del server, generalmente diverso da quello che appare nell'URL.
SERVER_PORT
Numero di porta TCP alla quale il server ha risposto. Tipicamente si tratta della numero 80. Nulla vieta di avviare diversi siti o diverse istanze del server Apache su porte diverse da quella standard. Ad esempio, gli utenti non root non possono avviare programmi server che stiano in ascolto su numeri di porta inferiori a 1024, ma possono senzaltro avviare un server, come Apache, che sia in ascolto sulla porta, ad esempio, numero 8080.
HTTP_COOKIE
Elenco dei cookie ritornati dal client. E' il contenuto del campo Cookie della intestazione MIME ritornata dal client.
URL_REFERRER
L'URL del documento dal quale il client è pervenuto a questo CGI. Utilissimo in varie circostanze, la più tipica è la realizzazione di analizzatori degli accessi. E' il contenuto del campo Referer (con una sola r!) della intestazione MIME ritornata dal client. Non è detto che tutti i client ritornino questa informazione.
HTTP_ACCEPT_LANGUAGE
Elenco delle lingue, in ordine di preferenza, che l'utilizzatore del client è in grado di riconoscere. E' il contenuto del campo Accept-Language della intestazione MIME ritornata dal client.
HTTP_USER_AGENT
Identificativo completo del client che ha eseguito la richiesta. Esempio:
    Mozilla/4.79 [en] (X11; U; Linux 2.4.2-2 i586)

E' il contenuto del campo User-Agent della intestazione MIME ritornata dal client.

Conclusioni

Ci sarebbero molti aspetti ancora da considerare: l'interfacciamento a un DBMS come potrebbe essere MySQL o PostgreSQL; la manipolazione delle immagini; la rotazione dei banner; l'interfacciamento a un POP server per la posta elettronica; l'uso avanzato dei cookie; ecc. Il mio intento era quello di mostrare il funzionamento della tecnologia CGI attraverso una serie di semplici esempi. A questo scopo abbiamo usato un linguaggio che tutti gli utilizzatori di GNU/Linux dovrebbero conoscere, e cioè la shell Bash. Non si tratta certo di un linguaggio di programmazione sofisticato, ma si dimostra molto utile per risolvere piccoli problemi pratici e per fare dei test. Linguaggi più specializzati e più sofisticati, magari più ricchi di librerie pronte all'uso, rimangono strumenti indispensabili per i progetti più complessi.

Ho verificato tutti gli esempi presentati qui, e ho riportato il loro sorgente esatto. Tuttavia la materia è abbastanza articolata, e lo sviluppo dei CGI coinvolge aspetti di programmazione, formati, protocolli e problemi sistemistici, e districarsi dai problemi richiede spesso una certa dimestichezza con tutti questi aspetti. Non mi illudo di essere stato esaustivo, nè tantomeno infallibile. Ecco perché ho già predisposto una pagina WEB all'indirizzo http://digilander.iol.it/salsi/erratacorrige dove ospitare le inevitabili correzioni e le integrazioni che si dovessero rendere necessarie. Ovviamente, si tratta di una vile manovra scaramantica.

Appendice

Riporto qui i due programmini in linguaggio C che effettuano la conversione URL-encoded richiesta per la codifica dei parametri della query. Compilare i due programmi è molto semplice:

    # gcc urldecode.c -o urldecode
    # gcc urlencode.c -o urlencode

Questo è il programma urldecode.c che decodifica il suo standard input e genera in output la sequenza di byte originaria:

    /* urldecode - v. RFC 1738 per i dettagli */
    #include <stdio.h>
    #include <ctype.h>
    #include <stdlib.h>

    int main()
    {
    char c, c1, c2;
    while( (c = getchar()) != EOF ){
        if( c == '%' ){
            c1 = getchar();
            c2 = getchar();
            if( c1 == EOF || c2 == EOF )  exit(0);
            c1 = tolower(c1);
            c2 = tolower(c2);
            if( ! isxdigit(c1) || ! isxdigit(c2) )  exit(0);
            if( c1 <= '9' )
                c1 = c1 - '0';
            else
                c1 = c1 - 'a' + 10;
            if( c2 <= '9' )
                c2 = c2 - '0';
            else
                c2 = c2 - 'a' + 10;
            putchar( 16 * c1 + c2 );
        } else if( c == '+' )
            putchar(' ');
        else
            putchar(c);
    }
    exit(0);
    }

Questo è il programma urlencode.c che trasforma il suo standard input in una sequenza di caratteri ASCII validi per la query:

    /* urlencode - v. RFC 1738 per i dettagli */
    #include <stdio.h>
    #include <ctype.h>
    #include <stdlib.h>

    int main()
    {
    int c;
    char *h = "0123456789abcdef";

    while( (c = getchar()) != EOF ){
        if( 'a' <= c && c <= 'z'
        || 'A' <= c && c <= 'Z'
        || '0' <= c && c <= '9'
        || c == '-' || c == '_' || c == '.' )
            putchar(c);
        else if( c == ' ' )
            putchar('+');
        else {
            putchar('%');
            putchar(h[c >> 4]);
            putchar(h[c & 0x0f]);
        }
    }
    exit(0);
    }

Se si ha accesso root al sistema, suggerisco di mettere i due programmi nella dir. /bin il cui PATH è sicuramente sempre disponibile. Altrimenti si dovrà specificare il pathfile completo negli esempi riportati qui, cosa un po' scomoda.

Bibliografia

Apache 1.3 User's Guide - La documentazione ufficiale di Apache; www.apache.org/doc.
Common Gateway Interface (CGI) - http://hoohoo.ncsa.uiuc.edu/cgi/interface.html.
RFC 2616 - Specifiche del protocollo HTTP versione 1.1.
Netscape cookies - La gestione dei cookies secondo Netscape www.netscape.com/newsref/std/cookie_spec.html.
RFC 2109 - La gestione dei cookie secondo gli RFC (prima proposta).
RFC 2965 - La gestione dei cookie secondo gli RFC (nuova versione).

NOTA: i documenti RFC sono reperibili in www.rfc-editor.org.



L'autore

Umberto Salsi <umberto-salsi@libero.it> ha scritto il suo primo programma nel 1981: un potente ciclo FOR stampava su schermo i numeri da 1 a 10000. Folgorato da questo successo, da allora non ha più smesso di seviziare computer nel software e nell'hardware, e di queste pratiche ne ha fatto il suo lavoro e il suo hobby. Nel 1992 scopre Internet e il mondo delle reti telematiche. Nel 1996 incontra GNU/Linux, ed è un'altra infatuazione.


<- SL: Intro - Copertina - SL: Postfix ->