Якщо ми надішлемо запит fetch на інший веб-сайт, він, ймовірно, завершиться невдало.
Наприклад, спробуймо запросити http://example.com:
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
Запит завершився невдало, як і очікувалося.
Основною концепцією тут є джерело (origin) – триплет домен/порт/протокол.
Запити на інші сайти – надіслані до іншого домену (навіть субдомену), протоколу чи порту – потребують спеціальних заголовків від віддаленої сторони.
Ця політика називається “CORS”: Cross-Origin Resource Sharing (“спільне використання ресурсів між різними джерелами”).
Навіщо потрібен CORS? Коротка історія
CORS існує для захисту інтернету від злих хакерів.
Серйозно. Зробімо дуже короткий історичний екскурс.
Протягом багатьох років скрипт з одного сайту не міг отримати доступ до вмісту іншого сайту.
Це просте, але потужне правило було основою інтернет-безпеки. Наприклад, шкідливий скрипт із веб-сайту hacker.com не міг отримати доступ до поштової скриньки користувача на веб-сайті gmail.com. Люди відчували себе в безпеці.
На той час JavaScript також не мав спеціальних методів для виконання мережевих запитів. Це була іграшкова мова для прикраси веб-сторінки.
Але веб-розробники вимагали більшої потужності. Щоб обійти обмеження та надсилати запити на інші веб-сайти, було винайдено різноманітні прийоми.
Використання форм
Одним із способів зв’язку з іншим сервером було відправлення туди <form>. Люди відправили його в <iframe>, просто щоб залишитися на поточній сторінці, ось так:
<!-- ціль форми -->
<iframe name="iframe"></iframe>
<!-- форма могла бути динамічно згенерована та надіслана за допомогою JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
...
</form>
Таким чином, можна було зробити запит GET/POST на інший сайт, навіть без мережевих методів, оскільки форми можуть надсилати дані будь-куди. Але оскільки доступ до вмісту <iframe> з іншого сайту заборонено, прочитати відповідь було неможливо.
Якщо бути точним, для цього насправді були хитрощі, вони вимагали спеціальних скриптів і в iframe, і на сторінці. Тож зв’язок із iframe був технічно можливим. Зараз немає сенсу вдаватися в подробиці, нехай ці динозаври спочивають з миром.
Використання скриптів
Іншим трюком було використання тегу script. Скрипт міг мати будь-який src з будь-яким доменом, наприклад <script src="http://another.com/…">. Такий скрипт можна виконати з будь-якого веб-сайту.
Якщо веб-сайт, наприклад another.com, мав на меті надавати дані для такого типу доступу, тоді використовувався так званий протокол “JSONP (JSON with padding)”.
Ось як це працювало.
Скажімо, нам на нашому сайті потрібно отримати дані з http://another.com, наприклад погоду:
-
По-перше, ми заздалегідь оголошуємо глобальну функцію для прийому даних, наприклад
gotWeather.// 1. Оголошення функції для обробки даних про погоду function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); } -
Потім ми створюємо тег
<script>ізsrc="http://another.com/weather.json?callback=gotWeather", використовуючи назву нашої функції як URL-параметрcallback.let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); -
Віддалений сервер
another.comдинамічно генерує скрипт, який викликаєgotWeather(...)з даними, які він хоче, щоб ми отримали.// Очікувана відповідь від сервера виглядає так: gotWeather({ temperature: 25, humidity: 78 }); -
Коли віддалений скрипт завантажується та виконується, запускається
gotWeather, і, оскільки це наша функція, ми отримуємо дані.
Це працює та не порушує безпеку, оскільки обидві сторони погодилися передавати дані таким чином. І, коли обидві сторони погоджуються, це точно не хак. Ще існують сервіси, які надають такий доступ, оскільки він працює навіть для дуже старих браузерів.
Через деякий час мережеві методи з’явилися в браузерному JavaScript.
Спочатку запити на інші джерела були заборонені. Але в результаті довгих обговорень було дозволено їх робити, але для будь-яких нових можливостей вимагався явний дозвіл від сервера, що виражений в спеціальних заголовках.
Безпечні запити
Існує два типи запитів на інші джерела:
- Безпечні запити.
- Всі інші.
Безпечні запити зробити простіше, тому почнемо з них.
Запит є безпечним, якщо він задовольняє дві умови:
- Safe method: GET, POST або HEAD
- Safe headers – єдині дозволені спеціальні заголовки:
Accept,Accept-Language,Content-Language,Content-Typeзі значеннямapplication/x-www-form-urlencoded,multipart/form-dataабоtext/plain.
Будь-який інший запит вважається “небезпечним”. Наприклад, запит із методом PUT або з HTTP-заголовком API-Key не відповідає обмеженням.
Суттєвою відмінністю є те, що безпечний запит можна зробити за допомогою <form> або <script> без будь-яких спеціальних методів.
Таким чином, навіть дуже старий сервер повинен бути готовий прийняти безпечний запит.
Навпаки, запити з нестандартними заголовками або наприклад метод DELETE не можна створити таким чином. Довгий час JavaScript не міг виконувати такі запити. Тож старий сервер може вважати, що такі запити надходять із привілейованого джерела, “оскільки веб-сторінка не може їх надіслати”.
Коли ми намагаємося зробити небезпечний запит, веб-переглядач надсилає спеціальний попередній запит “preflight”, який запитує сервер – чи погоджується він приймати такі запити між різними джерелами чи ні?
І, якщо сервер явно не підтвердить це за допомогою заголовків, небезпечний запит не надсилається.
Тепер розберемося в деталях.
CORS для безпечних запитів
Якщо запит відбувається між різними джерелами, браузер завжди додає до нього заголовок Origin.
Наприклад, якщо ми запитуємо https://anywhere.com/request з https://javascript.info/page, заголовки виглядатимуть так:
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
Як бачите, заголовок Origin містить саме джерело (домен/протокол/порт) без шляху.
Сервер може перевірити Origin і, якщо він погоджується прийняти такий запит, додати до відповіді спеціальний заголовок Access-Control-Allow-Origin. Цей заголовок має містити дозволене джерело (у нашому випадку https://javascript.info) або зірочку