Requête Ajax Cross-domain avec la balise <script>

le - Lien permanent 23 commentaires

La principale limitation que nous avons rencontré lorsque nous avons commencé à effectuer des requêtes Ajax est la contrainte de “Same Origin Policy” qui nous est imposée par les paramètres de sécurité des navigateurs : nos requêtes Ajax ne peuvent être effectuées que vers le domaine sur lequel notre site est déployé.

Typiquement, depuis “blog.pascal.martin.fr”, il n’est pas permis d’effectuer de requête Ajax vers “www.google.com“ : le navigateur l’interdit.

Cet article va nous permettre d’apprendre à contourner cette sécurité, en effectuant des requêtes Ajax sans passer par l’objet XmlHttpRequest, mais en écrivant dynamiquement des balises <script>.

Application d’exemple

Pour mes exemples au sein de cet article, je considérerai que nous souhaitons développer une mini-application dont le but est d’effectuer des multiplications :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
  <title>Démo Ajax serveur distant</title>
  <script type="text/javascript" src="lib/prototype.js"></script>
  <script type="text/javascript" src="ajax-distant.js"></script>
</head>
<body>
  <form action="" id="multiplier" onsubmit="return false;">
    <div>
      <input type="text" maxlength="6" size="8" id="a" name="a" />
      *
      <input type="text" maxlength="6" size="8" id="b" name="b" />
      <input type="button" name="do" id="do" value="=" onclick="multiply();" />
      <input type="text" size="8" id="result" name="result" readonly="readonly" />
    </div>
  </form>
  <div id="msg">&nbsp;</div>
  
  <script type="text/javascript">
    // Appui sur "ENTER" dans le formulaire => lance la multiplication
    Event.observe($('multiplier'), 'keydown', function (evt) {
        if (evt.keyCode == 13) {
            multiply();
        }
    });
  </script>
  
</body>
</html>

Deux champs de saisie, un bouton pour lancer la multiplication, et un champ pour afficher le résultat :

Application multiplier

Dans le principe, nul besoin d’effectuer une requête Ajax pour effectuer une multiplication : le code Javascript suivant ferait parfaitement l’affaire :

var multiply_noAjax = function () {
    var a = parseFloat($F('a'));
    var b = parseFloat($F('b'));
    $('result').value = a*b;
}; // multiply_noAjax

Pour faciliter les écritures, et ne pas avoir à changer la partie HTML de notre application, nous passerons par un alias : pour chaque nouvelle version, nous affecterons une nouvelle fonction à la variable multiply, par laquelle l’application passera toujours.

En travaillant avec la version “sans Ajax”, donc :

var multiply = multiply_noAjax;

Autrement dit, appeler la fonction multiply revient en fait à appeler la fonction multiply_noAjax.


Un appel Ajax “normal”

Passons maintenant à une version Ajaxifiée de notre application de calcul de multiplications : les deux opérandes sont passées, via un appel Ajax, à une application côté serveur, qui, elle, effectue la multiplication, et retourne le résultat.

Le code Javascript n’a alors plus qu’à afficher ledit résultat dans le champ correspondant, sans avoir à effectuer le moindre calcul.

Pour simplifier les choses (en particulier, la compatibilité cross-navigateur), j’utiliserai le Framework javascript prototype, que j’ai déjà présenté au cours de plusieurs articles.
Les données de retour transiteront au format JSON.
Je vous conseille donc la lecture des articles suivants :

Voici le code de la fonction Javascript effectuant l’appel Ajax :

var multiply_ajaxBase = function (url) {
    var a = parseFloat($F('a'));
    var b = parseFloat($F('b'));
    var myAjax = new Ajax.Request(
    url,
    {
      method: 'get',
      parameters: {
        a: a,
        b: b
      },
      onSuccess: function (xhr, jsonValue) {
          if (jsonValue) {
            $('result').value = jsonValue;
          }
        }, // onSuccess
      onException: function (xhr, e) {
          $('msg').innerHTML = e;
        } // onException
    });
}; // multiply_ajaxBase

Et voici comment elle est appelée, avec l’URL du service de calcul passée en paramètre.

var multiply_ajaxLocal = function () {
    multiply_ajaxBase('./multiplier-server.php');
}; // multiply_ajaxLocal

Naturellement, comme indiqué précédemment, nous aliasons cette fonction sous le nom multiply :

var multiply = multiply_ajaxLocal;

Et côté serveur, rien de très compliqué : on récupère les deux valeurs passées en GET, et on renvoit le résultat, au sein d’une en-tête X-JSON, tel qu’attendu par prototype :

$a = floatval($_GET['a']);
$b = floatval($_GET['b']);
$result = $a * $b;

// prototype.js attend des données dans 
// une en-tête nommée X-JSON :
header('X-JSON:' . json_encode($result));

Note : A vous de gérer les éventuels cas d’erreurs (du genre si l’utilisateur saisit n’importe quoi)

Note 2 : Nous pourrions aussi renvoyer les données sur la sortie standard, et utiliser la propriété responseText de l’instance d’objet XMLHttpRequest côté JavaScript… Ce que nous ferions certainement si nous n’utilisions pas le framework Prototype.


Echec en cross-domain

A présent, que se passe-t’il si nous essayons d’appeler un service serveur localisé sur un autre nom de domaine que celui où se trouve notre page d’application cliente ?

var multiply_ajaxDistantErreur = function () {
    multiply_ajaxBase('http://distant.example.com/ajax-distant/multiplier-server.php');
} // multiply_ajaxDistantErreur

(Libre à vous d’adapter l’URL pour pointer vers un autre site ; le tout étant que l’URL comporte un nom de domaine autre que celui où est déployée l’application HTML+Javascript ; Au besoin, ajouter un alias dans le fichiers hosts de votre système, et un second VirtualHost Apache fonctionne parfaitement - c’est ce que j’ai fait pour écrire cet article, d’ailleurs ^^)

Et, encore une fois, avec l’alias qui va bien :

var multiply = multiply_ajaxDistantErreur;

Lorsque nous cliquons sur le bouton censé lancer le calcul, nous obtenons affichage d’un message d’erreur :

Error: Permission denied to call method XMLHttpRequest.open

C’est ce à quoi nous nous attendions : la contrainte de “Same Origin Policy” nous empéche de réaliser des appels Ajax vers un nom de domaine autre que celui où est déployée notre application.


Solution à base de balise <script>

La contrainte de SOP nous empêche, nous venons de le constater, d’effectuer des appels Ajax vers un autre nom de domaine… Du moins, en passant par l’objet XMLHttpRequest.

Heureusement (du moins, dans le cadre de ce que nous voulons faire), cette contrainte n’est pas appliquée à tous les chargements de données distants.

En particulier, elle ne s’applique pas aux chargements de scripts Javascript : rien ne vous empêche d’écrire des balises <script> incluant un fichier Javascript depuis un autre nom de domaine que celui où est déployée votre application.


Code Javascript générant une balise <script> dynamique

Maintenant, plutôt que d’effectuer un “vrai” appel Ajax, nous pourrions créer dynamiquement une balise <script> et l’injecter dans le DOM de notre page d’application… En ajouotant les paramètres qui nous intéressent dans l’URL indiquée en attribut “src” de cette balise.

Au moment où la balise <script> sera injectée dans le DOM, le navigateur effectuera un appel vers le serveur distant pour charger le code Javascript correspondant ; mais on peut tout à fait utiliser un script (PHP, pour nos exemples) pour générer du Javascript !

Donc, pour notre cas :

var multiply_scriptTag = function () {
    var a = parseFloat($F('a'));
    var b = parseFloat($F('b'));
    
    if ($('requestMultiplier')) {
        // Suppression de la balise si elle existait déjà
        // (pour faire un brin de ménage dans le DOM)
        $('requestMultiplier').remove();
    }
    
    // Création de la nouvelle balise :
    var script = document.createElement('script');
    script.src = 'http://distant.example.com/ajax-distant/multiplier-server-tag.php?';
    script.src += 'func=resultFunction';
    script.src += '&a=' + a;
    script.src += '&b=' + b;
    
    script.id = 'requestMultiplier';
    script.type = 'text/javascript';
    
    // Et injection dans le DOM :
    document.body.appendChild(script);
}; // multiply_scriptTag

Que fait cette fonction ?

  • S’il existe déjà une balise <script> utilisée pour effectuer cet appel distant, elle sera supprimée (inutile d’en avoir 36, avec seulement la dernière qui serve encore à quelque chose)
  • Création de la nouvelle balise :
    • Renseignement de l’URL du code Javascript à charger, en lui passant les paramètres qui nous intéressent, dont un nom de fonction à appeler une fois le calcul effectué,
    • Renseignement de l’id et du type de la balise,
  • Et ajout de la balise à la fin de la page HTML.

Pourquoi indiquer à la page distante quelle sera la fonction à appeler une fois le calcul effectué ?

Parce que le chargement du script distant se fait en dehors de notre contrôle : une fois la balise ajoutée au document, nous n’avons plus aucune maîtrise ; et, en particulier, nous ne savons pas quand le script aura fini de charger - et nous n’avons donc aucun moyen de récupérer le résultat du calcul !

Alors que si le programme serveur génère, en fin du code Javascript, un appel à cette fonction, en lui passant en paramètre le résultat du calcul, nous saurons que celui-ci s’est terminé, et quelle valeur a été retournée.


Fonction appelée une fois le script distant chargé

Voici donc la fonction qui devra être appelée une fois le calcul terminé - une fois le script distant chargé :

/**
 * Appelée au retour de l'appel "Ajax"
 */
var resultFunction = function (jsonValue) {
    if (jsonValue) {
        $('result').value = jsonValue;
    }
    else {
        $('msg').innerHTML = 'Erreur...';
    }
}; // resultFunction

Dans le principe, on récupère la valeur passée en paramètre, en on l’affiche dans le champ de résultat.


Page distante effectuant le calcul

Et pour la page appelée via l’attribut src de la balise <script> :

<?php
// Le calcul à effectuer
$a = floatval($_GET['a']);
$b = floatval($_GET['b']);
$result = $a * $b;

// Et la fonction JS qui devrait être appelée au retour
$funcResult = $_GET['func'];

$jsonResult = json_encode($result);
echo "$funcResult($jsonResult);";

Le principe est composé de trois étapes :

  • Récupération des paramètres,
  • Calcul,
  • Et écriture du code Javascript qui sera interprété par le navigateur
    • En l’occurrence, appel à la fonction chargée de gérer la valeur retournée.


Échange HTTP

Lorsque la balise <script> est dynamiquement ajoutée à la page, une requête HTTP est effectuée vers l’URL précisée en attribut src de celle-ci.

Cette requête HTTP, dans notre cas, est la suivante :

GET /ajax-distant/multiplier-server-tag.php?func=resultFunction&a=2&b=10 HTTP/1.1
Host: distant.example.com
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b5) Gecko/2008040603 Firefox/3.0b5
Accept: */*
Accept-Language: fr-fr,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: UTF-8,*
Keep-Alive: 300
Connection: keep-alive
Referer: http://tests/ajax-distant/demo.html

Et une fois les calculs terminés, la réponse suivante est émise par le serveur :

HTTP/1.1 200 OK
Date: Mon, 14 Apr 2008 08:40:13 GMT
Server: Apache/2.2.4 (Ubuntu) DAV/2 SVN/1.4.4 mod_fastcgi/2.4.2 PHP/5.2.3-1ubuntu6.3 proxy_html/2.5 mod_perl/2.0.2 Perl/v5.8.8
X-Powered-By: PHP/5.2.3-1ubuntu6.3
Content-Length: 19

Keep-Alive: timeout=15, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

resultFunction(20);

Du côté de l’application cliente, tout ce qui est vu par le navigateur est une portion de code Javascript à exécuter :

resultFunction(20);

Et, considérant la définition de la fonction resultFunction, cela provoquera l’affichage du résultat de la multiplication !


Format de données pour les échanges

Pour une donnée simple comme un résultat de multiplication, la question du format de données à utiliser pour les échanges est peu importante…

… Mais lorsque vous travaillerez avec des cas concrets d’appels distants, “quel format utiliser ?” est une question que vous vous poserez certainement.

J’ai tendance, dans ce genre de situations, à privilégier le format JSON :

  • Facile à générer côté serveur : il existe des bibliothèques pour la plupart des langages orientés Web
  • Facile à interpréter côté navigateur (c’est du code Javascript valide)
  • C’est du code Javascript valide[1] : considérant que votre appel distant se fait via une balise <script type="text/javascript">, il semble logique que le serveur génère… du Javascript !


Quelques points à noter

Avant de terminer, j’aimerais attirer votre attention sur un point : avec ce mécanisme, vous offrez au serveur distant la possibilité d’injecter n’importe quelle portion de code Javascript dans votre page !

Autrement dit, vous offrez au serveur distant la possibilité de faire à peu près ce qu’il veut sur la page de votre site, via une injection XSS… Que vous avez volontairement créé. En vrac :

  • Vol de cookies des utilisateurs,
  • Ajout de publicités lui rapportant de l’argent - et non à vous,
  • Affichage de n’importe quoi sur votre page - vous décrédibilisant éventuellement,

Un exemple simple pour illustrer mes propos ?
Que se passera-t’il si le code du serveur distant contient la portion de code PHP suivante :

echo "document.getElementsByTagName('body')[0].innerHTML = 'MECHANT';";

Qui entraînera la génération de cette portion de code Javascript :

document.getElementsByTagName('body')[0].innerHTML = 'MECHANT';

Effectivement, en une ligne, le serveur distant a démoli votre site ^^

N’effectuez donc ce genre d’appel que vers des serveurs en lesquels vous avez entièrement confiance !


Note

[1] Oui, ça fait deux fois que je le dis ; mais c’est important

Vous avez apprécié cet article ? Faites le savoir !

Commentaires

1. Par andras le 2008-07-08 14:42
andras

excellent ! comment faire de l'Ajax sans l'objet XHR ... et en contournant ses restrictions. Je suis tombé par hasard sur votre post en cherchant s'il etait possible de contourner la limitation "cross-domain" d'AJAX. Votre solution est très astucieuse et très clairement expliquée. Merci !

2. Par Pascal MARTIN le 2008-07-08 22:25
Pascal MARTIN
Merci :-)

Ce n'est pas vraiment "ma" solution ^^ mais il est vrai qu'on ne la trouve que trop peu souvent mentionnée en français - comme trop de points traitant de développement Web :-(

La rédaction et les exemples sont 100% "miens", par contre, donc merci encore plus ;-)

3. Par Arthur J le 2008-11-14 14:45
Arthur J

Bonjour,

Juste un petit post de remerciement - cet article m'a été bien utile dans le développement d'un projet.

L'article est clair et explicite, c'est fantastique :)

Bonne continuation

-- Arthur

4. Par Pascal MARTIN le 2008-11-14 21:42
Pascal MARTIN

Merci :-)

Bonne continuation à vous aussi !

5. Par aroug le 2008-11-30 10:38
aroug

J'ai recherché longtemps ... je l'avais perdu dans mes liens divers mais je viens de retrouver cet article : totalement d'accord avec les deux remarques ci-dessus ... J'en avais déjà connaissance - de la possibilité de se passer des requêtes du type "xhr" mais la plus grande qualité de votre article est de l'expliquer "simplement" ... Merci du temps passé à rendre clair une technique un peu complexe et bonne continuation surtout !!

6. Par Pascal MARTIN le 2008-11-30 22:37
Pascal MARTIN

Merci :-)

7. Par yves aubry le 2008-12-18 10:48
yves aubry

Lorsque l'on intègre googlemaps dans un site, je crois qu'on utilise la même méthode.
On insère un lien du type:
<script src="http://maps.google.com/maps?file=ap..." type="text/javascript"></script>
et on crée un objet avec en paramètre l'id d'un div.

Quand on zoom ou se déplace dans la carte, ça télécharge des images depuis le site de google.

8. Par Ninidc le 2009-01-27 13:58
Ninidc

Étonnant car je parviens à communiquer avec un script php d'un autre domaine en utilisant seulement l'objet XMLHttpRequest, sans passer par prototype...
Quelqu'un peut-il faire le test pour confirmer cela ?

9. Par Ninidc le 2009-01-27 14:16
Ninidc

Au temps pour moi...
Après avoir vérifié, c'est mon php qui exécute le script distant et non l'XMLHttpRequest...
Mon schéma est donc JS <=> PHP local <=> PHP distant

10. Par Lotek le 2009-02-01 12:42
Lotek

Slt,

Il y a un truc que je ne comprend pas ou qui n'est pas logique...

Comment est il possible et de prétendre qu'on peut voler tes cookies (toi qui est en train de me lire) uniquement en chargeant une page sur un serveur et en ajoutant du code JS ???

Seul celui qui l'injecte le récupère et non pas l'utilisateur X!
A moins que X modifie le serveur par FTP login/pass ...

11. Par Pascal MARTIN le 2009-02-02 18:30
Pascal MARTIN

Bonsoir,

A partir du moment où vous pouvez injecter du code Javascript dans une page (c'est le cas ici), vous avez accès aux cookies de la page dans laquelle vous avez réalisé votre injection. (via document.cookie)

A partir du moment où vous avez accès à ces cookies, il suffit d'un bout de script pour les envoyer à un serveur distant (le votre, par exemple), où ils peuvent être loggués.

Une fois les cookies loggués, rien de plus facile pour vous que d'ouvrir un navigateur, et de les re-créer dans celui-ci (une manière relativement simple et graphique de faire est en utilisant l'extension "Web Developper" de Firefox)...

=> Vous avez, dans votre navigateur, les cookies qu'avait au départ un autre utilisateur -- le visiteur de la page sur laquelle se trouvait l'injection XSS... Et bien souvent, cela signifie que vous avez accès à sa session, à ses données personnelles, à son compte, aux opérations qu'il pouvait effectuer sur le site, ...

Pour plus d'informations, n'hésitez pas à jeter un coup d'œil sur les nombreux documents qui existent sur le sujet ; par exemple : http://www.owasp.org/index.php/XSS (et, plus particulièrement, l'exemple "Cookie Grabber")

En espérant que cela vous apporte un début de réponse,

Bon courage !

12. Par dkdos le 2009-03-07 16:49
dkdos

Bonjour et merci de cette contribution

Je me demande s'il est possible d'envoyer des variables en POST plutôt que via une url en get comme avec un objet xhr ?

merci :)

13. Par Pascal MARTIN le 2009-03-14 22:13
Pascal MARTIN

Bonsoir,

L'URL indiquée en src de la balise <script> est chargée par le navigateur. Vu que, avec cette balise, il ne s'agit pas normalement pas d'envoyer des données au serveur, mais de charger un fichier, le navigateur envoi une requête GET.

C'est l'usage que nous faisons ici de la balise <script> qui est "anormal" ^^

Donc, non, je ne pense pas qu'il soit possible d'envoyer une requête en POST de cette manière.

14. Par WebShaker le 2009-07-09 09:35
WebShaker

Salut.
Personellement j'utilise des iframes.

L'appelle à une balise script est elle asynchrone.
J'ai pas testé votre solution (encore)... Je vais le faire.

Merci a bientot.

15. Par evan le 2009-08-22 13:15
evan

Thanks a lot
Merci beaucoup
Gracias

well explanation

16. Par fabscanta le 2009-10-14 12:10
fabscanta

Bonjour,

Bravo pour l'article détaillé.

Je reprends la remarque de WebShaker. L'ajout de balise script rend le script synchrone. Dans Ajax, il y a Asynchrone. Je ne vois pas comment cette méthode rend asynchrone le script distant.
Pourrais-tu approfondir cette partie STP ?
Merci beaucoup.

fabscanta

17. Par Mistic le 2010-01-31 15:14
Mistic

Merci pour cet article, il m'a été très utile

18. Par Mistic le 2010-01-31 15:34
Mistic

(Désolé pour le doublon)
pour revenir sur les remarques de WebShaker et fabscanta, y a-t-il moyen d'interrompre l'exécution du script inséré ? (par exemple si la page appelée est indisponible)
j'ai lu que supprimer le Child inséré ne fonctionnait pas du moment que le script est chargé (ce qui est le cas ici) mais peut être y a-t-il un autre moyen..

19. Par Alex le 2010-09-25 22:20
Alex

Merci ! Bon article !

20. Par Développeur Web le 2011-04-10 23:01
Développeur Web

Pas mal du tout !

@WebShaker : les iFrame ne permettent pas le cross-domain, car elles deviennent inaccessibles depuis JavaScript lorsqu'elles font appel à un domaine différent de celui de la page parente...

21. Par Htpépé le 2011-06-16 12:33
Htpépé

Bonjour,
Cette stratégie de contournement est elle nécessaire lorsqu'on a accès au serveur cible? Accès en tant qu'administrateur.

22. Par Josiane le 2011-09-04 18:36
Josiane

Exellent, le seul inconvénient c'est qu'on doit contrôler le domaine distant et pouvoir mettre des fichier dessus...lol on peu pas faire une requêtes vers un site qui n'est pas le notre. quelle dommage!

23. Par Annuaire Francais le 2011-10-04 12:22
Annuaire Francais

Bonjour,

Merci Pascal pour ces informations. Je butte un peu sur ce probleme pour la page du moteur interne, qui pour le moment, est un iframe qui interroge le serveur distant des données, mais ce n'est pas la bonne méthode. J'essaye de rendre la V2 de mon annuaire conviviale, et je n'arrives pas a résoudre ce problème d'autorisation.

Je vais tester cette solution, car je n'utilise pas php et je n'arrives pas a donner les autorisations voulues a ma page distante pour afficher le contenu ajax.

Je met cette page en favori et donnerais le résultat
Marc L

Ce post n'est pas ouvert aux nouveaux commentaires (probablement parce qu'il a été publié il y a longtemps).