Как обнаружить ошибку перекрестного происхождения (CORS) по сравнению с другими типами ошибок для XMLHttpRequest() в Javascript

Я пытаюсь определить, когда происходит сбой XMLHttpRequest() из-за ошибки Cross Origin, а не из-за неверного запроса. Например:

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

Рассмотрим 4 случая для URL:

Случай 1: URL-адрес является допустимым адресом, где правильно задан access-control-allow-origin

  • Пример: http://192.168.8.35 где у меня есть сервер с Access-Control-Allow-Origin: * установить в шапке
  • Это легко обнаружить как ajaxObj.readyState==4 и ajaxObj.status==200

Случай 2: URL-адрес является недействительным адресом на существующем сервере

  • Пример: http://xyz.google.com где сервер отвечает, но это неверный запрос
  • Это приводит к ajaxObj.readyState==4 и ajaxObj.status==0

Случай 3: URL-адрес к несуществующему IP-адресу сервера

  • Пример: http://192.168.8.6 в моей локальной сети, где нечего ответить
  • Это приводит к ajaxObj.readyState==4 и ajaxObj.status==0

Случай 4: URL-адрес является допустимым адресом, на котором НЕ установлено значение access-control-allow-origin

  • Пример: http://192.168.8.247 где у меня есть сервер без Access-Control-Allow-Origin: * установить в шапке
  • Это приводит к ajaxObj.readyState==4 и ajaxObj.status==0

Проблема заключается в следующем: как различить вариант 4 (ошибка контроля доступа и разрешения источника) и варианты 2 и 3?

В случае 4 консоль отладки Chrome показывает ошибку:

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

Как сделать эту ошибку известной в Javascript?

Я пытался найти какое-то указание в ajaxObj но, похоже, ничто не отличается от случая 2 и 3.

Вот простой тест, который я использовал:

<!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>

Результаты с использованием 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

5 ответов

Нет, в отличие от W3C Spec, невозможно определить разницу.

Вот как спецификация CORS определяет простую процедуру запроса перекрестного происхождения:

Примените шаги создания запроса и соблюдайте приведенные ниже правила запроса при выполнении запроса.

Если флаг ручного перенаправления не установлен, а ответ имеет код состояния HTTP 301, 302, 303, 307 или 308. Примените шаги перенаправления.

Если конечный пользователь отменяет запрос: примените шаги отмены.

Если есть ошибка сети: В случае ошибок DNS, сбоя согласования TLS или других типов сетевых ошибок примените шаги ошибки сети. Не запрашивайте никакого взаимодействия с конечным пользователем...

В противном случае: выполните проверку совместного использования ресурсов. Если это возвращает неудачу, примените шаги ошибки сети...

В случае сбоя сетевого соединения или неудачного обмена CORS применяются шаги по ошибкам в сети, так что буквально нет возможности провести различие между этими двумя случаями.

Зачем? Одним из преимуществ является то, что он не позволяет злоумышленнику проверить топологию сети в локальной сети. Например, скрипт вредоносной веб-страницы может найти IP-адрес вашего маршрутизатора, запросив его HTTP-интерфейс, и, следовательно, узнать несколько вещей о топологии вашей сети (например, насколько велик ваш частный IP-блок, /8 или же /16). Поскольку ваш маршрутизатор не отправляет (или не должен) отправлять заголовки CORS, сценарий абсолютно ничего не изучает.

Может быть, в случае, если это кому-нибудь поможет... еще один способ справиться с разницей между cors и сетевыми ошибками... может работать с chrome или firefox... (хотя и не идеальное решение)

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
}

Чтобы отличить нарушение CORS от других неудачных запросов AJAX, вы можете проверить заголовки ответа на запрос HEAD, используя код на стороне сервера, и передать результаты обратно на свою страницу клиента. Например, если запрос AJAX не выполняется (состояние 0), вы можете вызвать этот скрипт (давайте назовем его cors.php) и точно знать, содержат ли заголовки ответа Access-Control-* заголовки.

Примеры:

cors.php URL = HTTP: //ip.jsontest.com
cors.php URL = HTTP: //www.google.com
cors.php URL = HTTP: //10.0.0.1

возвращается

HTTP / 1.1 200 OK Access-Control-Allow-Origin: *
HTTP / 1.1 302 найдено
Неверный запрос


cors.php - настроить по мере необходимости

<?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 */

Испытательная система на стороне клиента:

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 */
}

Жгут проводов:

// 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());
});

Результаты, достижения:

http://ip.jsontest.com - ОК

http://google.com - нет заголовка Access-Control-Allow-Origin

http://10.0.0.1 - не существует или истекло время ожидания

Удивительно, но вы можете сделать это для изображений (и, возможно, есть аналогичное решение для других конкретных типов контента). Хитрость заключается в том, что, очевидно, не относится к CORS, поэтому утверждение "URL-адрес не загружается с помощью XHR && url успешно загружается с помощью img src" подразумевает, что URL-адрес работает, но CORS заблокирован.

Это определенно помогает не во всех случаях, но в некоторых случаях (например, обнаружение утилит, если вы правильно передаете CORS), это может помочь. Например, я хотел выдать предупреждение в консоли, если мое приложение (отдельный интерфейс и бэкэнд) установлено с правильно настроенным URL-адресом бэкэнда и неправильно настроенным CORS на сервере, так что этого мне было вполне достаточно, поскольку я могу предоставить изображение для его тестирования на.

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. Я создал страницу probe.html для обслуживания в iframe (в удаленном источнике вы пытаетесь использовать CORS для связи)
  2. Я регистрирую обработчик window.onmessage на своей странице
  3. Я загружаю страницу probe.html на свою страницу, используя безопасный iframe
  4. На моей странице события iframe onload я отправляю сообщение в iframe с помощью window.postMessage()
  5. Страница probe.html проверяет URL-адрес (из того же источника внутри iframe)
  6. Страница probe.html отправляет мне подробности ответа xhr.status и xhr.statusText с помощью window.postMessage()
  7. Если статус является ошибкой, я регистрирую статус и statusText в нашей службе регистрации

Тем не менее, имейте в виду, что это очень трудно сделать безопасным, если передача параметров в любом случае (любой может встроить ваш iframe, другие стороны могут вызвать postMessage на страницу или iframe).

И он не идеален (он обнаруживает только ошибки, которые не являются одноразовыми).

Тем не менее, хотя это большая работа, вы можете получить больше информации о причине ошибки. Очень полезно, когда вы не можете получить прямой доступ к браузеру своих пользователей.

PS: это полностью отличается от ответа PHP, который предполагает, что ваш сервер должен общаться с удаленным сервером (а), который не очень полезен для диагностики причин сбоя связи, и (б) использование curl из PHP просто просит ваш сервер быть серьезно забитым!

Другие вопросы по тегам