Как обнаружить ошибку перекрестного происхождения (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();
}
Вот что я сделал - хотя для этого нужно внести изменения на удаленном сервере (добавьте страницу к нему).
- Я создал страницу probe.html для обслуживания в iframe (в удаленном источнике вы пытаетесь использовать CORS для связи)
- Я регистрирую обработчик window.onmessage на своей странице
- Я загружаю страницу probe.html на свою страницу, используя безопасный iframe
- На моей странице события iframe onload я отправляю сообщение в iframe с помощью window.postMessage()
- Страница probe.html проверяет URL-адрес (из того же источника внутри iframe)
- Страница probe.html отправляет мне подробности ответа xhr.status и xhr.statusText с помощью window.postMessage()
- Если статус является ошибкой, я регистрирую статус и statusText в нашей службе регистрации
Тем не менее, имейте в виду, что это очень трудно сделать безопасным, если передача параметров в любом случае (любой может встроить ваш iframe, другие стороны могут вызвать postMessage на страницу или iframe).
И он не идеален (он обнаруживает только ошибки, которые не являются одноразовыми).
Тем не менее, хотя это большая работа, вы можете получить больше информации о причине ошибки. Очень полезно, когда вы не можете получить прямой доступ к браузеру своих пользователей.
PS: это полностью отличается от ответа PHP, который предполагает, что ваш сервер должен общаться с удаленным сервером (а), который не очень полезен для диагностики причин сбоя связи, и (б) использование curl из PHP просто просит ваш сервер быть серьезно забитым!