web-dev-qa-db-fra.com

Comment détecter une erreur CORS (Cross Origin Origin) par rapport à d'autres types d'erreurs pour XMLHttpRequest () en Javascript

J'essaie de détecter lorsqu'une XMLHttpRequest () échoue en raison d'une erreur d'origine croisée, par opposition à une requête incorrecte. Par exemple:

    ajaxObj=new XMLHttpRequest()
    ajaxObj.open("GET", url, true); 
    ajaxObj.send(null);

Considérons 4 cas pour l'URL:

Cas 1: url est une adresse valide où access-control-allow-Origin est correctement défini

  • Exemple: http://192.168.8.35 où j'ai un serveur avec Access-Control-Allow-Origin: * défini dans l'en-tête
  • C'est facile à détecter comme ajaxObj.readyState == 4 et ajaxObj.status == 200

Cas 2: L'URL est une adresse invalide sur un serveur existant

  • Exemple: http://xyz.google.com où le serveur répond mais ce n'est pas une requête valide
  • Cela se traduit par ajaxObj.readyState == 4 et ajaxObj.status == 0

Cas 3: l'URL est à une adresse IP de serveur non existante

  • Exemple: http://192.168.8.6 sur mon réseau local où il n'y a rien à répondre
  • Cela se traduit par ajaxObj.readyState == 4 et ajaxObj.status == 0

Cas 4: url est une adresse valide où access-control-allow-Origin est NE PAS ensemble

  • Exemple: http://192.168.8.247 où j'ai un serveur sans pour autant Access-Control-Allow-Origin: * défini dans l'en-tête
  • Cela se traduit par ajaxObj.readyState == 4 et ajaxObj.status == 0

Le problème est: Comment différencier le cas 4 (erreur access-control-allow-Origin) des cas 2 et 3?

Dans le cas 4, la console de débogage de Chrome affiche l'erreur:

XMLHttpRequest cannot load http://192.168.8.247/. Origin http://localhost is not allowed by Access-Control-Allow-Origin.

Comment faire pour que cette erreur soit connue en Javascript?

J'ai essayé de trouver une indication dans ajaxObj mais rien ne semble différent des cas 2 et 3.

Voici un test simple que j'ai utilisé:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>CORS Test</title>
<script type="text/javascript">
function PgBoot()
{
//  doCORS("http://192.168.8.35");   // Case 1
//  doCORS("http://xyz.google.com"); // Case 2
    doCORS("http://192.168.8.6");    // Case 3
//  doCORS("http://192.168.8.247");  // Case 4
}

function doCORS(url)
{
    document.getElementById("statusDiv").innerHTML+="Processing url="+url+"<br>";
    var ajaxObj=new XMLHttpRequest();
    ajaxObj.overrideMimeType('text/xml');
    ajaxObj.onreadystatechange = function()
    {
        var stat=document.getElementById("statusDiv");
        stat.innerHTML+="readyState="+ajaxObj.readyState;
        if(ajaxObj.readyState==4)
            stat.innerHTML+=", status="+ajaxObj.status;
        stat.innerHTML+="<br>";
    }
    ajaxObj.open("GET", url, true); 
    ajaxObj.send(null);
}
</script>
</head>
<body onload="PgBoot()">
<div id="statusDiv"></div>
</body>
</html>

Résultats avec Chrome:

Processing url=http://192.168.8.35
readyState=1
readyState=2
readyState=3
readyState=4, status=200

Processing url=http://xyz.google.com
readyState=1
readyState=4, status=0

Processing url=http://192.168.8.6
readyState=1
readyState=4, status=0

Processing url=http://192.168.8.247
readyState=1
readyState=4, status=0
43
user2871305

Non, il n'y a aucun moyen de faire la différence, selon les spécifications du W3C.

Voici comment la spécification CORS spécifie la procédure simple requête croisée :

Appliquez les étapes de création de requête et observez les règles de requête ci-dessous lors de la requête.

Si l'indicateur de redirection manuelle est désactivé et que la réponse a un code d'état HTTP de 301, 302, 303, 307 ou 308: Appliquez les étapes de redirection.

Si l'utilisateur final annule la demande: Appliquez les étapes d'abandon.

En cas d'erreur réseau: _ En cas d'erreurs DNS, d'échec de la négociation TLS ou d'un autre type d'erreur réseau, applique les étapes de l'erreur réseau. Ne demandez aucune interaction de la part de l'utilisateur final ...

Sinon: Effectuez une vérification du partage des ressources. S'il échoue, applique les étapes d'erreur de réseau ...

En cas d'échec de la connexion réseau ou de l'échange CORS, les étapes d'erreur réseau sont appliquées, il n'y a donc littéralement aucun moyen de distinguer les deux cas.

Pourquoi? L'un des avantages est d'empêcher un attaquant d'inspecter la topologie réseau d'un réseau local. Par exemple, un script de page Web malveillant pourrait trouver l'adresse IP de votre routeur en demandant son interface HTTP et, ainsi, en apprendre davantage sur la topologie de votre réseau (par exemple, la taille de votre bloc IP privé, /8 ou /16). Puisque votre routeur n'envoie pas (ou ne devrait pas) envoyer les en-têtes CORS, le script n'apprend absolument rien.

35
apsillers

Peut-être que si cela peut aider quelqu'un ... un autre moyen de gérer la différence entre une erreur de réseau et une erreur de réseau ... peut fonctionner avec chrome ou firefox ... (solution non parfaite cependant)

var external = 'your url';

if (window.fetch) {
    // must be chrome or firefox which have native fetch
    fetch(external, {'mode':'no-cors'})
        .then(function () {
            // external is reachable; but failed due to cors
            // fetch will pass though if it's a cors error
        })
        .catch(function () {
            // external is _not_ reachable
        });
} else {
    // must be non-updated safari or older IE...
    // I don't know how to find error type in this case
}
6
Krishna K

Pour différencier une violation CORS des autres demandes AJAX ayant échoué, vous pouvez inspecter les en-têtes de réponse d'une demande HEAD à l'aide du code côté serveur et transmettre les résultats à votre page client. Par exemple, si la demande AJAX échoue (état 0), vous pouvez appeler ce script (appelons-le cors.php) et savoir avec certitude si les en-têtes de réponse contiennent des en-têtes Access-Control-*.

Exemples:

cors.php? url = http: //ip.jsontest.com
cors.php? url = http: //www.google.com
cors.php? url = http: //10.0.0.1

résultats

HTTP/1.1 200 OK Access-Control-Allow-Origin: *
HTTP/1.1 302 trouvé
Requête invalide


cors.php - Personnaliser selon les besoins

<?php /* cors.php */
$url = $_GET["url"];
if(isset($url)) {
    $headers = getHeaders($url);
    header("Access-Control-Allow-Origin: *");

    if(count($headers) == 0) {
        die("Invalid request"); // cURL returns no headers on bad urls
    } else {
        echo $headers[0];       // echo the HTTP status code
    }

    // Include any CORS headers
    foreach($headers as $header) {
        if(strpos($header, "Access-Control") !== false) {
            echo " " . $header;
        }
    }
}

function getHeaders($url, $needle = false) {
    $headers = array();
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4);        // Timeout in seconds
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);               // Timeout in seconds
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_VERBOSE, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD');    // HEAD request only
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use(&$headers) {
        array_Push($headers, $header);
        return strlen($header);
    });
    curl_exec($ch);
    return $headers;
} /* Drakes, 2015 */

Faisceau de test côté client:

function testCORS(url, $elem) {
    $.ajax({
      url: url,
      timeout: 4000
    })
    .fail(function(jqXHR, textStatus) {
       if(jqXHR.status === 0) {
           // Determine if this was a CORS violation or not
           $.ajax({
              context: url,
              url: "http://myserver.com/cors.php?url=" + escape(this.url),
           })
           .done(function(msg) {
              if(msg.indexOf("HTTP") < 0) {
                $elem.text(url + " - doesn't exist or timed out");
              } else if(msg.indexOf("Access-Control-Allow-Origin") >= 0) {
                $elem.text(url + " - CORS violation because '" + msg + "'");
              } else {
                $elem.text(url + " - no Access-Control-Allow-Origin header set");
              }
           });
       } else {
           // Some other failure (e.g. 404), but not CORS-related
           $elem.text(url + " - failed because '" + responseText + "'");
       }
    })
    .done(function(msg) {
      // Successful ajax request
      $elem.text(this.url + " - OK");
    }); /* Drakes, 2015 */
}

Pilote de harnais:

// Create a div and append the results of the URL calls
$div = $("<div>"); 
$("body").append($div);

var urls = ["http://ip.jsontest.com", "http://google.com", "http://10.0.0.1"];
urls.map( function(url) {
   testCORS(url, $div.append("<h4>").children().last());
});

Les resultats:

http://ip.jsontest.com - OK

http://google.com - pas d'en-tête Access-Control-Allow-Origin défini

http://10.0.0.1 - n'existe pas ou a expiré

3
Drakes

Étonnamment, vous pouvez le faire pour les images (et peut-être existe-t-il une solution similaire pour d'autres types de contenu particuliers). Le truc est que apparemment ne regarde pas CORS, donc la déclaration "url ne parvient pas à se charger par XHR && url réussit à se charger par img src" implique que l’URL fonctionne mais que CORS est bloqué.

Cela n’aide certainement pas dans tous les cas, mais dans certains cas (par exemple, si un utilitaire détecte si vous passez correctement CORS), cela peut faire l'affaire. Par exemple, je voulais déclencher un avertissement dans la console si mon application (frontend et backend séparées) est installée avec une URL correctement configurée pour le backend et CORS mal configurée sur le serveur. C'était donc parfaitement suffisant pour moi, car je peux servir une image pour la tester sur.

function testCors(url) {
    var myRequest = new XMLHttpRequest();
    myRequest.open('GET', url, true);
    myRequest.onreadystatechange = () => {
        if (myRequest.readyState !== 4) {
            return;
        }
        if (myRequest.status === 200) {
            console.log(url, 'ok');
        } else {
            var myImage = document.createElement('img');
            myImage.onerror = (...args) => {console.log(url, 'other type of error', args);}
            myImage.onload = () => {console.log(url, 'image exists but cors blocked');}
            myImage.src = url;
            console.log(url, 'Image not found');
        }
    };
    myRequest.send();
}
1
amik

Voici ce que j'ai fait - bien qu'il soit nécessaire que vous apportiez des modifications au serveur distant (ajoutez-y une page).

  1. J'ai créé une page probe.html à servir dans une iframe (sur l'origine distante que vous essayez d'utiliser CORS pour contacter)
  2. J'enregistre un gestionnaire window.onmessage dans ma page
  3. Je charge la page probe.html dans ma page à l'aide d'un iframe sécurisé
  4. Dans ma page sur l'événement iframe onload, j'envoie un message à iframe à l'aide de window.postMessage ()
  5. La page probe.html sonde l'url (de même origine dans l'iframe)
  6. La page probe.html m'envoie les détails de la réponse xhr.status et xhr.statusText à l'aide de window.postMessage ()
  7. Si le statut est une erreur, je consulte le statut et le statusText à notre service de journalisation

Cependant, sachez qu'il est très difficile de le sécuriser si vous passez les paramètres dans un sens ou dans l'autre (n'importe qui peut intégrer votre iframe, d'autres parties peuvent provoquer postMessage sur la page ou sur l'iframe).

Et ce n’est pas parfait (il ne détecte que les erreurs qui ne sont pas ponctuelles).

Cependant, même si cela représente beaucoup de travail, vous pouvez obtenir plus d'informations sur la cause d'une erreur. Très utile lorsque vous ne pouvez pas accéder directement au navigateur de vos utilisateurs.

PS: ceci est complètement différent de la réponse PHP qui suggère à votre serveur de parler au serveur distant (a) peu utile pour diagnostiquer les causes d'échec de la communication et (b) en utilisant curl de PHP demande simplement que votre serveur soit sérieusement utilisé!

0
robocat