introduzione alla programmazione cgi - Digilander

INTRODUZIONE ALLA PROGRAMMAZIONE CGI
Il tutto nasce all'inizio degli anni '90 grazie ad un gruppo di ingegneri e fisici del CERN di
Ginevra, che anziché progettare bombe atomiche o all'antimateria (e meno male!), crearono le
fondamenta per il primo sistema informativo planetario (...universale per chi non crede ai
marziani). Alla sua base stanno un protocollo di comunicazione come l'HTTP, un linguaggio
semplice ed efficace come l'HTML.
COS'È LA CGI
La CGI (Common Gateway Interface) è un'interfaccia che definisce il modo in cui un server di
informazioni (server web) e alcuni programmi (programmi gateway) comunicano. Lo scambio
di informazioni avviene mediante variabili d'ambiente speciali, argomenti della riga di comando
o, nel caso più frequente, mediante i parametri di un modulo da compilare (i form html, per
intenderci).
I METODI CGI
Lo scambio di informazioni tra client, programma cgi e server, avviene secondo la seguente
sequenza fondamentale:
•
•
•
•
il server web invia al client (browser dell'utente) il documento html richiesto, per
esempio un modulo da compilare.
quando si clicca sul pulsante di submit del modulo il browser (netscape o internet
explorer) invia i dati del modulo al server usando il metodo GET o POST definito nel tag
FORM mediante il parametro METHOD, così: <FORM METHOD="POST"
ACTION="http://etc.">
il server web verifica che si sta puntando ad un programma cgi (il parametro ACTION
del tag FORM serve proprio a questo) ed invia a quest'ultimo i dati del modulo.
il programma gateway elabora i dati del modulo e restituisce un nuovo documento html
al server che lo invia al client dell'utente.
Per quanto riguarda invece il programma cgi, questo può ricevere i dati da elaborare in uno dei
tre modi seguenti:
•
•
•
come argomenti sulla riga di comando, con le richieste ISINDEX o ISMAP
nella variabile d'ambiente QUERY_STRING, per tutte le richieste che usano il metodo
GET ed anche ISINDEX e ISMAP
sullo standard input, solo con il metodo POST. La lunghezza dei dati passati è definita
nella variabile d'ambiente CONTENT_LENGTH. In questo caso il programma gateway
dovrà avere una funzione di parsing per decodificare i dati che arrivano formattati in
base alle regole del protocollo HTTP.
IL SERVER
Come fa il server a sapere che i dati inviati dal browser dovranno essere processati da un
programma cgi? Bisogna smanettare nei file di configurazione del server. Se usiamo il server
Apache nel file access.conf ci sarà una direttiva simile a questa
<Directory /usr/local/etc/httpd/cgi-bin>
mentre nel file srm.conf ci sarà qualcosa come:
ScriptAlias /cgi-bin /usr/local/etc/httpd/cgi-bin
Tali direttive definiscono le directory in cui sono contenuti i programmi gateway. Per cui,
supponendo di lavorare a livello locale (client e server sullo stesso pc), il modulo che inizia con
<FORM METHOD="POST" ACTION="http://localhost/cgi-bin/prova.cgi"> invierà i dati al
programma prova.cgi nella directory /cgi-bin del server.
1
I PROGRAMMI GATEWAY
Per scrivere programmi compatibili con le specifiche cgi si può usare qualsiasi linguaggio di
programmazione, dal C al Visual Basic. Noi useremo il Perl, sia per mantenere una promessa
fatta e sia perchè questo linguaggio sembra nato proprio per scrivere script di gateway.
Teoricamente non vi sono limiti all'uso di questi programmi. In generale vengono usati per
consentire:
•
•
•
la ricerca di informazioni su database
la generazione dinamica di statistiche e grafici
la gestione di input tramite moduli da compilare.
SICUREZZA
Finché si lavora a livello locale la sicurezza del nostro server è in effetti un problema relativo.
Nel momento in cui però si inizia a programmare in un ambiente multiutente, lo sviluppo di
programmi cgi e la configurazione del server web debbono rispettare alcune norme di sicurezza
per la salvaguardia dell'integrità del sistema e dei dati in esso contenuti. Scrivere un
programma di gateway e renderlo eseguibile in una macchina collegata ad Internet equivale a
permettere l'esecuzione di quel programma al mondo intero. Esiste quindi la possibilità,
nemmeno tanto remota, che un client attacchi il server proprio attraverso il cgi. Ci sono alcune
semplici regole da seguire per programmare in modo sicuro. Innanzi tutto il server httpd non
deve mai essere eseguito con i diritti del SuperUser (root). Inoltre sarebbe molto utile che tutti
i dati e i programmi cgi degli utenti fossero gestiti in una partizione diversa dalla principale in
modo da "montare" un nuovo file system dedicato proprio a tale scopo. Poi bisogna fare
attenzione all'uso di chiamate di sistema nei programmi cgi, magari quando si costruiscono
comandi con le funzioni system(), exec() e popen() usando i dati provenienti dal client. Infine
è necessaria un'attenta configurazione del server prestando particolare attenzione alle direttive
di Server-Side-Includes.
PER INIZIARE
Non esiste niente di meglio e più economico che una macchina Linux per programmare
Internet a livello nativo. Nel CD di ioProgrammo di Maggio c'è tutto il necessario. Una
fantastica distribuzione di Linux, come la RedHat 4.1, e tutto il software per installare un
server web completo. Nei nostri esempi si lavorerà sempre in locale, per cui tutte le chiamate
al server avranno questa forma generale:
http://localhost/cgi-bin/scriptcgi
TESTIAMO L'AMBIENTE CGI
Questo semplice script Perl lista sulla pagina del browser tutte le variabili d'ambiente
supportate dallo standard CGI.
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<HTML><HEAD>\n";
print "<TITLE>Ambiente CGI</TITLE>\n";
print "</HEAD>\n";
Salviamo questo cgi nella directory /cgi-bin del server con il nome test.cgi e lo richiamiamo da
netscape con http://localhost/cgi-bin/test.cgi. Il risultato in figura 1.
SU INTERNET
Per sapere tutto su HTTP e CGI
http://www.w3.org
http://hoohoo.ncsa.uiuc.edu/cgi/
2
Per scaricare e configurare l'ultima versione del server web Apache
http://www.apache.org
METODI RICHIESTE E CGI-LIB.PL
Entriamo nel vivo della programmazione per Internet analizzando in dettaglio i metodi e le
richieste CGI. Concluderemo con un esempio completo di scripting in cui verrà illustrato l'uso di
cgi-lib.pl, una delle più famose librerie per gli script di gateway
Quando il server web esegue un programma cgi il passaggio dei dati da elaborare può avvenire
in uno dei tre modi seguenti: come argomenti passati sulla riga di comando, come stringa
codificata nella variabile d'ambiente QUERY_STRING, o sullo standard input. Nel primo caso si
usa un'interrogazione ISINDEX ovvero una chiamata diretta dal browser al gateway, separando
gli argomenti (parole-chiave) con il segno +, così
http://localhost/cgi-bin/isindex.cgi?uno+due+tre
Lo script sarà molto semplice. Si dovrà soltanto estrarre gli argomenti dall'array @ARGV.
#!/usr/bin/perl
print "Content-type: text/html\n\n";
if (@ARGV > 0) {
print "<ol>\n";
foreach $chiave (@ARGV) {
print "<li>$chiave\n";
}
print "</ol>\n";
}
print "<isindex>\n";
Gli ultimi due casi si adattano rispettivamente ai metodi GET e POST.
IL METODO GET
Viene così chiamato perché il browser usa il comando GET del protocollo HTTP per inviare i dati
di interrogazione al server. Questi dati vengono prima codificati, poi passati all'interno di un
URL ed infine memorizzati nella variabile d'ambiente QUERY_STRING. La codifica è necessaria
perché l'utente può introdurre nel form dati arbitrari e non tutti i caratteri ascii sono leciti in un
URL. Per esempio non si possono usare i caratteri < e >, lo spazio viene sostituito con un +, e
i caratteri speciali come / + : etc vengono codificati con un simbolo di % seguito da due cifre
esadecimali. Inoltre i dati immessi dall'utente nei campi del modulo vengono separati dal
carattere &.
http://localhost/cgi-bin/modulo.cgi?nome=pincoη=20&indirizzo=via+roma+1
Ecco perché serve una libreria di parsing che in sostanza decodifica il contenuto di
QUERY_STRING.
IL METODO POST
E' il metodo più usato per inviare dati tramite form. Rispetto al GET consente di trasferire le
richieste direttamente sullo standard input e l'unica variabile cgi impostata è
CONTENT_LENGTH che contiene il numero di dati passati.
L'OUTPUT CGI
Un programma cgi può restituire al client svariati tipi di documenti. Immagini, pagine html,
pagine di puro testo, addirittura filmati video e brani musicali. Può inoltre restituire documenti
presenti su altri server web sparsi qua e là su Internet. Ovviamente il browser deve sapere
quale è il tipo di oggetto restituito, e ad informarlo ci pensa proprio il gateway tramite una
richiesta diretta al server. Questa richiesta è in sostanza una istruzione print che stampa sullo
3
standard output il tipo MIME del documento da restituire seguito da una linea vuota (un \n in
più). In pratica la prima istruzione che apre l'invio dei dati verso il browser è
print "Content-type: tipo/sottotipoMIME\n\n";
Nel caso quindi che il mio cgi restituisca una pagina html, l'istruzione sarà
print "Content-type: text/html\n\n";
seguita da una serie di print di tag html. Se invece volessi restituire solo testo, dovrei scrivere
print "Content-type: text/plain\n\n";
seguito da alcuni print di testo semplice. Infine se avessi bisogno di inviare al browser un
documento non presente sul server in cui gira il mio programma cgi, dovrei scrivere
print "Location: http://indirizzo/pagina.html\n\n";
Quest'ultima istruzione può essere usata anche per riferirsi a documenti locali usando come
indirizzo la stringa localhost o semplicemente il path di una directory locale, purché sia vista
dal server web. Si noti sempre il doppio \n alla fine della prima istruzione print: non ce ne
dimentichiamo perché è fondamentale.
CGI-LIB.PL
Scritta da Steven E. Brenner e giunta alla versione 2.14, cgi-lib.pl è la libreria per il parsing cgi
più nota. Gestisce in modo del tutto trasparente e con un'unica funzione di nome ReadParse le
richieste ISINDEX, ISMAP ed di metodi GET e POST. ReadParse per default restituisce l'array
associativo %in che contiene le parole-chiave o le coppie indice-valore dei campi del form
html. L'indice è il valore del parametro NAME del form, mentre il valore è il contenuto del
campo INPUT, SELECT o TEXTAREA. In quest'ultimo caso le linee di testo scritte nel campo
vengono separate con il carattere \0. Sarà poi nostro compito sostituire il \0 con dei tag <BR>
o con dei \n. Se a ReadParse viene passata una variabile "glob" (*nomevariabile) i dati
saranno memorizzati nell'array associativo identificato dalla variabile stessa e non in %in. Nel
nostro esempio la funzione restituirà l'array %CGI poiché useremo la chiamata
&ReadParse(*CGI), ma prima si dovrà includere il file cgi-lib.pl nello script con la funzione
require del Perl.
LA RICHIESTA ISINDEX
Poiché la variabile QUERY_STRING è impostata anche per le richieste ISINDEX, è possibile
usare cgi-lib per processare gli argomenti passati sulla riga di comando, senza usare l'array
@ARGV: comodo no? Ecco lo script islib.cgi
#!/usr/bin/perl
require "cgi-lib.pl";
&ReadParse;
print "Content-type: text/html\n\n";
if (%in) {
print "<ol>\n";
foreach $chiave (split(' ',(keys(%in)) [0])) {
print "<li>$chiave\n";
}
print "</ol>\n";
}
che verrà caricato da Netscape con http://localhost/cgi-bin/islib.cgi?uno+due+tre
4
LA RICHIESTA ISMAP
Viene usata quando si vuole utilizzare una immagine cliccabile. Le coordinate del punto su cui
si clicca vengono passate al programma cgi separate da un punto e virgola (x;y). Per
convenzione il vertice in alto a sinistra dell'immagine ha coordinate 0;0. Per usare questo tipo
di richiesta si usa un tag HREF con all'interno un tag IMG
<A HREF="http://localhost/imgmap.pl">
<IMG SRC="immagine.gif" ISMAP>
</A>
e poi si sfrutta cgi-lib.pl per recuperare i valori di x e y
#!/usr/bin/perl
require "cgi-lib.pl";
&ReadParse;
print "Content-type: text/plain\n\n";
($x,$y)=split(',',(keys(%in)) [0]);
print "$x, $y\n";
Per motivi di spazio l'esempio è volutamente limitato. Le coordinate x e y si dovrebbero usare
con delle istruzioni if in modo da ottenere una certa risposta in base al punto dell'immagine su
cui si clicca.
LA RICHIESTA FORM
E' la richiesta più usata per consentire l'invio di dati tramite moduli da compilare. Si inizia col
predisporre un form html il cui parametro ACTION punta esattamente al cgi che dovrà
elaborare i dati inviati, così
<HTML>
<HEAD>
<TITLE>Modulo</TITLE>
</HEAD>
<BODY>
<FORM METHOD="POST" ACTION="http://localhost/cgi-bin/modulo.cgi">
<FONT SIZE=10><B>Io</B><FONT SIZE=5> e <FONT
SIZE=10><B>Linux</B></FONT>
<HR><P>
Nome: <INPUT TYPE="text" NAME="nome" VALUE="" SIZE=20 MAXLENGTH=30>
Eta': <INPUT TYPE="text" NAME="eta" VALUE="" SIZE=2 MAXLENGTH=2><P>
Utilizzo Linux per:
<SELECT NAME="uso">
<OPTION SELECTED>Hobby
<OPTION>Studio
<OPTION>Lavoro
<OPTION>Altro
</SELECT><P>
Cosa penso di Linux?<BR>
<TEXTAREA NAME="penso" VALUE="" COLS=40 ROWS=2></TEXTAREA><P>
Stampo i dati?
<INPUT TYPE="radio" NAME="stampo" VALUE="si" CHECKED><I>Si</I>
<INPUT TYPE="radio" NAME="stampo" VALUE="no"><I>No</I><P>
<INPUT TYPE="submit" NAME="ok" VALUE="OK">
<INPUT TYPE="reset" NAME="cancel" VALUE="Annulla">
</FORM>
</BODY>
</HTML>
5
Editiamo le righe precedenti nel file modulo.html e lo salviamo nella DocumentRoot (la
directory che contiene tutte le pagine html pubbliche) del server. Ora lo possiamo aprire con
Netscape. Successivamente si passa alla scrittura del codice Perl. Lo script è composto da tre
subroutine principali: &Controlli, che esegue un controllo sui principali errori di compilazione
del modulo;
&StampaModulo, usata per restituire il modulo compilato al client
in modo che l'utente possa stamparlo; &InviaModulo, che spedisce via e-mail i dati a colui che
ha predisposto il form.
#!/usr/bin/perl
require "cgi-lib.pl";
&ReadParse(*CGI);
&Controlli;
if ( $CGI{"stampo"} eq "si" ) {
&StampaModulo;
}
&InviaModulo;
sub Controlli {
Ecco il primo if che controlla se il campo "nome" contiene qualcosa. In caso negativo verrà
restituito un messaggio di errore
if ( $CGI{"nome"} eq "" ) {
&Errore("Non hai inserito il tuo nome");
exit;
}
Qui invece con una semplice espressione regolare si verifica se il nome immesso contiene
numeri o caratteri speciali
if ( $CGI{"nome"} !~ /^[a-zA-Z' ]*$/ ) {
&Errore("Il nome non puo' contenere numeri o caratteri speciali");
exit;
}
Poi si prosegue con i controlli sul campo "eta"
if ( $CGI{"eta"} eq "" ) {
&Errore("Non hai inserito la tua eta'");
exit;
}
if ( $CGI{"eta"} !~ /^[0-9]*$/ ) {
&Errore("L'eta' non puo' contenere lettere o caratteri speciali");
exit;
}
}
La subroutine &Errore riceve come parametro una stringa contenente il tipo di errore
commesso e prepara la pagina html da restituire al client
sub Errore {
local ($errore) = @_;
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD><TITLE>Errore</TITLE></HEAD>\n<BODY
bgcolor=#ffffff>\n";
print "<H1 align=center>Errore nella compilazione del modulo</H1>\n";
print "<H2 align=center>$errore</H1>\n<P align=center>\n";
print "<A HREF=\"http://localhost/modulo.html\">Ritorna al form</A>\n";
6
print "</BODY>\n</HTML>\n\n";
StampaModulo invece invia a Netscape tutti i dati inseriti nel form, ma solo nel caso in cui
l'utente abbia scelto l'opzione "Si" alla richiesta "Stampo i dati?".
sub StampaModulo {
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>\n";
print "<TITLE>Modulo</TITLE>\n";
print "</HEAD>\n";
print "<BODY bgcolor=#ffffff>\n";
print "<H1 align=center>MODULO INVIATO!</H1>\n
<FONT SIZE=10><B>Io</B><FONT SIZE=5> e <FONT
SIZE=10><B>Linux</B></FONT>\n
<HR><P>\n
<B>Nome: </B>$CGI{\"nome\"}<P>\n
<B>Eta': </B>$CGI{\"eta\"}<P>\n
<B>Utilizzo Linux per: </B>$CGI{\"uso\"}<P>\n
<B>Cosa penso di Linux:</B><P>\n";
foreach (split("\0", $CGI{"penso"})) {
s/\n/<br>\n/g;
print "$_\n";
}
print "</BODY>\n</HTML>\n\n";
}
Lo script si conclude con una chiamata al programma sendmail per inviare via posta elettronica
i dati del modulo. Supponendo che sia stato root a predisporre il servizio cgi la mail sarà quindi
inviata a root@localhost. Per finire si noti il controllo sull'esecuzione del comando sendmail con
la funzione die.
sub InviaModulo {
$sendmail = "/usr/bin/sendmail -bs";
open (EMAIL, "|$sendmail >/dev/null") || die "Cannot exec mail command";
print EMAIL "mail from: modulo.cgi\n";
print EMAIL "rcpt to: root\@localhost\n";
print EMAIL "data\n";
print EMAIL "From: modulo.cgi\n";
print EMAIL "To: root\@localhost\n";
print EMAIL "Subject: Io e Linux\n\n\n";
print EMAIL "Nome: $CGI{\"nome\"}\n\n";
print EMAIL "Eta': $CGI{\"eta\"}\n\n";
print EMAIL "Utilizzo Linux per: $CGI{\"uso\"}\n\n";
print EMAIL "Cosa penso di Linux:\n";
print EMAIL "$CGI{\"penso\"}\n";
print EMAIL ".\n";
print EMAIL "quit\n";
close (EMAIL);
}
Salviamo lo script modulo.cgi nella directory /cgi-bin del server e con un chmod 755
modulo.cgi daremo la possibilità a tutti gli utenti di eseguirlo.
GESTIRE GLI ERRORI
Uno script di gateway che si rispetti deve essere chiaro, veloce ma soprattutto deve prevedere
la maggior parte degli errori, sia di sistema e sia formali. Gli errori del primo tipo, come quelli
dovuti a problemi di I/O su file, di invio di posta elettronica, di chiamate al sistema operativo
etc, si controllano con istruzioni die. Lo script si blocca e il server web tiene traccia dell'errore
7
su un file di log (error_log se usiamo Apache). Per il secondo tipo di errori, commessi durante
la compilazione del form, si dovrebbe prima informare l'utente del tipo di errore commesso e
poi sospendere l'esecuzione dello script. E' sufficiente un messaggio (pagina html) restituito
dal cgi in cui venga evidenziato dove è stato commesso l'errore e perché (vedi figure 2 e 3).
Non è bello predisporre un form per consentire la registrazione ad un servizio e ricevere nomi
fasulli, età assurde e indirizzi e-mail inesistenti. Chiaramente non si può controllare tutto (gli
script neurali non esistono ancora) ma con gli strumenti che abbiamo si può fare molto. Si
potrebbe migliorare il nostro esempio scrivendo un po' di codice per restituire oltre al
messaggio anche il from con i campi corretti già compilati e quelli errati vuoti: un piccolo
esercizio prima delle ferie. Buone vacanze a tutti.
MENU DI LINK E CONTATORI
Lo scripting cgi, oltre che fornire un ponte interattivo tra la pagina web e l'utente finale, viene
spesso usato per superare i limiti piuttosto ristretti dell'HTML. Un tipico esempio riguarda sia la
creazione di un menu di link che consente di ottimizzare lo spazio disponibile sulla pagina e sia
la generazione di statistiche
Il trucco consiste nell'interfacciare il tag SELECT con uno script perl che restituirà il documento
html selezionato. In un primo tempo, quindi, svilupperemo un menu statico e poi scopriremo i
segreti dei contatori dinamici.
UN MENU STATICO
Poiché il nostro menu dovrà essere sempre visibile, formatteremo la pagina html utilizzando i
frame. Ecco il codice html di base: menu.html:
<HTML>
<HEAD>
<TITLE>Menu di Link</TITLE>
</HEAD>
<FRAMESET cols="150,*">
<FRAME name="sx" src="sx.html" scrolling="no">
<FRAMESET rows="100,*">
<FRAME name="form" src="form.html" scrolling="no">
<FRAME name="central" src="central.html" scrolling="auto">
</FRAMESET>
</FRAMESET>
</HTML>
sx.html, frame di sinistra:
<HTML>
<HEAD>
<TITLE>Menu di Link</TITLE>
</HEAD>
<BODY bgcolor="#000080">
</BODY>
</HTML>
form.html, frame del menu:
<HTML>
<HEAD>
<TITLE>Menu di Link</TITLE>
</HEAD>
<BODY bgcolor="#c0c0c0">
<CENTER>
<FORM METHOD="POST" ACTION="http://localhost/cgi-bin/menu.cgi"
target=central>
8
<SELECT NAME="link">
<OPTION SELECTED>Link 1
<OPTION>Link 2
<OPTION>Link 3
<OPTION>Link 4
<OPTION>Link 5
</SELECT>
<INPUT TYPE="submit" NAME="" VALUE="OK">
</FORM>
</CENTER>
</BODY>
</HTML>
central.html, frame centrale:
<HTML>
<HEAD>
<TITLE>Menu di Link</TITLE>
</HEAD>
<BODY bgcolor="#ffffff">
</BODY>
</HTML>
Ed ora passiamo allo script. Ci servirà innanzitutto un file (link.db) che conterrà tutti link. Una
specie di piccolo database così formattato:
Descrizione link;url
Link 1;http://localhost/link1.html
Link 2;../link2.html
Ogni riga sarà un record, costituito a sua volta da due campi separati da un puto e virgola ;.
Nel capo 1 ci sarà la descrizione del link, uguale a quella del tag SELECT nel frame del menu;
mentre nel campo 2 scriveremo l'URL assoluto o relativo. Salviamo il file link.db nella directory
dei cgi del server http ed assicuriamoci che i diritti di lettura siano attivi con chmod 644
link.db. A questo punto scriviamo il codice perl.
Lo script deve leggere il file link.db e con la funzione split memorizzare i due campi nell'array
@campo usando come separatore il punto e virgola. A questo punto basterà verificare se il
contenuto della variabile link del tag SELECT, e passata al cgi, e' uguale al primo campo del
record. Se ciò è vero si restituirà la pagina individuata dall'URL del campo 2 con un print
"Location:\n\n".
#!/usr/bin/perl
require "cgi-lib.pl";
$menudb = "./link.db";
&ReadParse(*CGI);
&CercaLink($menudb, $CGI{link});
sub CercaLink {
local ($db, $cgi) = @_;
open (DB, "<$db");
while (<DB>) {
@campo = split(/;/, $_);
if ($campo[0] =~ /$cgi/i) {
close(DB);
last;
}
}
print "Location: $campo[1]\n\n";
9
exit;
}
SERVER SIDE INCLUDES
Prima di affrontare il tema dei contatori dinamici è necessario spendere qualche parola sulle
direttive SSI. L'uso di un contatore in una pagina web, è reso possibile solo se sono state
attivate speciali istruzioni nel file di configurazione srm.conf del server (apache o NCSA).
AddType text/x-server-parsed-html .shtml
Tale istruzione è note anche come direttiva di Server Side Includes, e fornisce un metodo per
poter eseguire "al volo" programmi da un documento html. L'esecuzione dello script
counter.cgi si può quindi ottenere con la seguente istruzione nel file index.shtml.
<!--#exec cgi="cgi-bin/counter.cgi"-->
UN CONTATORE DINAMICO
Forse parlare di contatori oggi farà sorridere. In Rete se ne trovano per tutti i gusti e con
poche modifiche si possono adattare alle esigenze di ciascun utente. Tuttavia l'argomento
contatori, solleva una questione piuttosto importante della programmazione CGI, ovvero
l'accesso concorrente ai file. Ogni volta che si accede ad una pagina web dotata di counter,
parte un programmino che visualizza e poi incrementa un numero contenuto in un file.
Supponiamo che più client, sparsi per il mondo, accedano contemporaneamente a quella
pagina. Ne consegue che quel piccolo file di log potrà subire dei danneggiamenti o addirittura
cancellazioni a causa di operazioni di scrittura in contemporanea. Sorge quindi la necessità di
bloccare in esclusiva la scrittura sul file per ogni singola richiesta. Basta utilizzare una
chiamata alla funzione di sistema flock e al resto cui pensa il sistema operativo accodando
tutte le altre richieste e garantendo una scrittura corretta.
#!/usr/bin/perl
open (COUNT, "+<counter.log") || die "cannot open counter.log";
flock(COUNT, 2) || die "cannot lock counter.log";
$count = <COUNT>;
if ($count =~ /\n$/) {
chop $count;
}
@numero = split(//, $count);
@img = `ls *.gif`;
if (@img) {
print "Content-type: text/html\n\n";
$num = length($count);
$i=0;
while ($i <= $num-1) {
print "<img src=$numero[$i].gif>";
$i++;
}
}
else {
print "Content-type: text/plain\n\n";
print "Benvenuto, sei il visitatore $count\n";
}
$count++;
seek(COUNT, 0, 0);
print COUNT "$count";
close(COUNT);
10
Lo script è abbastanza semplice. Per comodità la directory del file di log (counter.log) e delle
immagini per il contatore (0.gif, 1.gif, etc.) è la stessa in cui sono contenuti i CGI del server. Il
file di log, inoltre, non deve essere protetto in scrittura consentendo a chi si connette alla
nostra home page di aggiornare il contatore. Il file counter.log deve esistere. Basta editarlo,
scrivere il numero 1 (per i più furbi va bene anche 1000) e salvarlo.
A questo punto possiamo analizzare il programma. counter.cgi, è questo il nome dello script,
inizia con una istruzione di apertura del file di log subito seguita dalla chiamata alla funzione
flock. Con <COUNT> si legge il file e con split si crea l'array @numero i cui elementi serviranno
per concatenare i tag IMG creando la sequenza corretta di immagini gif. Prima di chiudere il file
di log, si incrementa il contatore con $count++, si esegue un rewind con la funzione seek (in
pratica mi posiziono all'inizio del file) e poi scrivo il valore aggiornato con print COUNT
"$count". Benone. Se abbiamo 10 immagini gif con i numeri da 0 a 9 possiamo provare il
nostro contatore. Sarà un po' spartano ma rappresenta una buona base di partenza per fare
esercizio. E se non abbiamo i numeri .gif? Niente paura. Lo scrpt stamperà il contenuto di
counter.log come semplice testo anziché come sequenza di immagini: versatile no?
LE SPECIFICHE CGI
Nel momento in cui il server http esegue un cgi, vengono impostate numerose variabili
d'ambiente. Ecco le più utili ed importanti, usate soprattutto per ottenere informazioni
aggiuntive altrimenti irraggiungibili.
SERVER_SOFTWARE
Il nome e la versione del server http che risponde alla richiesta cgi
SERVER_NAME
Il nome del server http che può essere un alias di DNS o un indirizzo IP numerico
GATEWAY_INTERFACE
La versione delle specifiche CGI
SERVER_PROTOCOL
Il nome e la versione del protocollo di comunicazione (HTTP)
SERVER_PORT
Il numero di porta a cui e' indirizzata la richiesta (di solito la porta 80)
REQUEST_METHOD
Il metodo usato per la richiesta (GET, POST, etc)
SCRIPT_NAME
Il path completo del programma cgi
QUERY_STRING
Contiene i dati passati dalle richieste GET, ISINDEX, ISMAP
REMOTE_HOST
Il nome dell'host che invia la richiesta
REMOTE_ADDR
L'indirizzo IP dell'host che invia la richiesta
AUTH_TYPE
Il metodo usato dal protocollo di autenticazione dell'utente. Questa variabile è settata
solo se il server supporta tale protocollo e se il cgi è protetto.
REMOTE_USER
Il nome dell'utente autentificato. Questa variabile è settata solo se il server supporta il
protocollo di autenticazione e se il cgi è protetto.
CONTENT_TYPE
Per le richieste POST e PUT questa variabile contiene il tipo MIME dei dati passati al cgi
CONTENT_LENGTH
La lunghezza dei dati passati con il metodo POST.
HTTP_ACCEPT
I tipi MIME accettati dal client
HTTP_USER_AGENT
Il nome e la versione del browser che ha inviato la richiesta
Per risalire al contenuto di tale enviroment si usa l'istruzione $ENV{"nome_variabile"}.
11
BASI DI DATI E mSQLPERL
Il protocollo HTTP associato al linguaggio SQL ha esteso la gestione e l'interrogazione di
DataBase a livello mondiale. Soffermiamoci sugli aspetti principali di questa nuova tecnologia
in un ambiente di sviluppo facile ma efficace
Una buona installazione di Linux, il Mini SQL 2.0 di Hughes Technologies e la libreria MsqlPerl
1.17, rappresentano un mix formidabile di strumenti per lo sviluppo di applicazioni rivolte
all'uso di DataBase via Internet. Dopo una prima fase in cui affileremo gli "arnesi del
mestiere", passando dalla compilazione di mSQL all'installazione di MsqlPerl, saremo
finalmente pronti per capire come funzionano le query su archivi SQL utilizzando lo scripting
CGI. Un'attenta lettura dei files INSTALL e README, rispettivamente per i due pacchetti
software sopra elencati, e' raccomandabile per uscire indenni dalle compilazioni. Purtroppo per
motivi di spazio non sarà possibile affrontare in modo sistematico e graduale questo tema,
tuttavia, le tecniche che verranno illustrate serviranno come base per creare in proprio vere
applicazioni. Per finire due parole sul software che useremo. Su Linux, beh, penso non ci sai
molto da dire: e' il migliore!!! :-) mSQL e' il nostro RDBMS, ovvero un gestore di DataBase
relazionali sql. MsqlPerl, invece, e' una libreria di funzioni, che tramite l'uso delle API di mSQL,
fornisce la possibilità di gestire gli archivi con gli scripts Perl. Interfacciare il tutto con lo
standard CGI, sarà poi veramente facile.
COSA OFFRE MSQLPERL
Per iniziare ad usare la libreria MsqlPerl basta l'istruzione Perl use Msql, dopo di che è possibile
connettersi a uno o più servers mSQL e costruire query mediante un'interfaccia ad oggetti
molto semplice. Gli oggetti disponibili sono due: un DataBase handle e uno statment handle.
Perl ritorna un oggetto DataBase utilizzando il metodo Connect. In pratica per aprire un
DataBase si usa l'istruzione
$dbh = Msql->connect($hostname,$databasename);
A questo punto si può iniziare con le query usando l'istruzione
$sth = $dbh->query("serie di istruzioni SQL");
Il risultato di una query è sempre una tabella ed i metodi principali per risalire al contenuto o
alla struttura di tale tabella sono:
@row = $sth->fetchrow oppure %hash = $sth->fetchhash, ritornano il contenuto di un singolo
record della query.
$numrows = $sth->numrows, ricavo il numero di records restituiti dalla query.
$numfields = $sth->numfields, ottengo il numero di campi.
@list => $sth->name, ritorna il nome dei campi della tabella. Ce ne sono molti altri tutti
documentati negli esempi della libreria. Ora siamo pronti per creare una piccola applicazione
per la gestione di un archivio contenente nomi, indirizzi e telefoni dei nostri amici.
LA CREAZIONE DI UNA TABELLA
Prima di procedere alla scrittura dello script CGI, si dovrà creare il DataBase AMICI. Basta
entrare nella directory contenente i files binari di mSQL (tipicamente /usr/local/Hughes/bin) ed
eseguire il comando
msqladmin create AMICI
Poi si passa alla creazione della tabella amici. Si usa il comando msql AMICI ed al prompt
(mSQL>) si immette il comando \e Verrà aperto un editor in cui scriveremo le seguenti linee
create table amici (
NOME char(30),
INDIRIZZO char(40),
TELEFONO char(12)
12
)
Abbiamo in sostanza scritto una query SQL con cui creeremo la tabella amici composta da tre
campi (NOME, INDIRIZZO, TELEFONO) che ospiteranno dati di tipo carattere lunghi
rispettivamente 30,40 e 12. Salviamo il contenuto dell'editor e con il comando \g si eseguirà la
query. A questo punto possiamo uscire da mSQL ed iniziare a scrivere nella directory cgi-bin
del server http il file amici.cgi.
#!/usr/bin/perl
require "cgi-lib.pl";
use Msql;
&ReadParse(*CGI);
if ($dbh = Msql->connect("localhost", "AMICI")) {
&Controlli;
}
else {
&HTML("Impossibile aprire il DataBase");
}
sub Controlli {
if ($CGI{cerca}) { &Cerca; }
if ($CGI{tutto}) { &ListaTutto; }
if ($CGI{aggiorna}) { &Aggiorna; }
if ($CGI{cancella}) { &Cancella; }
}
sub HTML {
local($msg) = @_;
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>\n<TITLE>Risultati</TITLE>\n</HEAD>\n";
print "<BODY bgcolor=#ffffff>\n";
print "<H3 align=center>$msg</H3>\n";
print "</BODY>\n</HTML>\n";
exit;
}
L'INTERROGAZIONE SUL DB
Lo script amici.cgi permette sia una ricerca mirata delle informazioni e sia una ricerca estesa
su tutto il DataBase. Nel primo caso la subroutine &Cerca restituirà tutti i record che
soddisfano i criteri di interrogazione forniti nel form html, mentre con &ListaTutto si ottiene un
elenco completo e ordinato per nome di tutta al tabella amici. Iniziamo con il codice html
<HTML>
<HEAD>
<TITLE>DB Amici</TITLE>
</HEAD>
<BODY>
<FORM METHOD=POST ACTION=http://localhost/cgi-bin/amici.cgi>
Ricerca per
<SELECT name=campo>
<OPTION>NOME
<OPTION>INDIRIZZO
<OPTION>TELEFONO
</SELECT><BR>
<INPUT TYPE=text name=valore size=40 maxlength=40>
<INPUT TYPE=submit name=cerca value=Cerca>
<INPUT TYPE=reset name="" value=Cancella><BR>
<INPUT TYPE=submit name=tutto value="Lista tutto">
</BODY>
</HTML>
13
e poi il codice Perl.
sub Lista {
$records = $sth->numrows;
if ($records == 0) {
&HTML("Dato non trovato");
}
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>\n<TITLE>Risultati</TITLE>\n</HEAD>\n";
print "<BODY>\n";
print "<H2 align=center>Risultati della ricerca</H2>\n";
print "<CENTER>\n<TABLE border bgcolor=ffffff cellpadding=3>
<TR><TD><B>NOME</B></TD>
<TD><B>INDIRIZZO</B></TD>
<TD><B>TELEFONO</B></TD></TR>\n";
while ($i < $records) {
@field = $sth->fetchrow;
print
"<TR><TD>$field[0]</TD><TD>$field[1]</TD><TD>$field[2]</TD></TR>\n";
$i++;
}
print "</TABLE>\n</CENTER>\n";
print "</BODY>\n</HTML>\n";
exit;
}
Alla base di entrambe le ricerche c'è l'enunciato SELECT che nella subroutine &Cerca viene
associato all'operatore CLIKE e al carattere speciale %, garantendo la possibilità di effettuare
ricerche di sottostringhe ignorando inoltre i dati scritti in minuscolo o maiuscolo: una specie di
espressione regolare. Ovviamente possiamo eseguire ricerche per nome, indirizzo o numero
telefonico, quindi su tutti e tre i campi della tabella.
La clausola ORDER BY NOME ASC, infine, permette la restituzione dei dati in ordine alfabetico e
tale ordinamento viene eseguito sul campo NOME.
sub Cerca {
if ($sth = $dbh->query("SELECT * FROM amici WHERE $CGI{campo}
CLIKE '%$CGI{'valore'}%' ORDER BY NOME ASC")) {
&Lista;
}
else {
&HTML("Impossibile eseguire la ricerca");
}
}sub ListaTutto {
if ($sth = $dbh->query("SELECT * FROM amici ORDER BY NOME ASC")) {
&Lista;
}
else {
&HTML("Impossibile listare il DataBase");
}
}
Ricordiamo che l'operatore CLIKE non è compatibile con il linguaggio ANSI SQL.
L'AGGIORNAMENTO DEL DB
Le forme principali di aggiornamento di un DataBase consistono nell'inserimento di un
nuovo record in una tabella, oppure nella cancellazione di dati. Nel nostro caso, mediante
14
la subroutine &Aggiorna, forniremo la possibilità di inserire nuovi nomi, indirizzi e numeri di
telefono dei nostri amici, mentre con &Cancella si potrà eliminare un intero record inserendo in
un form il nome della persona che si desidera cancellare. Prepariamo quindi il form html
<HTML>
<HEAD>
<TITLE>DB Amici</TITLE>
</HEAD>
<BODY>
<H3>Aggiornamento DataBase</H3>
<FORM METHOD=POST ACTION=http://localhost/cgi-bin/amici.cgi>
<TABLE>
<TR>
<TD><B>Nome:</B></TD>
<TD><INPUT TYPE=text name=nome size=30 maxlength=30></TD>
</TR>
<TR>
<TD><B>Indirizzo:</B></TD>
<TD><INPUT TYPE=text name=indirizzo size=40 maxlength=40></TD>
</TR>
<TR>
<TD><B>Telefono:</B></TD>
<TD><INPUT TYPE=text name=telefono size=12 maxlength=12></TD>
</TR>
<TR>
<TD colspan=3><INPUT TYPE=submit name=aggiorna value=Aggiorna>
<INPUT TYPE=reset name="" value=Cancella></TD>
</TR>
</TABLE>
</BODY>
</HTML>
e poi scriviamo il codice per le due subroutine. Si noti come le operazioni di inserimento e di
cancellazione vengano rispettivamente eseguite usando gli enunciati INSERT e DELETE del
linguaggio ANSI SQL.
sub Aggiorna {
if ($sth = $dbh->query("INSERT INTO amici VALUES ('$CGI{'nome'}',
'$CGI{'indirizzo'}',
'$CGI{'telefono'}')")) {
&HTML("DataBase aggiornato con successo");
}
else {
&HTML("Impossibile aggiornare il DataBase");
}
}
sub Cancella {
$sth = $dbh->query("DELETE FROM amici WHERE NOME='$CGI{'nome'}'");
&HTML("Dato eliminato con successo");
}
SU INTERNET
L'ultima versione di mSQL può essere scaricata dal sito http://www.Hughes.com.au Va
ricordato che il programma non può essere usato per fini commerciali. In questo caso è
necessaria una richiesta scritta da inoltrare alla Hughes Technologies ([email protected]).
Per la libreria MsqlPerl http://franz.ww.tu-berlin.de/msqlperl.
15
RICERCHE FULL-TEXT
Il successo di Internet è dovuto in larga misura alla presenza di numerosi motori di ricerca che
permettono di reperire in tempi più o meno brevi qualsiasi tipo di informazione
Yahoo, Altavista, Magellano, tutti nomi famosi. Ogni navigatore però ha il suo preferito e lo
sfrutta il più possibile. Noi ce ne costruiremo uno personale che potremo usare per offrire un
servizio utile al nostro sito web oppure solo per il gusto di programmare qualcosa di nuovo.
MYSEARCH.CGI
Lo script mysearch.cgi è composto da quattro subroutine principali: &HTML, &Next, &Form e
&Cerca. Nel "main" del programma
#!/usr/bin/perl
require "cgi-lib.pl";
$host = "http://localhost/";
$search_dir = "/home/httpd/html/";
@url_dir = ('', 'www/');
chdir($search_dir);
foreach $dir (@url_dir) {
push(@file, `ls $dir*.htm*`);
}
&ReadParse(*CGI);
&HTML;
&Cerca;
si richiama la libreria cgi-lib.pl con la consueta funzione require e poi si impostano alcune
variabili scalari. $host è l'URL del nostro server, $search_dir contiene il path assoluto della
directory in cui sono contenuti tutti i documenti html pubblici (la DocumentRoot nel file
httpd.conf del server Apache). Poi abbiamo l'array @url_dir, inizializzato con le sottodirectory
pubbliche del server web e che servirà per costruire i link con i tag <A HREF> restituiti dalla
ricerca. Ovviamente tali variabili possono essere adattate alla particolare configurazione del
server senza modificare altre parti del programma. La funzione chdir, predefinita in Perl,
cambia la directory di lavoro da /home/httpd/cgi-bin/ a /home/httpd/html, e per ogni
sottodirectory web si "riempie" l'array @file con la funzione push tramite la chiamata di
sistema ls che restituisce la lista di tutti i file htm o html su cui si farà la ricerca. Sulla funzione
&HTML
sub HTML {
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>\n<TITLE>Risultati della
ricerca</TITLE>\n</HEAD>\n";
print "<BODY bgcolor=#ffffff>\n";
print "<H3>Risultati della ricerca</H3>\n";
}
non c'è molto da dire. In sostanza apre l'output di tag html per costruire la pagina di risposta
alla nostra interrogazione. Tutti i motori di ricerca che si rispettino, offrono la possibilità di
ottenere una paginazione dei link restituiti. Yahoo ed Altavista fanno da esempio. In pratica,
poiché e' molto scomodo e poco efficiente ottenere una valanga di tag <A HREF> con
rispettiva descrizione, conviene sospendere l'elenco restituito per poi riprenderlo cliccando sul
consueto pulsante a piè di pagina. All'utente verrà da un lato risparmiata l'attesa della fine
della ricerca e dall'altro gli si premette di scegliere tra il proseguire con l'elenco o l'eseguire
una nuova ricerca. Il numero di link per pagina può essere impostato sullo stesso form usato
per immettere la parola chiave oppure direttamente nel programma. Tra le due opzioni noi
sceglieremo la seconda, lasciando la prima a quanti vorranno fare esercizio con Perl.
sub Next {
local($next, $cerca) = @_;
print "<form method=post action=http://localhost/cgi-bin/mysearch.cgi>";
16
print
print
print
print
}
"<input type=hidden name=next value=$next>";
"<input type=hidden name=cerca value=$cerca>";
"<input type=submit name=nextbutton value=Altri...>";
"</form>\n";
sub Next è una funzione un po' particolare. Innanzi tutto riceve due parametri, passaggio
effettuato con una chiamata all'interno della subroutine &Cerca, e poi prepara un form html (il
pulsante per la paginazione) in cui compaiono ben due campi di input nascosti. Cliccando sul
pulsante Altri..., non si fa altro che passare allo script search.cgi il contenuto delle variabili
$next e $cerca, rispettivamente il numero di file processati dall'algoritmo di ricerca e la parola
chiave da trovare. La stampa dei link verrà poi ripresa se i documenti rimanenti soddisfano i
criteri di interrogazione. Anche Form e' molto semplice. Viene usata per poter fare una nuova
ricerca alla fine di ogni schermata di link, evitando l'uso ripetuto del pulsante "Back" di
Netscape per ritornare al form di partenza.
sub Form {
print "<form method=post action=http://localhost/cgi-bin/mysearch.cgi>";
print "<input type=hidden name=next value=0>";
print "<input type=text name=cerca size=40 maxlength=40>";
print "<input type=submit value=Cerca>";
print "<input type=reset value=Annulla>";
print "</form>";
print "</body>";
print "</html>\n";
}
Si noti la presenza di un campo nascosto, usato per azzerare il numero di file processati e
poter quindi eseguire una ricerca sul totale dei documenti html. Ed ora inizia il bello. Siamo
finalmente arrivati alla subroutine Cerca. Non spaventiamoci perché anche questa funzione non
è affatto complicata. Grazie alla potenza delle espressioni regolari di Perl, potremo fare
ricerche full-text scrivendo in pratica solo due righe di codice: provate con il C/C++ e buona
fortuna con i puntatori. Poiché il risultato della nostra ricerca sarà un elenco di titoli di pagine
html e rispettivo url, il metodo migliore per presentarlo consiste nell'uso delle liste puntate con
i tag <UL>/<LI>. Inizializziamo subito la variabile $link per tale compito. Poi ci servono due
contatori, $i e $k. Il primo avrà valore massimo pari al numero di link per pagina che si vorrà
restituire; il secondo memorizza il numero di documenti interrogati.
sub Cerca {
$link = "<UL>\n";
$i = 0; $k = 0;
foreach $file (@file) {
open (FILE, "$search_dir$file");
while (<FILE>) {
if ($_ =~ /<title>(.*)<\/title>/i) {
$title = "$1";
}
else {
$title = $file;
}
if ($_ =~ /$CGI{'cerca'}/i) {
if ($k > $CGI{'next'}) {
$trovato = "yes";
$link .= "<LI><A HREF=$host$file>$title</A><BR>\n";
$link .= "<I>$host$file</I>\n</LI><P>\n";
$i++;
last;
}
17
}
}
close(FILE);
$k++;
if ($i == 3) {
last;
}
}
if (! $trovato) {
print "\"<B>$CGI{'cerca'}\"</B> non trovato\n";
}
else {
print "$link</UL>\n";
}
if ($i == 3) {
&Next($k-1, $CGI{'cerca'});
}
&Form;
}
Il ciclo foreach consente di aprire ogni file html dell'array @file, estrarne il titolo e verificare se
il file contiene la stringa o sottostringa cercata. Il titolo viene memorizzato nella variabile
speciale $1. In Perl questa variabile è il contenuto delle parentesi dell'espressione regolare
/<title>(.*)<\/title>/i. Questa "regular expression" dice in pratica di riscontrare qualsiasi
sequenza di caratteri, eccetto il new-line, racchiusa tra i tag <TITLE> e
</TITLE>. Siamo quindi sicuri che tale sequenza è proprio il titolo del file html. Se però il
riscontro non dovesse avere successo, il titolo sarà il nome del file. Successivamente con
/$CGI{'cerca'}/i si riscontra la parola cercata e poi si verificano ed incrementano i contatori.
Infine una ulteriore serie di confronti stabilirà se il risultato della ricerca potrà essere stampato
oppure permettere una nuova interrogazione se invece la ricerca dovesse fallire. Abbiamo
veramente concluso. Questa rassegna di articoli sul Perl e lo scripting CGI termina qui. Si
potrebbero scrivere ancora molte pagine ma è giusto lasciare spazio a nuove idee e nuovi
argomenti. Le nuove tecnologie per lo sviluppo su Internet richiedono macchine sempre più
potenti e software affamati di risorse. Linux e il suo kit di strumenti di programmazione non
deve temere i giganti come Microsoft e Lotus poiché rappresenta un sistema eccezionale per
programmare a basso costo ma con facilità ed efficacia. Nel frattempo buona ricerca a tutti.
18