AlcaLabs

[[articles:bosh_strophe]]

Traccia: » Bosh, Strophe e compagni di merende

Ti trovi qui: Alca Labs » articles » Bosh, Strophe e compagni di merende

Login

Non sei ancora collegato! Inserisci il tuo nome utente e la tua password per autenticarti. E' necessario che il tuo browser abbia i cookie abilitati.

Entra

Hai dimenticato la password? Richiedine una nuova: Invia nuova password

Bosh, Strophe e compagni di merende

Un'introduzione facile e veloce alla comunicazione persistente su HTTP orientata ad XMPP (con un tocco di servizi Publish-Subscribe).

BOSH

BOSH, ovvero Bidirectional-streams Over Synchronous HTTP, è un protocollo di trasporto che implementa dei flussi di dati bidirezionali tra due entità (client e server), utilizzando delle coppie sincrone di rischieste/risposte HTTP, senza l'utilizzo di meccanismi di polling.

Il protocollo è stato introdotto dalla XMPP Standard Foundation con l'intento di estendere la fruibilità del protocollo XMPP anche ad applicazioni implementate all'interno di semplici pagine web, scritte in HTML e javascript.

Dal punto di vista tecnico, BOSH rientra nella categoria delle tecniche “PUSH”. In particolare nel nostro caso si tratta di HTTP Server PUSH, ovvero un meccanismo che consenta ad un web server di inviare dati ad un client (un browser) registrato, senza che quest'ultimo ne abbia fatto una richiesta esplicita.

La nostra avventurà sarà essenzialmente incentrata sull'utilizzo di XMPP attraverso BOSH, pertanto avremo bisogno di un server XMPP che supporti tale protocollo; magicamente, la provvidenza ha già provveduto a questo nostro bisogno, infatti la comunità Open Source ha realizzato Ejabberd. Non ci addentreremo pesantemente all'interno della configurazione di questo servizio e ci concentreremo principalmente sugli aspetti necessari a far funzionare BOSH.

Ejabberd

Ejabberd è stato installato dal repository apt di una ubuntu jaunty, che ci offriva la versione 2.0.3, sufficiente per realizzare il nostro progetto. Dopo l'installazione abbiamo configurato in /etc/ejabberd/ejabberd.cfgil nome host secondo il nostro bisogno (yatest) e creato un utente amministrativo:

fragment configurazione host

{hosts, ["yatest", "localhost"]}.

comando impostazione utente

ejabberdctl register USERNAME yatest PASSWORD

fragment configurazione amministratore

{acl, admin, {user, USERNAME, "yatest"}}.

(dove USERNAME nel nostro caso è “alcadm”). Fatto ciò, è possibile provare ad utilizzare il server con un qualsiasi client jabber (pigin, psi, empathy, ecc…).

Per quanto riguarda BOSH, è possibile utilizzare un modulo aggiuntivo di ejabberd che aggiunge tale funzionalità, mod_http_bind. Per configurarlo è necessario modificare il file di configurazione in due punti, in maniera tale che si istruisca ejabberd a caricare il modulo e ad accettare richieste sulla porta dedicata (5280 di default):

abilitare il modulo

...
{modules,
 [
...
  {mod_http_bind, []},
..
 ]
}.
...

abilitare l'ascolto

...
{listen,
 [
...
  {5280, ejabberd_http, [
			 http_poll,
			 web_admin,
			 http_bind
			]},
...
 ]}.
...

Riavviando il servizio, nel nostro caso BOSH sarà disponibile all'indirizzo http://yatest:5280/http-bind.

Strophe

Ora che abbiamo sistemato il server, dobbiamo essere in grado di comunicare con lui. Le librerie che ci consentono di farlo sono tante e sono implementate in vari linguaggi; per i nostri esempi utilizzeremo Strophe, una libreria disponibile sia in C, sia in javascript, in particolare utilizzeremo questa seconda versione, che faremo girare all'interno di semplice pagina HTML.

Strophe.js è una libreria piuttosto semplice e ben progettata. La classe principale è Strophe.Connection, il cui costruttore vuole come parametro l'url del servizio BOSH. Qui è necessaria una piccola digressione per risolvere un piccolo problema. Strophe utilizza oggetti XMLHttpRequest per gestire le connessioni con il servizio BOSH, pertanto c'è una limitazione circa l'indirizzo di tale servizio (deve avere la stessa forma dell'indirizzo della pagina che ha lanciato la richiesta). Per superare questa difficolta è possibile far servire la pagina HTML ad un web server locale che configureremo in maniera tale che rigiri le richieste verso /http-bind ad http://yatest:5280/http-bind. Ad esempio, nel caso di Apache2 aggiungeremo queste direttive nel file del virtualhost in uso:

<Proxy *>
   Order Deny,Allow
   Allow from all
</Proxy>

ProxyPass /http-bind http://yatest:5280/http-bind
ProxyPassReverse /http-bind http://yatest:5280/http-bind

AGGIORNAMENTO: L'autore di Strophe ha scritto sul suo blog un'interessante notizia circa la possibilità di far funzionare la libreria in maniera cross-domain, applicando una semplice patch ai sorgenti di ejabberd (versione 2.1.2). I test effettuati hanno dato esito positivo, Strophe funziona collegandosi senza problemi all'url BOSH esposta da ejabberd.

Ritorniamo a Strophe. Come funziona? Per rendere breve una lunga storia, diciamo che, dato un id jabber/XMPP valido e la relativa password, Strophe apre una connessione bidirezionale verso l'url BOSH e rimane in ascolto in attesa di messaggi, rendendo disponibile allo stesso tempo un meccanismo per l'invio di messaggi allo stesso servizio. Comunque sia, è arrivato il momento di smetterla con tutte queste parole e scrivere codice. Prima di tutto dobbiamo aver scaricato già Strophe, successivamente spostiamoci in una directory esposta dal nostro web server locale e realizziamo una pagina HTML semplice semplice, che contiene solo il tag html per caricare il file javascript:

<html>
<head>
<!-- abbiamo spostato la directory di strophe nella stessa directory in cui abbiamo salvato la pagina html -->
<script type="text/javascript" src="strophe/strophe.js"></script>
</head>
<body></body>
</html>

Fatto ciò, facciamo partire il nostro Firefox e premiamo F12 per far partire FireBug. Nel pannello che si aprirà, abilitiamo la console e così potremo divertirci a provare il codice javascript che segue, senza preoccuparci troppo.

// Creiamo l'oggetto Strophe Connection
var bosh = new Strophe.Connection('/http-bind');
// effettuiamo il login su ejabberd
bosh.connect('user@yatest', 'password', function () { return true; });

In questo modo, se tutto è andato bene, saremo connessi e pronti per chattare con qualche amico… anche se in maniera piuttosto spartana. Il codice è semplice, tuttavia il terzo parametro passato al metodo connect richiede qualche spiegazione. In sostanza, connect è più contento se gli si passa una funzione per gestire il login; a questa funzione viene passato lo stato della connessione, quindi è utile per effettuare alcune azioni in occasione di determinate situazioni: ad esempio, potremmo avvisare l'utente che è stato disconnesso dal servizio ed eventualmente riconnetterlo automaticamente.

Una volta effettuato il login, possiamo notificare al server lo stato della nostra presenza, in maniera tale che i nostri amici ci vedano disponibili, away, ecc.. Per farlo è necessario inviare un messaggio xml al server ejabberd. A questo punto qualcuno potrebbe già aver messo le mani nei capelli, visto che xml non piace a tutti. Nessun problema, Strophe ci viene incontro anche in questo, offrendoci un costruttore di messaggi xml:

bosh.send($pres().tree());

In una sola linea, abbiamo creato il messaggio xml per mostrare la nostra presenza (di default è “disponibile”) con $pres().tree() e lo abbiamo inviato con il metodo send().

A questo punto, possiamo anche inviare un messaggio di chat ad un nostro amico che sappiamo essere online; anche in questo caso si tratta di inviare al server un messaggio xml e nuovamente abbiamo un helper per farlo comodamente:

bosh.send($msg({'to': 'amico@yatest', 
		'type': 'chat'}
	      ).c('body').t('Ciao amico!'));

Ed ora? Se il nostro amico ci rispondesse? E' bene che gli si dica di aspettare 5 minuti, il tempo necessario per spiegare come si fa ad intercettare i messaggi di chat:

bosh.send($msg({'to': 'amico@yatest', 
		'type': 'chat'}
	      ).c('body').t('Aspetta 5 minuti prima di rispondermi'));

Ogni dato inviato dal server, che sia un messaggio di chat o un messaggio di controllo (una stanza in generale), scatena in Strophe la generazione di un evento al quale può essere associato un handler per gestire la situazione. Faremo proprio questo, anche se in maniera piuttosto elementare.

bosh.addHandler(function (msg) {alert(msg.getElementsByTagName('body'));},
			 null,
			 'message',
			 'chat',
			 null,
                         null);

In questo modo, ogni volta che Strophe intercetterà una stanza “message” di tipo &ldquo;chat&rdquo; richiamerà il codice che gli abbiamo fornito, quindi lancerà un alert che conterrà il testo del body del messaggio. Notiamo in ultimo che all'handler viene passato come parametro il codice xml del messaggio ricevuto e quindi può essere manipolato con i normali metodi messi a disposizione da javascript per questo linguaggio.

Un client XMPP fatto in casa

Bene, ora abbiamo capito spannometricamente come funziona Strophe, quindi è giunto il momento di incollare tutti i pezzi e fare qualcosa di più maturo. L'idea è quella di fare un piccolo client su pagina HTML che ci consenta di comunicare con il nostro amico. La pagina conterrà un'area in cui saranno scritti i messaggi, un campo in cui scrivere i messaggi ed un bottone per inviarli.

<div id="box" style="width: 500; border: 1px solid red; height: 500px;overflow: auto;">
</div>
 
<div>
   <input id="text" name="text" style="width: 200px"/>
   <input name="sendbtn" value="INVIA" type="button" />
</div>

Sul fronte del codice di “logica”, invece, realizzeremo un oggetto javascript che raggrupperà ed estenderà quanto abbiamo già visto poc'anzi (il tutto sarà salvato nel file xmpp-test.js).

var XmppAlca = {
 
    // Proprietà in cui salveremo l'oggetto Strophe.Connection
    bosh: null,
 
    // Gestiamo la connessione in maniera semplice
    start: function (jid, pass) {
	XmppAlca.bosh    = new Strophe.Connection(BOSH_URL);
 
	XmppAlca.bosh.connect(jid, pass, XmppAlca.connect_callback);
    },
 
    // Perché bisogna essere sempre bravi ragazzi...
    disconnect: function () {
	XmppAlca.bosh.disconnect();
    },
 
    // Inviamo il messaggio 
    send_message: function (to, msg) {
	XmppAlca.send($msg({'to': to,
			    'type': 'chat'}
			  ).c('body'
			     ).t(msg));
    },
 
    // Handler per la ricezione del messaggio
    recv_message: function (msg) {
	var from = msg.getAttribute('from');
	var type = msg.getAttribute('type');
	var text = msg.getElementsByTagName('body');
 
	if ( type == 'chat' && text.length > 0 )
	    XmppAlca.write_html_message(from, text[0].textContent);
 
	return true;
    },
 
   // Utility per scrivere in cima
    append_on_top: function (text) {
	var msgbox = document.getElementById(HTML_ID);
	var old    = msgbox.innerHTML;
 
	msgbox.innerHTML = text + old;
    },
 
    // Utility per scrivere i messaggi in maniera piacevole
    write_html_message: function (who, msg) {
	var color  = (who == 'me') ? 'navy' : 'red';
	var text  =  '<span style="color: '+color+'">'+who+': '+msg+'</span><br />';
 
	XmppAlca.append_on_top(text);
    },
 
    // Metodo essenziale per un vero hacker
    send: function (data) {
	XmppAlca.bosh.send(data);
    },
 
    // Callback per la connect (con gestione dello stato)
    connect_callback: function (status) {	
	switch (status) {
	case Strophe.Status.ERROR :
	    XmppAlca.append_on_top("Errore fatale<br />");
	    break;
	case Strophe.Status.CONNFAIL :
	    XmppAlca.append_on_top("Errore di connessione<br />");
	    break;
	case Strophe.Status.AUTHFAIL :
	    XmppAlca.append_on_top("Controlla jid/password<br />");
	    break;
	case Strophe.Status.CONNECTED :
	    // Siamo connessi, perciò inviamo la nostra presenza
	    XmppAlca.send($pres().tree());
	    // registriamo anche il nostro handler
	    XmppAlca.bosh.addHandler(XmppAlca.recv_message, null, 'message', null, null, null);
	    // ed inviamo un messaggio di cortesia
	    XmppAlca.append_on_top("Connesso<br />");
	    break;
	case Strophe.Status.DISCONNECTED :
	    XmppAlca.append_on_top("Disconnesso<br />");
	    break;
	case Strophe.Status.DISCONNECTING :
	    XmppAlca.append_on_top("Disconnessione in corso<br />");
	    break;
	}
    }
}

Ovviamente, dovremo rivedere un po' il codice della nostra pagina, in maniera tale da rendere il tutto più gradevole.

<html>
  <head>
    <title>test strophe</title>
    <script src="strophe/strophe.js" type="text/javascript"></script>
    <script src="xmpp-test.js" type="text/javascript"></script>
 
    <script type="text/javascript">
      // Un po' di variabili per inizializzare il nostro codice
      var BOSH_URL = '/http-bind';
      var HTML_ID  = 'msgbox'
      var my_jid   = 'user@yatest';
      var my_pass  = 'password';
      var friend   = 'amico@yatest';
 
      var sendMsg = function (text) {
          XmppAlca.write_html_message('me', text);
          XmppAlca.send_message(friend, text);
          document.getElementById('text').value = '';
      };
 
 
      XmppAlca.start(my_jid, my_pass);
    </script>
 
 
</head>
 
 
<body>
 
<div id="msgbox" style="width: 800px; border: 1px solid red; height: 500px; overflow: auto;">
</div>
 
<div style="padding-top: 4px;">
   <input id="text" name="text" style="width: 730px" />
   <input name="sendbtn" value="INVIA" type="button" style="width: 66px"
    onClick="sendMsg(document.getElementById('text').value)" />
</div>
 
<input name="discbtn" value="Disconnetti" type="button"
   onClick="XmppAlca.disconnect()" style="position: absolute; top: 4px; right: 4px;"/>
 
</body>
</html>

A questo punto siamo pronti per partire, lanciamo tutto con Firefox ed incrociamo le dita; se tutto è andato bene, dovremmo essere connessi ed il nostro amico dovrebbe essere in grado di vedere la nostra presenza. Realizzare questo client è stato semplicissimo, ma nella sua semplicità può funzionare con qualsiasi chat XMPP che offra un URL BOSH.

Perdersi nei meandri di XMPP

Strope è una libreria piuttosto generica e per questo motivo ci da la possibilità di utilizzarla in maniera molto versatile. Ad esempio, grazie alla sua architettura, possiamo realizzare un consumer di messaggi per servizi Publish-Subscribe (Pub-Sub). Sebbene possa apparire un'idea bislacca e campata in aria, un client di questo tipo è molto utile per avere pagine HTML che si aggiornino in “tempo reale”; questo è il caso, ad esempio, di un widget per mostrare contenuti, del tipo utilizzato per i feed RSS.

Immaginiamo di seguire un blog o un giornale elettronico, per il quale vorremmo essere sempre informati circa la pubblicazione di nuovi contenuti, una strada è quella di sottoscrivere il feed RSS all'interno di un aggregatore, che periodicamente (ad esempio ogni 10 minuti) controllerà tutti i feed e ci mostrerà quelli che presentano aggiornamenti. Un'altra ed interessante possibilità è quella di sfruttare il meccanismo di Pub-Sub di XMPP, pertanto il blog (o il giornale elettronico) potrebbe associare alla creazione di un nuovo contenuto, l'invio di un messaggio di “publishing” ad un determinato nodo pub-sub per cui è un utente publisher; in questo modo, tutti i sottoscrittori di quel nodo riceverebbero istantaneamente un messaggio con il nuovo contenuto.

Vediamo come realizzare un meccanismo di questo genere con Strophe, che ci offre già di suo un plugin per maneggiare i servizi Pub-Sub. Nonostante ciò, con l'intento di essere didattici, realizzeremo una semplice e parziale implementazione all'interno del codice che abbiamo già scritto e per essere più precisi realizzeremo dei metodi per consentire di iscriversi ad un nodo Pub-Sub, cancellare la propria iscrizione, pubblicare contenuti nel nodo e ricevere gli stessi.

Prima di tutto, è necessario configurare ejabberd affinché utilizzi il modulo per il servizio Pub-Sub:

...
{access, pubsub_createnode, [{allow, all}]}.
...
{modules,
 [
...
  {mod_caps,     []},
  {mod_pubsub,   [
		   {access_createnode, pubsub_createnode},
		   {plugins, ["default", "pep"]}
		  ]},
...
 ]
}.
...

L'implementazione consisterà nella realizzazione del codice necessario ad inviare e ricevere le opportune stanze di tipo iq e presentarle nella nostra pagina. Anche in questo caso è giunto il momento di dire “basta con le parole” e di partire con il codice.

// Tutto il codice è da considerarsi estratto da XmppAlca
 
    // indirizzo del servizio Pub-Sub
    pubsub: 'pubsub.yatest',
 
    // Un altro metodo per veri hacker
    sendIQ: function (data, ok_back, ko_back) {
	XmppAlca.bosh.sendIQ(data, ok_back, ko_back);
    },
 
    // Metodo per iscriversi ad un nodo Pub-Sub
    subscribe_to_pubsub: function (node) {
	var stanza = $iq({'type':'set',
			  'from': XmppAlca.bosh.jid,
			  'to': XmppAlca.pubsub,
			  'id': XmppAlca.bosh.getUniqueId('sub')}
			).c('pubsub', 
			    {'xmlns': 'http://jabber.org/protocol/pubsub'}
			   ).c('subscribe',
			       {'node': node,
				'jid': Strophe.getBareJidFromJid(XmppAlca.bosh.jid) 
			       });
 
	XmppAlca.sendIQ(stanza,
			function (data) {
			    var ok   = "Iscrizione a "+node+" avvenuta con successo<br />";
			    var type = data.getAttribute('type');
 
			    if (type == 'result')
				XmppAlca.append_on_top(ok);
			},
			function () {
			    var ko = "Impossibile iscriversi a "+node+"<br />";
 
			    XmppAlca.append_on_top(ko)
			});
    },
 
    // Metodo per rimuovere la propria iscrizione
    unsubscribe_from_pubsub: function (node){
 
	var stanza = $iq({'type':'set',
			  'from': XmppAlca.bosh.jid,
			  'to': XmppAlca.pubsub,
			  'id': XmppAlca.bosh.getUniqueId('unsub')}
			).c('pubsub', 
			    {'xmlns': 'http://jabber.org/protocol/pubsub'}
			   ).c('unsubscribe',
			       {'node': node,
				'jid': Strophe.getBareJidFromJid(XmppAlca.bosh.jid) 
			       });
 
	XmppAlca.sendIQ(stanza, 
			function (data) {
			    var ok   = "Iscrizione a "+node+" rimossa con successo<br />";
			    var type = data.getAttribute('type');
 
			    if (type == 'result')
				XmppAlca.append_on_top(ok);
			},
			function () {
			    var ko = "Impossibile rimuovere l'iscrizione a "+node+"<br />";
 
			    XmppAlca.append_on_top(ko)
			});
    },
 
    // Metodo per pubblicare un contenuto (testuale) su un nodo pubsub
    publish_to_pubsub: function (node, text) {
	var stanza = $iq({'type':'set',
			  'from': XmppAlca.bosh.jid,
			  'to': XmppAlca.pubsub,
			  'id': XmppAlca.bosh.getUniqueId('pub')}
			).c('pubsub', 
			    {'xmlns': 'http://jabber.org/protocol/pubsub'}
			   ).c('publish',
			       {'node': node}
			      ).c('item').c('text').t(text);
 
	XmppAlca.sendIQ(stanza, 
			function (data) {
			    var ok   = "Contenuto pubblicato su "+node+"<br />";
			    var type = data.getAttribute('type');
 
			    if (type == 'result')
				XmppAlca.append_on_top(ok);
			},
			function () {
			    var ko = "Impossibile pubblicare il contenuto su "+node+"<br />";
 
			    XmppAlca.append_on_top(ko)
			});
    },
 
    // Riscriviamo l'handler per la ricezione del messaggio
    recv_message: function (msg) {
	var from     = msg.getAttribute('from');
	var type     = msg.getAttribute('type');
	var messages = [];
 
	if ( from.match(/pubsub/i) ) {
	    from = msg.getElementsByTagName('items')[0].getAttribute('node');
	    var items = msg.getElementsByTagName('text');
	    console.dir(items)
 
	    for (var i=0; i<items.length; i++)
		messages[i] = Strophe.getText(items[i]);
	} else if ( type == 'chat' )
	    messages[0] = Strophe.getText(msg.getElementsByTagName('body')[0]);
	// non vogliamo gestire altri messaggi
 
	for (var i=0; i<messages.length; i++)
	    if (messages[i].length > 0)
		XmppAlca.write_html_message(from, messages[i]);
 
	return true;
    },

Immaginiamo che il blog utilizzi il metodo XmppAlca.publish_to_pubsub() per inviare un estratto di un post appena pubblicato al nodo (precedentemente creato) /home/yatest/blog/post; a questo punto, se utilizziamo il metodo XmppAlca.subscribe_to_pubsub() per iscriverci a tale nodo, avremo la possibilità di ricevere tali estratti in tempo reale.

Conclusioni

Abbiamo visto come Strophe ci consenta di interagire semplicemente con XMPP attraverso BOSH per realizzare un semplice client per chattare con un amico e/o utilizzare un servizio Pub-Sub. I limiti di utilizzo sono semplicemente quelli dettati dalla nostra fantasia e vanno anche oltre i normali scopi per cui tutto ciò è nato; ad esempio, potremmo utilizzare questi strumenti per far dialogare in maniera affidabile e completa dei servizi che diversamente non avrebbero modo di “parlarsi”.

Un interessante direttrice di sviluppo è quella di integrare Strophe con JQuery; stando a quanto racconta l'autore, la nostra adorata libreria xmpp è stata progettata per essere totalmente compatibile con tale framework.

Bene, ora è il vostro turno. Divertitevi.

Riferimenti

Autore

Domenico Delle Side <domenico.dellesideATalcacoop.it>