Archive for the ‘Multimedia’ Category

Songbird 1.7.3, LastFM, problemi di autenticazione: questione di casing

mercoledì, agosto 11th, 2010

Era qualche settimana ormai che dal mio player preferito Songbird (IMHO una delle applicazioni meglio riuscite e anche più famose sviluppate su piattaforma Mozilla XUL) non riuscivo più ad utilizzare il mio account LastFM tramite il plugin appositamente progettato (ver. 1.0.3.1700).

A prima vista la causa era sicuramente da imputare al meccanismo di autenticazione, infatti il plugin non riusciva in alcun modo a farsi rilasciare il token di sessione per poter attivare i classici servizi di streaming e scrobbling.

Causa lavoro non avevo avuto il tempo di investigare sull’anomalia “parcheggiando” temporaneamente Songbird e tornando ad utilizzare il player standalone ufficiale di LastFM.

Oggi ho ripreso la questione ed analizzando in maniera più approfondita mi sono reso conto che il problema era dovuto banalmente ad una URL errata hardcoded nel codice Javascript del plugin.

Tecnicamente la questione è la seguente:

  1. LastFM ha da poco introdotto una API completamente basata su servizi di tipo REST, compresa la gestione dell’autenticazione;
  2. l’estensione LastFM per Songbird altro non è che un componente software scritto completamente in Javascript che tramite l’infrastruttura messa a disposizione dal core di Mozilla riesce facilmente ad interagire con interfacce di tipo REST. Tutto il codice Javascript in oggetto è contenuto nel seguente file: [user folder]\AppData\Roaming\Songbird2\Profiles\fhkljfmg.default\extensions\audioscrobbler@songbirdnest.com\components\sbLastFm.js;
  3. LastFM ha ultimamente introdotto, per problemi di security, una sorta di meccanismo di autorizzazione per le applicazioni esterne che interagiranno con i servizi REST. Il trusting è completamente gestibile dall’utente che alla prima richiesta di un software “sconosciuto” viene rediretto verso una pagina che permette di concedere o negare l’autorizzazione al software richiedente.

Tutto il meccanismo fin qui descritto è implementato dalla funzione:

   1: // authenticate against the new Last.fm "rest" web service APIs

   2: sbLastFm.prototype.apiAuth = function sbLastFm_apiAuth(onSuccess, onFailure) {

   3:   // clear our old session

   4:   this.sk = null;

   5:   Application.prefs.setValue("extensions.lastfm.session_key", "");

   6:  

   7:   // clear any web cookies we may have already

   8:   dump("web cookies cleared\n");

   9:   var cookieMgr = Cc["@mozilla.org/cookiemanager;1"]

  10:         .getService(Ci.nsICookieManager);

  11:   cookieMgr.remove(".last.fm", "Session", "/", false);

  12:   cookieMgr.remove(".last.fm", "s_cc", "/", false);

  13:   cookieMgr.remove(".last.fm", "s_sq", "/", false);

  14:   cookieMgr.remove(".last.fm", "wwwlang", "/", false);

  15:   cookieMgr.remove(".last.fm", "__qcb", "/", false);

  16:   cookieMgr.remove(".last.fm", "TREA", "/", false);

  17:   cookieMgr.remove(".last.fm", "s_nr", "/", false);

  18:   cookieMgr.remove(".last.fm", "__qca", "/", false);

  19:   cookieMgr.remove(".last.fm", "s_lastvisit", "/", false);

  20:   cookieMgr.remove(".last.fm", "LastUser", "/", false);

  21:   cookieMgr.remove(".last.fm", "AnonTrack", "/", false);

  22:   cookieMgr.remove(".last.fm", "fastq", "/", false);

  23:  

  24:   // get a lastfm desktop session

  25:   var self = this;

  26:     this.webLogin(function success() {

  27:         dump("webLogin SUCCESS\n");

  28:  

  29:     self.login_phase = AUTH_PHASE_TOKEN_REQUEST;

  30:     self._token_xhr = self.apiCall('auth.getToken', { },

  31:       function response(success, xml, xmlText) {

  32:         if (!success) {

  33:           dump("auth.getToken: FAILED TO AUTHENTICATE: " + xmlText + "\n\n");

  34:           return;

  35:         }

  36:  

  37:         var authtoken = xml.getElementsByTagName('token');

  38:         if (authtoken.length != 1) {

  39:           dump("auth.getToken: FAILED TO FIND TOKEN: " + xmlText + "\n\n");

  40:           return;

  41:         }

  42:         authtoken = authtoken[0].textContent;

  43:         dump("auth.getToken SUCCESS: " + authtoken + "\n");

  44:  

  45:         var window = Cc['@mozilla.org/appshell/window-mediator;1']

  46:                        .getService(Ci.nsIWindowMediator)

  47:                        .getMostRecentWindow('Songbird:Main');

  48:         if (!window) {

  49:           self.listeners.each(function(listener) {

  50:             listener.onLoginFailed();

  51:           });

  52:           return;

  53:         }

  54:         var gBrowser = window.gBrowser;

  55:  

  56:         function removeAuthListeners() {

  57:           gBrowser.removeEventListener("DOMContentLoaded",

  58:                                        self._authListener, false);

  59:           gBrowser.removeEventListener("unload", removeAuthListeners, false);

  60:           authTab.removeEventListener("TabClose",

  61:                                       self._authTabCloseListener, false);

  62:         }

  63:  

  64:         // Create a listener for last.fm's authorization grant page.

  65:         self._authListener = function (e) {

  66:  

  67:           // Ensure we are on the right tab.

  68:           if (gBrowser.getBrowserForDocument(e.target) !=

  69:               gBrowser.getBrowserForTab(authTab)) {

  70:             return;

  71:           }

  72:   

  73:           // We're listening for the LastFM "Permissions Granted" page. It will

  74:           // have pathname "/api/grantAccess" on the last.fm domain or a

  75:           // localized version such as lastfm.fr

  76:           var loc = e.target.location;

  77:           if (!/last\.?fm/.test(loc.host)) {

  78:             // If we get here, it implies that the user navigated away from

  79:             // LastFM without authorizing.

  80:             removeAuthListeners();

  81:             self.listeners.each(function(listener) {

  82:               listener.onLoginFailed();

  83:             });

  84:             return;

  85:           }

  86:  

  87:           if (loc.pathname != "/api/grantaccess") {

  88:             // Ignore LastFM pages that aren't the "Permissions Granted" page.

  89:             return;

  90:           } 

  91:  

  92:           // We should be on the grantAccess page now, so remove the listeners

  93:           // and try to grab a session key.

  94:           removeAuthListeners();

  95:  

  96:           self.login_phase = AUTH_PHASE_SESSION_REQUEST;

  97:           self._session_xhr = self.apiCall('auth.getSession', {

  98:               token: authtoken

  99:             },

 100:             function response(success, xml, xmlText) {

 101:               if (!success) {

 102:                 dump("auth.getSession: FAILED TO AUTHENTICATE: " +

 103:                   xmlText + "\n\n");

 104:                 return;

 105:               }

 106:               var keys = xml.getElementsByTagName("key");

 107:               if (keys.length != 1) {

 108:                 dump("auth.getSession: FAILED TO AUTH. TOKEN: " +

 109:                   xmlText + "\n\n");

 110:                 return;

 111:               }

 112:               self.sk = keys[0].textContent;

 113:               dump("auth.getSession: AUTHENTICATED\n");

 114:               dump("session key: " + self.sk + "\n");

 115:               Application.prefs.setValue('extensions.lastfm.session_key',

 116:                 self.sk);

 117:               var subscribers = xml.getElementsByTagName("subscriber");

 118:               if (subscribers.length == 1)

 119:                 self._subscriber = (subscribers[0].textContent == "1");

 120:               if (Application.prefs.getValue(

 121:                     "extensions.lastfm.subscriber_override", false))

 122:                 self._subscriber = true;

 123:               dump("subscriber: " + self._subscriber + "\n");

 124:               self.listeners.each(function(l) {

 125:                 l.onAuthorisationSuccess();

 126:               });

 127:  

 128:               if (typeof(onSuccess) == "function")

 129:                 onSuccess();

 130:           });

 131:         } 

 132:  

 133:         // Load the user authorization page.

 134:         var authURL = "http://" + self.geoBaseDomain + "/api/auth?api_key=" +

 135:                       API_KEY + "&token=" + authtoken;

 136:  

 137:         gBrowser.addEventListener("DOMContentLoaded", self._authListener, false);

 138:         // Make sure we don't leak the listeners if the user takes no action.

 139:         gBrowser.addEventListener("unload", removeAuthListeners, false); 

 140:  

 141:         var authTab = gBrowser.loadOneTab(authURL, null, null, null, false);

 142:       

 143:         // The user could close the auth page tab without granting permission.

 144:         self._authTabCloseListener = function(e) {

 145:           removeAuthListeners();

 146:           self.listeners.each(function(listener) {

 147:             listener.onLoginFailed();

 148:           });

 149:         }

 150:         

 151:         authTab.addEventListener("TabClose", self._authTabCloseListener, false);

 152:  

 153:     }, function failure() {   // auth.getToken failure

 154:       dump("webLogin FAILED\n");

 155:       self.listeners.each(function(listener) {

 156:         listener.onLoginFailed();

 157:       });

 158:     }); // auth.getToken api call

 159:   }, function() {

 160:     dump("weblogin FAILURE\n");

 161:     self.listeners.each(function(listener) {

 162:       listener.onLoginFailed();

 163:     });

 164:   }); // weblogin

 165: }

ed in particolare il punto 3 viene gestito tramite la funzione di callback ad evento settata alla riga 65.

Dalla riga 87 alla 90, nella funzione di callback sopra citata, viene fatto un controllo al fine di ignorare tutte le pagine-URL diverse da quella che LastFM usa per permettere all’utente di settare la grant per la propria “external application”:

   1: if (loc.pathname != "/api/grantAccess") {

   2:             // Ignore LastFM pages that aren't the "Permissions Granted" page.

   3:             return;

   4:} 

Siccome avevo notato che la funzione di callback usciva sempre in quel punto ho provato via browser la URL testata su loc.pathname per controllare se restituisse correttamente un status HTTP 200: ebbene ho scoperto che il motore di routing-rewriting usato da LastFM essendo case sensitive alla URL http://www.lastfm.it/api/grantAccess mi restituiva un bel HTTP 404 (pagina non trovata) mentre puntando ad http://www.lastfm.it/api/grantaccess ritornava correttamente la pagina di trusting applicativo.

Alla luce della scoperta mi è bastato sostituire “api/grantAccess” con “api/grantaccess” (riga 1399 del file sbLastFm.js) che il mio Songbird ha ripreso a “dialogare allegramente” con lo strepitoso servizio di LastFM.

Successivamente, facendo qualche ricerca su Google, ho scoperto che il problema era già stato fixato sul trunk del plugin per la versione 1.8.0 di Songbird (attualmente in beta 3 e quindi non ancora disponibile dalla pagina di download principale http://www.getsongbird.com)

… insomma questione di casing

Technorati Tags: ,,

Tags: , , , ,
Posted in Browsers, Javascript, Multimedia, Software | 1 Comment »