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