photo of devloop

devloop :: blog

sécurité informatique, programmation, Linux et plus encore

Subscribe to RSS feed

Posts tagged with "anywhere.fm"

Jouons avec l'API de IMEEM.com

, , , ...

Introduction
Il y a quelques temps, je m'étonnais que mon script permettant de lire ma musique depuis Anywhere.FM ne fonctionnait plus, pensant qu'il sagissait de changements dans l'API du site.

En réalité la cause s'est avérée être le rachat de Anywhere.FM par le site "musico-social" Imeem.com.
Lors de ma dernière connexion sur l'ancien site, on m'a demandé obligé d'effectuer la transition des musiques vers Imeem.com. Une barre de status en Flash (bien évidemment) semblait affirmer que la migration se déroulait parfaitement... seulement leur outil m'a perdu les 9/10ème de ma collection furious
Déjà que les anciens scripts n'ont plus d'utilité, je l'ai senti passer (il m'avait bien fallu un mois pour tout uploader) sad

Décu mais pas désespéré pour autant, j'ai profité du fait qu'une vrai API soit disponible pour Imeem.com (contrairement à Anywhere.FM que j'avais du étudier moi-même sous toutes les coutures) pour coder un petit programme d'upload et peut-être à venir un nouveau player smile

L'API
Les bons points de l'API d'Imeem, c'est que l'on est pas obligé de manipuler cet affreux AMF propriétaire smile On peut communiquer au format XML ou Json pour lequel j'ai opté.

Communiquer avec les serveurs s'avère beaucoup plus simple, en revanche différentes protections sont présentes, comme la nécessité de disposer d'une clé d'API fournie par le site et de la communiquer à chaque appel de fonction. Anywhere.FM n'était aucunement documenté mais aucune protection n'était vraiment présente.
Les méthodes proposées par l'API donnent un goût de déjà vu : on se connecte (usersLogin), on récupère son ID (usersGetCurrentUser), on demande les IDs de nos listes de lecture (playlistsGetByUser), on en extrait les IDs des musiques (mediaGetByPlaylist) et on obtient au final une adresse où lire le titre (mediaGetInfo).

Le problème c'est que ces adresses sont destinées à être lues depuis un navigateur car il ne s'agit pas de lien direct vers un flux. Sans compter que ces urls permettent de lire le titre seul et qu'à côté de ça le Flash permettant de jouer toute une playlist ne fonctionne pas (les joies de Flash...)
Enfin bref, le site est à chier et plus vite je pourrais communiquer directement avec les serveurs mieux ce sera bigsmile

La lecture des flux
Seulement la lecture des flux risquent de poser problème.
D'abord ce n'est plus de mp3 mais du flv (beurk). Ensuite ça passe par une méthode non documentée (ce serait trop beau) nommée "mediaGetStreamInfo" qui prend comme argument :
  • key : l'ID du média concerné
  • forceSample : un booléen (mettre à false)
  • isEmbed : booléen (false)
  • isFeatured : booléen (false)
  • methodVersion : nombre (à fixer à 2)
  • referrer : chaine, à fixer à "web"
  • supportsHD : booléen (probablement selon vos goûts)

Cette merveilleuse fonction recrache trois paramêtres :
  • h : le nom du serveur où se trouve le flv (par exemple "srv0202-01.sjc3.imeem.com")
  • p : un path qui semble fixe à chaque média (ex: "/g/m/5352700b33c7347ae3342390187a7fa4.flv") mais inexploitable directement
  • ep : une chaine de cracatère très longue qui fait penser à du base64

Ces trois paramêtres sont traitées dans notre navigateur par l'animation Flash du site (audio_player3.swf) qui nous recrache une URL valide vers un flv.
Il faudrait donc parvenir à extraire l'Action Script du Flash pour trouver l'algorithme permettant de récupérer une adresse de flux valide.
Si vous en avez le courage, toute aide sera la bienvenue ;-)

L'upload
Pour ce qui est de l'upload, le problème est réglé. L'API permet deux possibilitées, les temps passent par la page http://upload.imeem.com/apiupload.aspx :
  • Upload depuis une URL : on envoit des données en spécifiant où une des machines de Imeem doit aller chercher le fichier mp3
  • Upload par HTTP Post : un upload "classique" où il fait bien prendre soin au formatage des données.

Dans les deux cas, la page répond (si la requête est bien formatée) par un XML qui donne le code d'erreur correspondant (0 en cas de succès).
Certains auront déjà pensé aux possibilités offertent par la première méthode (transformer le serveur d'Imeem en outil de scan de ports etc p ) mais ce n'est pas le sujet de l'article.

J'ai implémenté les deux méthodes d'upload, la méthode traditionnelle me semble un peu plus rapide. J'ai développé un outil en ligne de commande qui prend un répertoire en argument et upload tous les mp3s trouvés dans l'arborescence smile
Dans cette archive vous trouverez ma petite implémentation de l'API d'Imeem en Python, une librairie pour l'upload de fichier en HTTP (trouvée sur VoidSpace, légérement retouchée), l'uploader en ligne de commande ainsi qu'une capture au format pcap qui montre comment on arrive à la lecture d'un flux FLV en partant d'une adresse récupérée par mediaGetInfo (reste à savoir comment l'adresse du flux est calculée côté client).

Anywhere.FM Python CLI Player

, , , ...

C'est après pas mal de recherches et de tests de code que je suis finalement parvenu à développer en Python un lecteur de ma musique présente sur le site Anywhere.FM smile

Le dernier bug rencontré était, comme expliqué dans un précédent, le décodage de certaines chaines de caractères qui ne respectaient pas le format UTF-8. Le responsable était finalement le site lui-même ou plutôt les utilisateurs qui envoit des données au serveur à l'aide d'un navigateur utilisant un autre encodage.

Ainsi dans mon cas, le décodage de mon profil échouait car dans les données retournées se trouvait entre autres le nom mal encodé Ted Gärdestad correspondant à l'un des artistes les plus écoutés par une personne dans ma liste d'amis (la fonctione RPC get_complete_profiles du site renvoie beaucoup d'infos superflues :-| )

Pour résoudre ce souci il fallait intervenir sur la méthode readString de la classe Decoder de PyAMF (module amf3).
Comme je ne voulais pas et d'ailleurs ne pouvais pas modifier le code de PyAMF (installé sous forme de "egg" sur mon système), j'ai plutôt imaginé une technique de "Python API Hooking".
En fait il s'agit simplement de la redéfinition puis d'un écrasement d'une méthode d'une classe Python... mais avouez que dit comme ça, ça en jette beaucoup moins p

La vérification ajoutée à la méthode readString consistait à lever une exception si le passage en unicode échouait et dans ce cas, à remplacer les mauvais caractères par des underscore. Ce qui donnait :
from pyamf.amf3 import Decoder
from string import maketrans

in_string="".join([chr(i) for i in range(126,256)])
out_string="_"*130
trans=maketrans(in_string,out_string)

def my_readString(object, use_references=True):
  def readLength():
      x = object.readUnsignedInteger()

      return (x >> 1, x & 1 == 0)

  length, is_reference = readLength()

  if use_references and is_reference:
      return object.context.getString(length)

  buf = object.stream.read(length)

  try:
    result = unicode(buf, "utf8")
  except UnicodeDecodeError:
    buf=buf.translate(trans)
    result = unicode(buf, "utf8")

  if len(result) != 0 and use_references:
      object.context.addString(result)

  return result

sav_readString=Decoder.readString
Decoder.readString=my_readString

L'opération de récupération et de correction de la liste de lecture "Entire Library" pouvant prendre un certain temps si la liste de lecture est importante, j'ai préféré diviser le programme en deux : le premier code récupère la liste de lecture et l'enregistre sous forme d'une liste Python dans un fichier ; le second est le lecteur qui charge la liste de lecture et va lire de façon aléatoire et unique un titre de la bibliothèque smile
Au final on gagne largement en charge réseau et en temps d'accès par rapport à l'utilisation du site Internet et on n'a pas à subir les utilisations mémoire et processeur gourmandes du plugin Flash p

Vous avez donc get_library.py qui donne un résultat de ce style :
Connection on the server...
Connexion successfull!
user_id = 5418
Hooking the PyAMF Decoder
Getting the Entire Library playlist...
Library dumped!

qui génère un fichier library.py utilisé par le lecteur player.py qui joue les flux musicaux et donne un affichage dans ce genre :
Connecting on the server...
Connexion successfull
user_id = 5418
White America - Eminem ( The Eminem Show )
Troisième tour - Rapaces ( 2000 )
Les Poubelles du Coeur - Parabellum ( 1984-2004 )
Question de fun - Les Shériff ( La Saga Des Sheriff (disc 2) )
Alcohol - Dropkick Murphys ( Live On St. Patrick's Day )
Marilyn Moore - Sonic Youth ( Evol )
Come Back, Baby - Ramones ( The Chrysalis Years (disc 1) )
The Watcher - Dr. Dre ( 2001 )
...

La partie lecteur nécessite la présence de mplayer sur le système. Seul regret : on ne peut pas trop jouer sur l'interface de mplayer pour avoir quelque chose de moins verbeux et agréable... une simple barre de défilement de la lecture en cours (à l'instar de wget) m'aurait bien plu... mais c'est une autre histoire smile

Lire directement la musique de Anywhere.FM : ça marche (presque)

, , , ...

Suite de ma précédente étude du fonctionnement de Anywhere.FM.

Je me suis lancé aujourd'hui dans la "pratique" en écrivant quelques lignes de code et les résultats sont pour le moins encourageants smile
Je me suis orienté naturellement vers les modules urllib2 et cookielib de Python que j'avais utilisé pour Wapiti.

Les premières étapes se sont réalisées sans problème (envoies de données par GET ou POST, utilisation d'une "boîte à cookies" p )
Pour la seconde étape j'avais le choix entre utiliser les capacitées de PyAMF à effectuer les requêtes mais en devant injecter mes entêtes HTTP ou rester sur l'utilisation du module urllib2 et injecter les données encodées à l'aide de PyAMF.
J'ai finalement opté pour la seconde méthode, même s'il m'a fallu un peu de temps pour comprendre quels arguments passer à quelles fonctions. Pour résumer j'utilise seulement les fonctionnalitées de création d'objet (plus propre) et d'encodeur/décodeur de PyAMF et toute la partie communication est réalisée par urllib2.

J'ai d'abord implémenté les étapes que j'avais observé au complet puis j'ai ensuite commenté certaines étapes dont l'utilité était douteuse.
Au final les réponses aux questions précédentes sont :
  • _unique_request_id_ est aléatoire. On peut même le définir nous même au début. Reprendre la même valeur à chaque nouvelle session ne semble pas poser de problème, même sur des intervalles de temps rapprochés. Je n'ai pas vérifié si on pouvait se passer de l'incrémentation d'une requête à une autre
  • Les fameuses variabes __qca et __qcb définies par le biais du Javascript sur le serveur quantserve semblent totalement superflues. Ne pas les utiliser n'entrave en rien la communication avec Anywhere.FM. De toute évidence elles sont utilisées uniquement à des fins de tracking
  • tracker_id n'a pas d'utilité, malgré qu'il soit explicitement défini (une requête est générée uniquement à cet effet). Il pourrait s'agir d'une fonctionnalitée que les administrateurs n'ont pas encore pû implémenté mais à venir ?!
  • Certains arguments passés à get_songs_for_user_playlists restent un mystère, toutefois on n'a pas besoin d'en savoir plus pour accèder à la librairie complète d'un utilisateur

Le code que vous pourrez trouver ici : PyAnywhereFM.py et sur pastebin.com n'est pas totalement fonctionnel.
En effet si on parvient bien à ouvrir une session sur le site, à effectuer des requêtes AMF et obtenir les résultats, on ne parvientpas pour le moment à décoder les réponses AMF à cause d'erreurs de décodage UTF-8. A l'heure actuelle je ne saurais pas dire si le problème vient de Anywhere.FM, de PyAMF ou encore de Python...

Il est tout de même possible d'obtenir un jeton valide pour accèder à un titre dont on connait par avance le "master_id" et le "song_ressource_id" (paramêtres qui sont fixes).
Le code tente de se connecter avec les identifiants présents dans la source (à vous d'y insérer les votres) ou fonctionnera avec l'utilisateur par défaut (démo, id = 89 comme vu la dernière fois) si les identifiants sont invalides. Il va ensuite lire un titre dans ma librairie à l'aide de mplayer.

Vous pouvez utiliser un sniffeur comme Wireshark pour comprendre ce qui se passe en fond lors de l'utilisation du script. Il faut noter toutefois que l'utilisateur "demo" a une librairie vide, c'est pour cela qu'il est possible de décommenter une ligne fixant le user_id à 46 (correspondant au compte "Free Music")

L'étape restante consiste donc à extraire les données des réponses AMF obtenues, ce qui ne devrait pas être trop difficile puisque le "master_id" et le "song_ressource_id" sont envoyés comme des chaines de caractères et non comme des entiers.
Tout cela montre qu'il est assez aisé (le code fait 129 lignes une fois les commentaires et les lignes vides retirées) de télécharger de la musique sur Anywhere.FM et malgré qu'il n'y ait pas de système permettant de rechercher un titre en particulier, il serait possible de générer une base de données des titres disponibles en récupérant les profils puis les listes de lecture des utilisateurs du site. Evidemment ça prendrait du temps, mais ce serait toujours plus rapide que de fouiller par le biais de l'interface web du site p