Réaliser de A à Z un webservice XML-RPC en Shell UNIX

Comprendre et mettre en place un webservice XML-RPC en Shell UNIX

Le XML-RPC est un truc simple : le client envoie une requête type web (comme celle du navigateur web) au format XML à un programme sur un serveur qui sert de webservice chargé de récupérer le XML. Dans ce XML, il y a une méthode appelée et ses paramètres à appliquer (les paramètres dépendent de la méthode). Après quelques vérifications protocolaires d'usage, le webservice analyse le XML pour extraire les informations dont il a besoin pour appeler la méthode avec ses paramètres puis il renvoie le résultat, au format XML également.

Voyons concrètement avec un exemple.

Côté client

Prenons l'exemple de base : Hello! (mais nous pourrions faire une méthode rpc.maMethodePerso si nous voulions, c'est à nous de définir ce que nous voulons à partir du moment où nous avons la main sur le serveur également).

C'est la méthode standard pour tester que ça fonctionne. La méthode associée s'appelle "demo.sayHello".

Nous voulons donc demander à un webservice sur le serveur distant d'exécuter cette méthode.

Construction de la requête

Faisons une jolie requête en XML qui va ressembler à ceci (le format de cette requête ne sort pas du chapeau, il provient des spécifications) :

<?xml version="1.0" encoding="utf-8"?>
<methodCall>
<methodName>demo.sayHello</methodName>
<params/>
</methodCall>

Rien de bien compliqué. C'est du XML tout bête, sans fioritures. Nous demandons juste l'appel (methodCall) de la méthode donnée (methodName). Celle-ci n'a pas de paramètres (<params/>).

Ça, c'est la requête que nous avons sur notre machine locale, à la maison, dans un petit fichier qui porte (par exemple) le joli nom de demo.sayHello.xml dans notre exemple (mais nous pouvons ce que nous voulons ou le mettre dans une variable).

Ensuite, nous voulons demander au webservice d'exécuter ça et de nous renvoyer le résultat.

Envoi de la requête

Notre requête étant prête, il ne nous reste plus qu'à envoyer le tout avec quelques lignes de shell pour bien comprendre la mécanique.

On commence par déterminer la longueur de la requête (qui permettra de déterminer que l'entier du message est bien reçu) :

LENGTH="$(cat demo.sayHello.xml | wc -c)"

Puis nous envoyons le tout avec wget (par exemple) :

wget --no-check-certificate --header="Content-Type: text/xml" --header="Content-Length: ${LENGTH}" --post-file=demo.sayHello.xml http://le.webserv.eur/xmlrpc.sh

--header="Content-Type: text/xml" : c'est pour dire que le message est au format XML;

--header="Content-Length: ${LENGTH}" : c'est pour dire que le message a la longueur précédemment calculée;

--post-file=demo.sayHello.xml : c’est pour dire que nous envoyons le contenu du fichier en question;

http://le.webserv.eur/xmlrpc.sh : c'est l'adresse du webservice auquel nous envoyons le tout.

Côté serveur

Configuration du serveur web

Un webservice a besoin d'un serveur web pour fonctionner. Par défaut, il n'exécute pas nativement le Shell UNIX. Il faut donc activer l'option. Pour Apache, il s'agit d'ajouter la configuration :

Options ExecCGI
AddHandler cgi-script .sh

Il suffira ensuite de poser dans les scripts en question dans les répertoires du serveur web, comme n'importe quel autre CGI (et donner les droits d'exécution à Apache).

Réception de la requête

Pour récupérer la requête, il suffit juste d'un petit :

cat - > ${RPC_FILE}

Puis de faire quelques vérifications protocolaires.

Vérification de la méthode

Le protocole impose d'utiliser la méthode POST. Il faut donc vérifier qu'elle soit bien utilisée :

[ "${REQUEST_METHOD}" != "POST" ] && {
HTTP_CODE="405"
HTTP_MSG="Method Not Allowed"
http_error
}

Vérification du format de contenu

Le type de contenu doit être text/xml :

[ "${CONTENT_TYPE}" != "text/xml" ] && {
HTTP_CODE="400"
HTTP_MSG="Bad Request"
http_error
}

Vérification de la longueur du message

Le message doit avoir une longueur définie :

[ ${CONTENT_LENGTH:-0} -eq 0 ] && { HTTP_CODE="411"
HTTP_MSG="Length Required"
http_error
}

La longueur du message reçu doit correspondre à celui envoyé :

QUERY_LENGTH=$(cat ${RPC_FILE} | wc -c)
[ ${QUERY_LENGTH} -ne ${CONTENT_LENGTH} ] && {
HTTP_CODE="400"
HTTP_MSG="Bad Request (${QUERY_LENGTH} <> ${CONTENT_LENGTH})"
http_error
}

Envoi du message d'erreur HTTP

En cas d'erreur dans une de ces vérifications, il ne reste plus qu'à renvoyer un message avec le code erreur en question le cas échéant :

http_error () {
# Envoyer l'erreur HTTP et sortir gentiment
echo "Status: ${HTTP_CODE} ${HTTP_MSG}"
echo 'Content-type: text/html'
echo
echo "<title>${HTTP_CODE} ${HTTP_MSG}</title>"
echo "<h1>${HTTP_CODE} ${HTTP_MSG}</h1>"
echo "<p>Unexpected error processing XML-RPC request.</p>"
exit 0
}

Traitement de la requête

Comme nous venons de tester toutes les erreurs de protocole, le message est normalement arrivé à bon port. Voyons comment le traiter.

Extraction de la méthode

Nous avons reçu un message, mais ne nous ne savons pas encore ce qu'il contient. Nous allons donc extraire la méthode appelée. Pour ça, rien de plus simple qu'un petit coup de XSLT générant l'extraction dans un fichier :

<xsl:template match="methodName">methodName=<xsl:value-of select="."/></xsl:template>

qui génèrera une réponse qui nous récupèrerons par la suite :

METHOD_NAME=$(cat ${CALL_FILE} | grep ^methodName= | cut -d'=' -f2-)

À ce stade, nous savons à présent quelle méthode nous devons appeler pour lui envoyer la requête.

Exécution de la méthode

La méthode n'est donc pas un truc qui tombe du ciel. C'est un bout de code que nous écrivons également (ou un programme externe) et que nous mettons en place sur le webservice qui sert de point d'entrée unique. Dans le cas présent (demo.sayHello), quand on l'appelle, il renvoie (avec des «echo») simplement un message XML qui dit :

<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param>
<value><string>Hello!</string></value>
</param>
</params>
</methodResponse>

Et ensuite c'est fini. Au client de savoir ce qu'il doit en faire. Probablement du XSLT ou équivalent -- moins propre -- pour extraire la réponse.

Quelques exemples

Cas d'une seule valeur

Si nous devions appeler une méthode avec paramètres, comme un pingback, alors il faudrait envoyer au webservice :

<?xmlversion="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://siteweb.se</string></value></param>
<param><value><string>http://mywebsite.com/some-url</string></value></param>
</params>
</methodCall>

pingback.ping prend 2 paramètres (deux URL) : la source et la cible.

Pour extraire les infos côté webservice, c'est assez simple. Le XSLT est :

<xsl:template match="methodCall[methodName='pingback.ping']">
methodName=<xsl:value-of select="methodName"/>
sourceURI=<xsl:value-of select="params/param[1]/value/string"/>
targetURI=<xsl:value-of select="params/param[2]/value/string"/>
</xsl:template>

qui génère un résultat sous forme "plate" (clef=valeur) aisément manipulable par un shell :

valeur=$(cat ${CALL_FILE} | grep ^clef= | cut -d'=' -f2-)

Pour chaque méthode, nous développons un XSLT pour récupérer la bonne forme (et la valider) puis récupérer les valeurs, en faire ce que nous voulons (inscription dans un fichier, une base de données, etc.) et renvoyer une réponse pour dire que c'est fait :

Dans ce cas, la réponse serait :

<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param>
<value><string>Pingback OK</string></value>
</param>
</params>
</methodResponse>

ou un autre message au choix.

Cas d'un enregistrement du plusieurs valeurs de types différents

Plus généralement, dans le cas d'une réponse avec un enregistrement contenant plusieurs valeurs de types différents, il faut utiliser une structure.

Le motif de base de la réponse reste le même :

<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param>
[…]
</param>
</params>
</methodResponse>

Et le paramètre de retour est une structure complexe avec un ou plusieurs membres nommés :

[…]
<struct>
<member>
<name>[…]</name>
<value>[…]</value>
</member>
[…]
</struct> […]

Ce qui donne :

<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param><value>
<struct>
<member>
<name>ServerID</name>
<value><string>AIE45EUSUR89URU0</string></value>
</member>
<member>
<name>DateMesure</name>
<value><dateTime.iso8601>20170801T05:22:09</dateTime.iso8601></value>
</member>
<member>
<name>MemTotal</name>
<value><int>2038544</int></value>
</member>
<member>
<name>MemAvailable</name>
<value><int>1621136</int></value>
</member>
<member>
<name>ProcessorUsage</name>
<value><double>19.5</double></value>
</member>
</struct>
</value></param>
</params>
</methodResponse>

Les données seraient alors extraites de la façon suivante :

<xsl:template match="member[name='ServerID']">ServerID=<xsl:value-of select="value/string"/></xsl:template>
<xsl:template match="member[name='DateMesure']">DateMesure=<xsl:value-of select="value/dateTime.iso8601"/></xsl:template>
<xsl:template match="member[name='MemTotal']">MemTotal=<xsl:value-of select="value/int"/></xsl:template>
<xsl:template match="member[name='MemAvailable']">MemAvailable=<xsl:value-of select="value/int"/></xsl:template>
<xsl:template match="member[name='ProcessorUsage']">ProcessorUsage=<xsl:value-of select="value/double"/></xsl:template>

Si nous ne voulions pas nommer les valeurs, mais juste avoir un résultat plus compact, alors il serait tout à fait correct d'utiliser un tableau de la façon suivante :

<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param><value>
<array>
<data>
<value><string>AIE45EUSUR89URU0</string></value>
<value><dateTime.iso8601>20170801T05:22:09</dateTime.iso8601></value>
<value><int>2038544</int></value>
<value><int>1621136</int></value>
<value><double>19.5</double></value>
</data>
</array>
</params>
</methodResponse>

Ce qui obligerait à identifier les valeurs non pas en fonction de leur nom (name), mais en fonction de leur position :

<xsl:template match="array/data/value[position()=1]">ServerID=<xsl:value-of select="string"/></xsl:template>
<xsl:template match="array/data/value[position()=2]">DateMesure=<xsl:value-of select="dateTime.iso8601"/></xsl:template>
<xsl:template match="array/data/value[position()=3]">MemTotal=<xsl:value-of select="int"/></xsl:template>
<xsl:template match="array/data/value[position()=4]">MemAvailable=<xsl:value-of select="int"/></xsl:template>
<xsl:template match="array/data/value[position()=5]">ProcessorUsage=<xsl:value-of select="double"/></xsl:template>