Introduzione
Questo articolo è il primo di una serie di piccoli passi grazie ai quali ci accosteremo gradualmente al mondo dei Web Services. Cominceremo con l'occuparci di quella che (per ora) rappresenta una delle due colonne portanti degli standard più diffusi per i "servizi web": gli scambi HTTP (l'altra è l'utilizzo di XML).
Scopriremo come imitare l'attività di un browser attraverso le funzioni per i socket di php: la connessione ad un server WEB, la richiesta e la lettura di file, ma soprattutto simuleremo con Php l'invio di di dati attraverso i form.
TCP/IP e HTTP
HTTP (HyperText Transfer Protocol) sta alla base dell'intero World Wide Web, poichè regola il modo in cui i browser richiedono i file ai webserver e quello in cui quest'ultimi rispondono: in un certo senso si tratta del linguaggio nel quale i due interlocutori si intendono.
A sua volta HTTP poggia su TCP/IP (Transmission Control Protocol/Internet Protocol) dal quale eredita molte proprietà, ricorrendo anche in questo caso ad una metafora un po' forzata potremmo dire che le comunicazioni HTTP viaggiano sulle ali di TCP/IP.
Come funzionano gli scambi HTTP?
Ecco una descrizione estremamente semplificata di ciò che accade quando navighiamo utilizzando il browser. Immaginiamo di voler visitare la pagina http://freephp.html.it/scripts.php
- Il browser chiede al sistema di aprire una connessione sulla porta 80 della macchina identificata nel WEB dall'indirizzo IP 212.110.12.173 (quello corrispondente al domain freephp.html.it).
- su tale porta sta in ascolto il Webserver che una volta ricevuti gli "HTTP request header" (la richiesta da parte del browser) si farà carico di reperire il documento identificato dal path "/scripts.php".
- Se tutto va bene verrà generato un "HTTP response header" che informerà il client dell'esito positivo, ad esso seguirà il flusso di dati del documento. Nel caso vi siano dei problemi invece il webserver comunicherà un messaggio di errore.
- Quando il server ha terminato di inviare dati chiude la connessione.
SOCKET
Alla fase della connessione corrisponde l'apertura dei socket TCP/IP, sempre banalizzando al massimo possiamo definire i socket come le due estremità del canale di comunicazione che si è appena creato: attraverso questo canale client e server si scambiano request e response.
I socket TCP/IP vengono detti anche stream-socket (ovvero "socket di flusso").
Su TCP/IP, HTTP e i socket ci sarebbe ancora molto da dire, tuttavia ho provato a semplificare al massimo i concetti per evitare divagazioni che non sarebbero utili agli scopi di questo articolo; chiunque cerchi approfondimenti troverà nella rete una quantità enorme di ottimo materiale.
Socket con Php
Php dalla versione 4.1 mette a disposizione un estensione completa per il networking tale da consentire di realizzare uno script Php che agisca da socket-server, ma sin da Php 3 esiste una funzione generica in grado di fornire un'interfaccia per la gestione dei socket lato client, fsockopen(), ed è questa che utilizzeremo negli esempi che seguiranno.
Simulare request e respose HTTP
Appena comparve sulla scena HTTP era molto semplice, ecco lo striminzito header (intestazione) inviato dal client al server:
GET /percorso_pagina.html
la richiesta terminava immediatamente con i caratteri rn (carriage return e line feed) e in caso di successo la risposta consisteva semplicemente nei dati che rappresentavano il documento.
fsockopen()
In apparenza con fsockopen() dobbiamo operare come quando viene aperto un puntatore ad un file con fopen() in modalità "w+": si scrivono dei dati con fputs() o fwrite(), e si possono leggere con fgets(). In realtà si tratta soltanto di una somiglianza superficiale perchè, come possiamo immaginare, quanto avviene sotto l'interfaccia è molto diverso.
Grazie a questa funzione potremmo creare un socket verso qualsiasi servizio (FTP, SMTP etc.etc.) e "dialogare" con esso purchè siano note le specifiche del protocollo in questione. Ovviamente le prestazioni non sono paragonabili a quelle delle estensioni php apposite, quando ci sono. Il servizio che in questo momento ci interessa è il server WEB e le specifiche sono quelle HTTP.
Esempio di richiesta GET semplice
<?php
/***
SCRIPT simple_get.php
***/
$host="localhost" ;
$target="/info.php" ;
$port=80 ;
$timeout=60;
$sk=fsockopen($host,$port,$errnum,$errstr,$timeout) ;
if(!is_resource($sk)){
exit("Connessione fallita: ".$errnum." ".$errstr) ;
}
else{
fputs ($sk, "GET $targetrn");
$dati="" ;
while (!feof($sk)) {
$dati.= fgets ($sk,2048);
}
}
fclose($sk) ;
echo($dati) ;
?>
Consiglio di creare sul server a cui ci si connette una pagina chiamata "info.php" in cui inserire un semplice:
<?php
/***
PAGINA info.php
***/
phpinfo() ;
?>
e di utilizzare questa come file di prova per la richiesta al Webserver: tenendo d'occhio il risultato, e precisamente la tabella "HTTP headers Information", otterremo informazioni davvero utili su request e response.
Richiamate poi "info.php" normalmente da un browser qualsiasi e noterete la differenza con la pagina prelevata attraverso lo script precedente: questa volta la tabella contiene molte più informazioni, e precisamente tutti gli header scambiati tra browser e server.
N.B.
Naturalmente anche nelle prove in locale il percorso della pagina da richiedere (nel nostro caso la variabile $target), deve essere completo a partire dall'host o dall'IP del server cui si effettua la connessione.
Quindi ad esempio in localhost/directory1/directoy2/pagina.php $target
sarebbe
"/directory1/directory2/pagina.php"
L'evoluzione di HTTP
Con HTTP/1.0, e ancor di più con HTTP/1.1, assistiamo una proliferazione degli header e dei metodi accettati: a GET si sono aggiunti POST e HEAD, dei quali parleremo più avanti, inoltre non solo il server riconosce un numero maggiore di intestazioni HTTP, ma risponde a sua volta con una serie di header che precedono l'invio del documento richiesto.
Richiesta GET HTTP 1.0 con fsockopen()
<?php /*** SCRIPT complex_get.php ***/ $host="localhost" ; $target="/info.php?var1=valore1&var2=valore2" ; $port=80 ; $timeout=60; $protocol="HTTP/1.0" ; $br="rn" ; $sk=fsockopen($host,$port,$errnum,$errstr,$timeout) ; if(!is_resource($sk)) { exit("Connessione fallita: ".$errnum." ".$errstr) ; } else { $headers="GET ".$target." ".$protocol.$br ; $headers.="Accept: image/gif, image/x-xbitmap, image/jpeg".$br ; $headers.="Accept-Language: dialetto veneto".$br ; $headers.="Host: ".$host.$br ; $headers.="Connection: Keep-Alive".$br ; $headers.="User-Agent: Socket-PHP-browser 1.0".$br; $headers.="Referer: http://www.bwbwabwa.it".$br ; $headers.="X-INVENTATO: Ciao a tutti".$br.$br; fputs($sk,$headers) ; $dati=""; while (!feof($sk)) { $dati.= fgets ($sk,2048); } } fclose($sk) ; echo($dati) ; ?>
Per motivi di efficienza sarebbe preferibile inserire l'IP, quando conosciuto o conoscibile, come argomento di fsockopen() al posto del nome dell'HOST.
La prima intestazione deve essere quella che specifica il metodo (ed è anche l'unica davvero obbligatoria per GET), in ogni caso la serie di header deve chiudersi con doppio rn ($br.$br) altrimenti otterremmo sicuramente una risposta d'errore da parte del server.
Anche con le ordinarie funzioni per il filesystem come fopen() e file() possiamo prelevare documenti WEB remoti (l'apertura del socket è incorporata) ma non vi è modo di inviare intestazioni come farebbe un browser.
Osservazioni importanti
Dopo l'esecuzione dello script precedente, esaminando la consueta tabella "HTTP headers Information", ci si accorgerà che:
- possiamo imitare perfettamente la request di un browser; ci siamo persino inventati un nuovo UserAgent ("Socket-PHP-browser 1.0"), e la pagina di provenienza (http://www.bwabwabwa.it).
- Abbiamo anche inviato un'intestazione creata da noi di sana pianta (X-INVENTATO) il cui nome, come richiedono le specifiche HTTP, deve essere preceduto dal prefisso "X-".
- Il protocollo usato per la richiesta è HTTP/1.0 e non la versione più recente perchè, purtroppo, l'API Php per i socket lavora molto male (ed è lentissima) se si effettua una request con HTTP/1.1. Fortunatamente la cosa è abbastanza irrilevante per i nostri scopi.
- Come avevo anticipato anche il server ha restituito degli header, infatti questi precedono il corpo della pagina e se vorremo eliminarli dovremo "trattare" la stringa $dati prima di stamparla con echo() oppure limitarci alla richiesta semplice, senza specificare il protocollo (come nello script "simple_get.php" visto precedentemente)
- Sono state spedite allo script php sul server ("info.php") anche un paio di variabili nella query string, e le ritroviamo prontamente riportate nella tabella "PHP Variables" come _GET["var1"] e _GET["var2"].
- L'header "Host": è Importantissimo, fu introdotto con HTTP/1.1 ma non risulta incompatibile con le request HTTP/1.0. È bene utilizzarlo comunque, infatti si tratta dell'unica informazione in grado di identificare con precisione il sito richiesto, nei casi i cui dietro un unico IP si nascondano più hosts distinti (VirtualHosts).
- Le intestazioni non sono qualcosa su cui fare troppo affidamento per il controllo degli accessi ad un sito, infatti sono facilmente falsificabili.
HEAD
Il metodo HEAD consente di ottenere esclusivamente gli header inviati dal server anzichè l'intero corpo del documento e, dato che la risposta è molto più rapida, diventa utile in operazioni come la verifica dei link interrotti all'interno di un sito.
Per la request è sufficiente un unico header semplicissimo
$header="HEAD /percorso/pag.ext HTTP 1.0 rnrn" ;
POST
Il metodo HTTP GET non è idoneo a spedire grandi quantità di dati: le variabili vengono passate aggiungendole in coda all'URL (...?var1=val1&var2=val2) ma vi è una restrizione relativa alla lunghezza della query string: quest'ultima indicativamente non potrà comprendere più di 1024 caratteri. Al contrario il metodo HTTP POST non è soggetto a tali limitazioni, e i dati sono inseriti nel cosiddetto "corpo" (o body) della request e non tra le intestazioni.
In realtà le request POST e quelle GET non differiscono molto nell'aspetto, ecco header e body prodotti dall'invio di un form HTML come il seguente
Il Form
<form action="http://localhost/info.php" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="user" value="Alì Baba e i 40 ladroni">
<input type="password" name="pass" value="apriti sesamo">
<input type="submit" name="send" value="Invia">
</form>
La Request
La Request corrispondente, con gli elementi fondamentali e tipici del metodo POST evidenziati in grassetto
POST /info.php HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Accept-Encoding: gzip, deflate
Accept-Language: it
Connection: Keep-Alive
Content-Length: 60
Content-Type: application/x-www-form-urlencoded
Host: localhost
Referer: http://localhost/Formpage.html
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
user=Al%EC%20Baba%20e%20i%2040%20ladroni&pass=apriti%20sesamo&send=Invia
Osservazioni sulla request di tipo POST
Content-Type e codifica del body: Application/x-www-form-urlencoded è il formato predefinito per l'invio dei dati (quindi avremo potuto anche non specificarlo nel tag <form....>) ed è lo stesso cui ricorre GET: in base a questa codifica tutti gli spazi e gli altri caratteri non ammessibili in un URL vengono sostituiti con il loro corrispondente esadecimale preceduto dal simbolo "%". Un browser "regolarizza" automaticamente le stringhe, invece nei nostri script che simulano la request dovremo applicare la codifica al contenuto di tutte le variabili non conformi in questo modo:
<?php
$input="nome_var=".htmlentities(rawurlencode("Contenuto non accettabile nell'url")) ;
?>
Per informazioni più dettagliate consiglio di leggere quanto il manuale Php dice a proposito di urlencode(), ovviamente questa trasformazione andrebbe applicata anche agli esempi visti nella prima parte dell'articolo qualora si inseriscano spazi nella query string.
Content-Length. È un parametro obbligatorio, specifica la lunghezza in byte della stringa di input in modo che il server sappia quanti dati dovrà aspettarsi: se inseriamo una cifra troppo piccola l'invio risulterà incompleto, se inseriamo una cifra maggiore rischiamo di mandare in stallo la comunicazione http.
POST con Php
Ecco dunque gli headers con cui sostituire quelli visti nella prima parte di questo articolo
<?php
/*....qui sopra il codice già visto....*/
$post_vars=array('user'=>'Alì Baba e i 40 ladroni','pass'=>'apriti sesamo') ;
$req_body="" ;
foreach($post_vars as $key=>$val){
$req_body.="&".$key."=".rawurlencode(htmlentities($val)) ;
}
$headers="POST ".$target." ".$protocol.$br ;
/*.... tutti gli headers utilizzabili per GET più i seguenti, necessari per POST....*/
$headers.="Content-Type: application/x-www-form-urlencoded".$br ;
$headers.="Content-Length: ".strlen($req_body).$br.$br ;
/*
Inviamo la request aggiungendovi il "body"
*/
fputs($sk,$headers.$req_body) ;
/*prosegue come già visto....*/
?>
Da notare il fatto che in $target (la pagina cui inviare le variabili) può continuare ad essere presente la query string come se stessimo utilizzando il metodo GET, in questo modo abbiamo l'invio contemporaneo di variabili GET e POST.
Content-Type: text/xml
La codifica di default, application/x-www-form-urlencoded, è comoda in quanto consente l'invio di molte variabili fornendo un metodo valido per differenziarle e distinguerle, ma non è la più efficiente per un unico blocco indistinto di dati come avviene nell'ambito dei Webservices. In effetti il protocollo HTTP non consente trasmissioni particolarmente rapide, ed è quindi preferibile adottare una soluzione più semplice quando ciò sia possibile.
La codifica semplice più nota è "text/plain", ma in teoria è accettabile qualsiasi mime-type di tipo "text/*": i dati inviati dal client saranno disponibili lato-server nella variabile $HTTP_RAW_POST_DATA, che non è compresa tra quelle descritte da phpinfo() o quelle reperibili nell'array $_SERVER.
Ecco come viene inviata una request XML da un client per i Webservices:
<?php
/***
SCRIPT xml_post.php
invia la request xml
***/
$host="localhost" ;
/***
Sostituisci con il tuo path assoluto
***/
$target="/php_http/up/xml_receiver.php" ;
$port=80 ;
$timeout=60;
$protocol="HTTP/1.0" ;
$br="rn" ;
$xml_body="<prova attributo="primo attributo">
<commento>Questo è un test</commento>
<contenuto>Invio di una request XML di prova</contenuto>
</prova>" ;
$sk=fsockopen($host,$port,$errnum,$errstr,$timeout) ;
if(!is_resource($sk)){
exit("Connessione fallita: ".$errnum." ".$errstr) ;
}
else{
$headers="POST ".$target." ".$protocol.$br ;
$headers.="Host: ".$host.$br ;
$headers.="Content-Type: text/xml".$br ;
$headers.="Content-Length: ".strlen($xml_body).$br.$br ;
fputs($sk, $headers.$xml_body) ;
$dati="" ;
while (!feof($sk)) {
$dati.= fgets ($sk, 2048);
}
}
fclose($sk) ;
echo($dati) ;
?>
Al posto del solito phpinfo(), una pagina di test che stampa il contenuto di $HTTP_RAW_POST_DATA
<?php
/***
SCRIPT xml_receiver.php
***/
echo("<h2>Response: ho ricevuto i seguenti dati nel formato ".$_SERVER["CONTENT_TYPE"]."</h2>") ;
echo(htmlentities($HTTP_RAW_POST_DATA)) ;
?>
Pagine schermate da un sistema di autenticazione HTTP
Se le pagine che vorremmo raggiungere tramite fsockopen() sono protette da un sistema di autenticazione HTTP, come quello riproducibile con Apache (ad esempio tramite i file .htaccess/.htpasswd) o attraverso la funzione header() di php, allora è sufficiente aggiungere un ulteriore intestazione a quelle già note.
Ecco come autenticarsi inviando i propri username e password
...
...
$headers.='Authorization: Basic '.base64_encode($username.':'.$password).$br
...
...
Conclusione
Gli script che ho presentato sono prevalentemente dimostrativi, hanno lo scopo di illustrare sia la costruzione di una corretta request HTTP, sia il recupero dei risultati emessi dal server:
in questo modo dovrebbe essere chiaro il funzionamento interno delle diverse librerie specifiche per i Webservices, delle quali ci occuperemo molto presto.
Per ogni altro utilizzo sta alla fantasia e alle esigenze di chi legge decidere se costruire una serie di funzioni (o una classe) in grado di gestire le comunicazioni HTTP partendo da questo articolo, oppure se servirsi di strumenti già predisposti come la classe PEAR Http_Request o l'estensione CURL di php. Quest'ultima in particolare è molto performante, anche se purtroppo non consente di manipolare la request in modo così dettagliato quanto il "fai da te" con fsockopen().