<- SL: Intro - Copertina - SL: Postfix -> |
Sistemi Liberi
L'articoloSi 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. |
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.
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.
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:
UserDir public_html
per dar modo agli utenti di redigere le loro home page..cgi
che informa Apache che questo è un programma. Questo meccanismo lo
abbiamo impostato usando la direttiva ScriptAliasMatch
nel precedente articolo.pippo
dovrebbe apparire con
questi permessi di accesso:
$ ls /home/pippo -d drwx-----x 4 pippo users 4096 Aug 17 22:20 /home/pippo |
users
non ha accesso alla
directory di pippo
, mentre Apache, che gira come
nobody:nobody
(o altra identità dipendente dalla
vostra configurazione), ha il permesso di attraversamento che gli permette
di accedere ai file del WEB di Pippo.
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.
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:
bash: xxx: No such file or directory
dove xxx
è il nome dello script).
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
, ...
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.
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:
pippo
e il gruppo sarà users
.
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.
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.
Il metodo POST differisce dal metodo GET come segue:
REQUEST_METHOD=POST
, e mette a disposizione del CGI la query
ricevuta attraverso il file stdin del CGIVediamo 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.
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
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.
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:
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!
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.).
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.
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 è un numero!" return; fi if ! echo "$b" | grep -q "^[0-9]\+$"; then genera_errore "Il secondo termine non è 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.
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:
urlencode
;
naturalmente i valori che poi saranno ritornati dal browser si dovranno
riconvertire con urldecode
.
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.
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
HTTP_HOST
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
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
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
http://localhost.localdomain/~pippo/text.cgi?a=AA&b=BB
REMOTE_ADDR
REQUEST_METHOD
SERVER_NAME
SERVER_PORT
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
Cookie
della intestazione MIME ritornata dal client.
URL_REFERRER
Referer
(con una sola r!) della intestazione MIME
ritornata dal client. Non è detto che tutti i client ritornino
questa informazione.
HTTP_ACCEPT_LANGUAGE
Accept-Language
della intestazione MIME ritornata dal client.
HTTP_USER_AGENT
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.
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.
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.
www.apache.org/doc
.http://hoohoo.ncsa.uiuc.edu/cgi/interface.html
.www.netscape.com/newsref/std/cookie_spec.html
.
NOTA: i documenti RFC sono reperibili in www.rfc-editor.org
.
L'autoreUmberto 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 -> |