Skip to content

Date archive for: juin 2015

UPnP, Linux et Freebox : un peu de « hacking » pour trouver les URIs de ressources

Alors, voici mon problème initial :

J’utilise un media center pour accéder au flux TV d’une Freebox Revolution. Cet accès se fait via UPnP. Or, à chaque redémarrage de la Freebox, l’URI de la ressource UPnP change et je dois donc reconfigurer ma source TV.

Le media center utilise Kodi (ex-XBMC) sur un Raspberry Pi (cf. distribution OpenElec). Mon idée est donc de scanner les ressources UPnP au démarrage pour modifier automatiquement le chemin de ma source TV.

Cette URI a le format suivant (en l’occurrence dans le fichier ~/.kodi/userdata/guisettings.xml) :

upnp://abcdefgh-8b30-359b-01e9-d706ce4bf569/0/0/106/

et je cherche à trouver la partie suivante : « 0/0/106/ » qui correspond au chemin de la ressource « Freebox TV ».

Recherche des serveurs

Première étape : trouver les serveurs UPnP du réseau pour accéder à la Freebox.

Pour trouver les serveurs UPnP disponibles sur le réseau, vous pouvez utiliser gssdp-discover (issu de gupnp-tools) pour lister les ressources :

> gssdp-discover --timeout=3

Cette commande retournera une liste de ressources (toutes celles qui auront répondu en moins de 3 secondes), dont celle qui nous intéresse :

resource available
  USN:      uuid:abcdefgh-8b30-359b-01e9-d706ce4bf569::urn:schemas-upnp-org:service:ContentDirectory:1
  Location: http://192.168.0.254:52424/device.xml

Recherche des services

Une fois le serveur trouvé, il nous faut le descriptif du service ContentDirectory.
On utilise donc l’URL trouvée précédemment :

> wget http://192.168.0.254:52424/device.xml
> cat device.xml
...
<service>
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
<controlURL>/service/ContentDirectory/control
</controlURL>
<eventSubURL>/service/ContentDirectory/event
</eventSubURL>
<SCPDURL>/service/ContentDirectory/scpd</SCPDURL>
</service>
...

Nous avons donc un ensemble d’URIs, dont une qui nous donnera le descriptif des APIs du service (le SCPD) :

> wget http://192.168.0.254:52424/service/ContentDirectory/scpd
> cat scpd
...

le XML obtenu contient les différentes APIs utilisables, dont celle qui nous intéresse Browse :

<action>
<name>Browse</name>
<argumentList>
  <argument>
    <name>ObjectID</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_ObjectID
    </relatedStateVariable>
  </argument><argument>
    <name>BrowseFlag</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_BrowseFlag
    </relatedStateVariable>
  </argument>
  ...
  <argument>
    <name>TotalMatches</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_Count
    </relatedStateVariable>
  </argument><argument>
    <name>UpdateID</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_UpdateID
    </relatedStateVariable>
  </argument>
</argumentList>
</action>

Nous avons ainsi l’ensemble des informations nécessaires pour adresser le service :

  • L’URL
  • Le chemin du service que nous recherchons
  • Les paramètres à lui fournir
  • Les informations qu’il nous retournera

Requêter le service « Browse »

Voilà, maintenant on peut rassembler tous les morceaux !

Nous allons devoir requêter le service en lui fournissant les bons paramètres. On a déjà de bonnes informations grâce au descriptif XML. On peut en rassembler encore plus en effectuant un sniffing réseau durant une recherche UPnP. En l’occurrence, un petit tcpdump durant un browsing UPnP est très informatif :

> tcpdump -s 0 -A -vvv host 192.168.0.254 and tcp port 52424

Je ne recopierai pas le détail des informations obtenues, mais juste la requête SOAP que j’ai pu forger :

<s:Envelope 
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>0/0</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>id,dc:title,res,sec:CaptionInfo,
sec:CaptionInfoEx,pv:subtitlefile</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>0</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>

La partie structurante de cette requête est :

<ObjectID>0/0</ObjectID>

qui définit le chemin dans lequel je cherche les ressources UPnP : 0/0.
(j’ai déjà déterminé que le chemin de « Freebox TV » était au format « 0/0/XX », alors autant en profiter)

Un petit curl permettra alors de tester cette requête :

 curl -XPOST "http://192.168.0.254:52424/service/ContentDirectory/control" -d '...'

(où les « … » sont à remplacer par la requête SOAP)

Traitement de la réponse

La réponse aura l’allure suivante :

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:BrowseResponse 
xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result>
...
</Result>
<NumberReturned>4</NumberReturned>
<TotalMatches>4</TotalMatches>
<UpdateID>0</UpdateID>
</u:BrowseResponse>
</s:Body>
</s:Envelope>

Les données présentes dans la balise Result constituent la réponse en elle-même. Elle doit être à son tour désérialisée en XML :

<DIDL-Lite 
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" 
xmlns:dc="http://purl.org/dc/elements/1.1/" 
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" 
xmlns:sec="http://www.sec.co.kr/" 
xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">
<container id="0/0/86" parentID="0/0" restricted="1">
<dc:title>Freebox TV</dc:title>
<upnp:class>object.container.storageFolder</upnp:class>
</container>
<container>...</container>
...
</DIDL-Lite>

YESSS !!! On y est : l’information est là :

  • title: Freebox TV
  • container id : 0/0/86

On doit donc rechercher le titre ‘Freebox TV’ pour trouver l’ID : ‘0/0/86’ qui est le chemin initialement recherché. Win !

Hop, on assemble tout ça !

Maintenant, il faut scripter tout ça pour mettre à jour mes paramètres.

Pour ce genre de job, plusieurs possibilités me sautent à l’esprit.

Pourquoi pas un script shell ? Non, trop fouilli à maintenir et vraiment pas idéal pour traiter du XML.

Un script Groovy ? C’est mon premier réflexe pour ce type de traitement : flexible avec une API riche. Problème : la plateforme. Si on tourne sur Raspberry Pi, il faut que ça ‘poutre’. On ne peut pas se permettre de monter une JVM en mémoire à chaque démarrage.

Dernière possibilité qui me saute à l’esprit : un programme Go. Des performances dignes du C avec une API très riche et bien documentée. Banco !

Le résultat

Je ne vais pas rentrer dans le détail du programme Go, juste quelques éléments de réponse.

Le code du programme est disponible sur github : https://github.com/gcastel/go-snippets/blob/master/upnp/findFbxUpnpPath.go

Le requêtage s’effectue grâce à l’API http.NewRequest :

req, err := http.NewRequest("POST", url, bytes.NewBuffer(soap_data))
req.Header.Set("SOAPACTION", "urn:schemas-upnp-org:service:ContentDirectory:1#Browse")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
      panic(err)
}
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)

L’unmarshalling XML nécessite de définir les structures à accéder (avec leurs tags). Exemple :

type Container struct {
        XMLName xml.Name `xml:"container"`
        Title string `xml:"title"`
        Class string `xml:"class"`
        Id string `xml:"id,attr"`
}

Le parsing/unmarshalling fonctionne alors avec l’API xml.Unmarshal :

var d DIDLLite
var id string
xml.Unmarshal([]byte(response), &d)
for _,container := range d.Containers {
   if container.Title == "Freebox TV" {
     id = container.Id
   }
}

Et voilà, on en est venu à bout.
J’espère que quelques uns de ces éléments pourront être utiles 😀