network draft
|
@ -445,6 +445,23 @@ function upload(file) {
|
|||
</script>
|
||||
```
|
||||
|
||||
## Cross-origin requests
|
||||
|
||||
`XMLHttpRequest` can make cross-domain requests, using the same CORS policy as `fetch`.
|
||||
|
||||
Just like `fetch`, it doesn't send cookies and HTTP-authorization to another origin by default. To enable them, set `xhr.withCredentials` to `true`:
|
||||
|
||||
```js
|
||||
let xhr = new XMLHttpRequest();
|
||||
*!*
|
||||
xhr.withCredentials = true;
|
||||
*/!*
|
||||
s
|
||||
xhr.open('POST', 'http://anywhere.com/request');
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Typical code of the GET-request with `XMLHttpRequest`:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# Cross-Origin Fetch
|
||||
# Cross-Origin Requests
|
||||
|
||||
If we make a `fetch` from an arbitrary web-site, that will probably fail.
|
||||
|
||||
The core concept here is *origin* -- a domain/port/protocol triplet.
|
||||
|
||||
Cross-origin requests -- those sent to another domain or protocol or port -- require special headers from the remote side.
|
||||
Cross-origin requests -- those sent to another domain or protocol or port -- require special headers from the remote side. That policy is called "CORS": Cross-Origin Resource Sharing.
|
||||
|
||||
For instance, let's try fetching from `http://example.com`:
|
||||
|
||||
|
@ -20,373 +20,294 @@ Fetch fails, as expected.
|
|||
|
||||
## Why?
|
||||
|
||||
Cross-origin requests are subject to the special safety control with the sole purpose to protect the internet from evil hackers.
|
||||
Because cross-origin restrictions protect the internet from evil hackers.
|
||||
|
||||
Seriously. Let's make a very brief historical digression.
|
||||
|
||||
For many years Javascript was unable to perform network requests.
|
||||
For many years Javascript did not have any special methods to perform network requests.
|
||||
|
||||
The main way to send a request to another site was an HTML `<form>` with either `POST` or `GET` method. People submitted it to `<iframe>`, just to stay on the current page.
|
||||
One way to communicate with another server was to submit a `<form>` there. People submitted it into `<iframe>`, just to stay on the current page, like this:
|
||||
|
||||
```html
|
||||
<form target="iframe" method="POST" action="http://site.com">
|
||||
<!-- javascript could dynamically generate and submit this kind of form -->
|
||||
<iframe name="iframe"></iframe>
|
||||
|
||||
<form target="iframe" method="POST" action="http://another.com/…">
|
||||
<!-- a form could be dynamically generated and submited by Javascript -->
|
||||
</form>
|
||||
|
||||
<iframe name="iframe"></iframe>
|
||||
```
|
||||
|
||||
So, it *was* possible to make a request. But if the submission was to another site, then the main window was forbidden to access `<iframe>` content. Hence, it wasn't possible to get a response.
|
||||
- So, it was possible to make a GET/POST request to another site.
|
||||
- But as it's forbidden to access the content of an `<iframe>` from another site, it wasn't possible to read the response.
|
||||
|
||||
2. Open another site in `<iframe>`.
|
||||
**A script from one site could not access the content of another site.**
|
||||
|
||||
That simple, yet powerful rule was a foundation of the internet security. E.g. a script from the page `hacker.com` could not access user's mailbox at `gmail.com`. People felt safe.
|
||||
|
||||
But growing appetites of web developers demanded more power. A variety of tricks were invented.
|
||||
|
||||
One of them was to use a `<script src="http://another.com/…">` tag. It could request a script from anywhere, but again -- it was forbidden to access its content (if from another site).
|
||||
|
||||
So if `another.com` was going to send back the data, it would be wrapped in a callback function with the name known to the receiving side. Usually, an url parameter, such as `?callback=…`, was used to specify the name.
|
||||
|
||||
For instance:
|
||||
1. We declare a global function, e.g. `gotWeather`.
|
||||
1. Our page is requesting `http://another.com/weather.json?callback=gotWeather`.
|
||||
2. The remote server dynamically generates a script wrapping the response data into `gotWeather(...)`. As `gotWeather` is our global function, it handles the response: we have the data.
|
||||
|
||||
|
||||
Here's the demo:
|
||||
|
||||
For many years cross-domain requests were simply unavailable. To access another
|
||||
```js run
|
||||
function gotWeather({ temperature, humidity }) {
|
||||
alert(`temperature: ${temperature}, humidity: ${humidity}`);
|
||||
}
|
||||
|
||||
The internet got used to it, people got used to it.
|
||||
let script = document.createElement('script');
|
||||
script.src = `http://cors.javascript.local/article/fetch-crossorigin/demo/script?callback=gotWeather`;
|
||||
document.body.append(script);
|
||||
```
|
||||
|
||||
The response looks like this:
|
||||
|
||||
```js
|
||||
gotWeather({
|
||||
temperature: 25,
|
||||
humidity: 78
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Imagine that `fetch` works from anywhere.
|
||||
That works, and doesn't violate security, because both sides agreed to pass the data this way. And, when both sides agree, it's definitely not a hack.
|
||||
|
||||
How an evil hacker could use it?
|
||||
Then `XMLHttpRequest` appeared, and `fetch` afterwards. After long discussions, it was decided that cross-domain requests would be possible, but in a way that does not add any capabilities unless explicitly allowed by the server.
|
||||
|
||||
## Simple requests
|
||||
|
||||
They would create a page at `http://evil.com`, and when a user comes to it, then run `fetch` from his mail server, e.g. `http://gmail.com/messages`.
|
||||
|
||||
Such a request usually sends authentication cookies, so `http://gmail.com` would recognize the user and send back the messages. Then the hacker could analyze the mail and go with online-banking, and so on.
|
||||
|
||||

|
||||
|
||||
How `gmail.com` and other sites protect users from such hacks now?
|
||||
|
||||
That's simple -- when a request is made from they add an additional secret value to all requests.
|
||||
The protection is quite simple.
|
||||
How `gmail.com`
|
||||
|
||||
Right, because o
|
||||
|
||||
Because of cross-origin restrictions such a hack is impossible. They prevent an evil-minded person from doing anything that they couldn't do before.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Кросс-доменные запросы проходят специальный контроль безопасности, цель которого -- не дать злым хакерам(tm) завоевать интернет.
|
||||
|
||||
Серьёзно. Разработчики стандарта предусмотрели все заслоны, чтобы "злой хакер" не смог, воспользовавшись новым стандартом, сделать что-то принципиально отличное от того, что и так мог раньше и, таким образом, "сломать" какой-нибудь сервер, работающий по-старому стандарту и не ожидающий ничего принципиально нового.
|
||||
|
||||
Давайте, на минуточку, вообразим, что появился стандарт, который даёт, без ограничений, возможность делать любой странице HTTP-запросы куда угодно, какие угодно.
|
||||
|
||||
Как сможет этим воспользоваться злой хакер?
|
||||
|
||||
Он сделает свой сайт, например `http://evilhacker.com` и заманит туда посетителя (а может посетитель попадёт на "злонамеренную" страницу и по ошибке -- не так важно).
|
||||
|
||||
Когда посетитель зайдёт на `http://evilhacker.com`, он автоматически запустит JS-скрипт на странице. Этот скрипт сделает HTTP-запрос на почтовый сервер, к примеру, `http://gmail.com`. А ведь обычно HTTP-запросы идут с куками посетителя и другими авторизующими заголовками.
|
||||
|
||||
Поэтому хакер сможет написать на `http://evilhacker.com` код, который, сделав GET-запрос на `http://gmail.com`, получит информацию из почтового ящика посетителя. Проанализирует её, сделает ещё пачку POST-запросов для отправки писем от имени посетителя. Затем настанет очередь онлайн-банка и так далее.
|
||||
|
||||
Спецификация [CORS](http://www.w3.org/TR/cors/) налагает специальные ограничения на запросы, которые призваны не допустить подобного апокалипсиса.
|
||||
|
||||
Запросы в ней делятся на два вида.
|
||||
|
||||
[Простыми](http://www.w3.org/TR/cors/#terminology) считаются запросы, если они удовлетворяют следующим двум условиям:
|
||||
|
||||
1. [Простой метод](http://www.w3.org/TR/cors/#simple-method): GET, POST или HEAD
|
||||
2. [Простые заголовки](http://www.w3.org/TR/cors/#simple-header) -- только из списка:
|
||||
[Simple requests](http://www.w3.org/TR/cors/#terminology) must satisfy the following conditions:
|
||||
|
||||
1. [Simple method](http://www.w3.org/TR/cors/#simple-method): GET, POST or HEAD
|
||||
2. [Simple headers](http://www.w3.org/TR/cors/#simple-header) -- only allowed:
|
||||
- `Accept`
|
||||
- `Accept-Language`
|
||||
- `Content-Language`
|
||||
- `Content-Type` со значением `application/x-www-form-urlencoded`, `multipart/form-data` или `text/plain`.
|
||||
- `Content-Type` with the value `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`.
|
||||
|
||||
"Непростыми" считаются все остальные, например, запрос с методом `PUT` или с заголовком `Authorization` не подходит под ограничения выше.
|
||||
Any other request is considered "non-simple". For instance, a request with `PUT` method or with an `Authorization` HTTP-header does not fit the limitations.
|
||||
|
||||
Принципиальная разница между ними заключается в том, что "простой" запрос можно сформировать и отправить на сервер и без XMLHttpRequest, например при помощи HTML-формы.
|
||||
**The essential difference is that a "simple request" can be made with a `<form>` or a `<script>`, without any special methods.**
|
||||
|
||||
То есть, злой хакер на странице `http://evilhacker.com` и до появления CORS мог отправить произвольный GET-запрос куда угодно. Например, если создать и добавить в документ элемент `<script src="любой url">`, то браузер сделает GET-запрос на этот URL.
|
||||
So, even a very old server should be ready to accept a simple request.
|
||||
|
||||
Аналогично, злой хакер и ранее мог на своей странице объявить и, при помощи JavaScript, отправить HTML-форму с методом GET/POST и кодировкой `multipart/form-data`. А значит, даже старый сервер наверняка предусматривает возможность таких атак и умеет от них защищаться.
|
||||
Contrary to that, headers with non-standard headers or e.g. `DELETE` method can't be created this way. For a long time Javascript was unable to do such requests. So an old server may assume that such requests come from a privileged source, "because a webpage is unable to send them".
|
||||
|
||||
А вот запросы с нестандартными заголовками или с методом `DELETE` таким образом не создать. Поэтому старый сервер может быть к ним не готов. Или, к примеру, он может полагать, что такие запросы веб-страница в принципе не умеет присылать, значит они пришли из привилегированного приложения, и дать им слишком много прав.
|
||||
That's why we try to make a non-simple request, the browser sends a special "preflight" request that asks the server -- does it agree to accept such cross-origin requests, or not?
|
||||
|
||||
Поэтому при посылке "непростых" запросов нужно специальным образом спросить у сервера, согласен ли он в принципе на подобные кросс-доменные запросы или нет? И, если сервер не ответит, что согласен -- значит, нет.
|
||||
And, unless the server explicitly confirms that with headers, a non-simple request is not sent.
|
||||
|
||||
```summary
|
||||
В спецификации CORS, как мы увидим далее, есть много деталей, но все они объединены единым принципом: новые возможности доступны только с явного согласия сервера (по умолчанию -- нет).
|
||||
```
|
||||
Now we'll go into details. All of them serve a single purpose -- to ensure that new cross-origin capabilities are only accessible with an explicit permission from the server.
|
||||
|
||||
## CORS для простых запросов
|
||||
## CORS for simple requests
|
||||
|
||||
В кросс-доменный запрос браузер автоматически добавляет заголовок `Origin`, содержащий домен, с которого осуществлён запрос.
|
||||
A browser always adds to cross-origin requests header `Origin`, with the origin of the request.
|
||||
|
||||
В случае запроса на `http://anywhere.com/request` с `http://javascript.ru/page` заголовки будут примерно такие:
|
||||
For instance, if we request `https://anywhere.com/request` from `https://javascript.info/page`, the headers will be like:
|
||||
|
||||
```
|
||||
GET /request
|
||||
Host:anywhere.com
|
||||
*!*
|
||||
Origin:http://javascript.ru
|
||||
Origin:https://javascript.info
|
||||
*/!*
|
||||
...
|
||||
```
|
||||
|
||||
Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к себе.
|
||||
Please note: `Origin` has no path, only domain/protocol/port.
|
||||
|
||||
Если сервер разрешает кросс-доменный запрос с этого домена -- он должен добавить к ответу заголовок `Access-Control-Allow-Origin`, содержащий домен запроса (в данном случае "javascript.ru") или звёздочку `*`.
|
||||
The server can inspect the `Origin` and, if it agrees to accept such a request, add a special header `Access-Control-Allow-Origin` to the response. That header should contain the allowed origin (in our case `https://javascript.info`), or a star `*`.
|
||||
|
||||
**Только при наличии такого заголовка в ответе -- браузер сочтёт запрос успешным, а иначе JavaScript получит ошибку.**
|
||||
The browser plays the role of a trusted mediator here:
|
||||
1. It ensures that the corrent `Origin` is sent in any cross-domain request.
|
||||
2. If checks for correct `Access-Control-Allow-Origin` in the response, grants Javascript access if it's so, forbids otherwise.
|
||||
|
||||

|
||||
|
||||
То есть, ответ сервера может быть примерно таким:
|
||||
|
||||
Here's an example of an "accepting" response:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
*/!*
|
||||
```
|
||||
|
||||
Если `Access-Control-Allow-Origin` нет, то браузер считает, что разрешение не получено, и завершает запрос с ошибкой.
|
||||
## Response headers
|
||||
|
||||
При таких запросах не передаются куки и заголовки HTTP-авторизации. Параметры `user` и `password` в методе `open` игнорируются. Мы рассмотрим, как разрешить их передачу, чуть далее.
|
||||
For cross-origin request, by default Javascript may only access "simple response headers":
|
||||
|
||||
```warn header="Что может сделать хакер, используя такие запросы?"
|
||||
Описанные выше ограничения приводят к тому, что запрос полностью безопасен.
|
||||
- `Cache-Control`
|
||||
- `Content-Language`
|
||||
- `Content-Type`
|
||||
- `Expires`
|
||||
- `Last-Modified`
|
||||
- `Pragma`
|
||||
|
||||
Действительно, злая страница может сформировать любой GET/POST-запрос и отправить его, но без разрешения сервера ответа она не получит.
|
||||
Any other header is forbidden.
|
||||
|
||||
А без ответа такой запрос, по сути, эквивалентен отправке формы GET/POST, причём без авторизации.
|
||||
```smart
|
||||
Please note: there's no `Content-Length` header in the list! So, if we're downloading something, we can't track the percentage of progress.
|
||||
```
|
||||
|
||||
## Ограничения IE9-
|
||||
To grant Javascript access to any other header, the server must list it in the `Access-Control-Expose-Headers` header.
|
||||
|
||||
В IE9- используется `XDomainRequest`, который представляет собой урезанный `XMLHttpRequest`.
|
||||
|
||||
На него действуют ограничения:
|
||||
|
||||
- Протокол нужно сохранять: запросы допустимы с HTTP на HTTP, с HTTPS на HTTPS. Другие протоколы запрещены.
|
||||
- Метод `open(method, url)` имеет только два параметра. Он всегда асинхронный.
|
||||
- Ряд возможностей современного стандарта недоступны, в частности:
|
||||
- Недоступны методы, кроме GET или POST.
|
||||
- Нельзя добавлять свои заголовки, даже нельзя указать свой `Content-Type` для запроса, он всегда `text/plain`.
|
||||
- Нельзя включить передачу кук и данных HTTP-авторизации.
|
||||
- В IE8 в режиме просмотра InPrivate кросс-доменные запросы не работают.
|
||||
|
||||
Современный стандарт [XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/) предусматривает средства для преодоления этих ограничений, но на момент выхода IE8 они ещё не были проработаны, поэтому их не реализовали. А IE9 исправил некоторые ошибки, но в общем не добавил ничего нового.
|
||||
|
||||
Поэтому на сайтах, которые хотят поддерживать IE9-, то на практике кросс-доменные запросы редко используют, предпочитая другие способы кросс-доменной коммуникации. Например, динамически создаваемый тег `SCRIPT` или вспомогательный `IFRAME` с другого домена. Мы разберём эти подходы в последующих главах.
|
||||
|
||||
```smart header="Как разрешить кросс-доменные запросы от доверенного сайта в IE9-?"
|
||||
Разрешить кросс-доменные запросы для "доверенных" сайтов можно в настройках IE, во вкладке "Безопасность", включив пункт "Доступ к источникам данных за пределами домена".
|
||||
|
||||
Обычно это делается для зоны "Надёжные узлы", после чего в неё вносится доверенный сайт. Теперь он может делать кросс-доменные запросы `XMLHttpRequest`.
|
||||
|
||||
Этот способ можно применить для корпоративных сайтов, а также в тех случаях, когда посетитель заведомо вам доверяет, но почему-то (компьютер на работе, админ запрещает ставить другой браузер?) хочет использовать именно IE. Например, он может предлагаться в качестве дополнительной инструкции "как заставить этот сервис работать под IE".
|
||||
```
|
||||
|
||||
```smart header="В IE разрешён другой порт"
|
||||
В кросс-доменные ограничения IE не включён порт.
|
||||
|
||||
То есть, можно сделать запрос с `http://javascript.ru` на `http://javascript.ru:8080`, и в IE он не будет считаться кросс-доменным.
|
||||
|
||||
Это позволяет решить некоторые задачи, связанные с взаимодействием различных сервисов в рамках одного сайта. Но только для IE.
|
||||
```
|
||||
|
||||
Расширенные возможности, описанные далее, поддерживаются всеми современными браузерами, кроме IE9-.
|
||||
|
||||
## Заголовки ответа
|
||||
|
||||
Чтобы JavaScript мог прочитать HTTP-заголовок ответа, сервер должен указать его имя в `Access-Control-Expose-Headers`.
|
||||
|
||||
Например:
|
||||
For example:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
Content-Length: 12345
|
||||
Authorization: 2c9de507f2c54aa1
|
||||
*!*
|
||||
X-Uid: 123
|
||||
X-Authorization: 2c9de507f2c54aa1
|
||||
Access-Control-Expose-Headers: X-Uid, X-Authentication
|
||||
Access-Control-Expose-Headers: Content-Length, Authorization
|
||||
*/!*
|
||||
```
|
||||
|
||||
По умолчанию скрипт может прочитать из ответа только "простые" заголовки:
|
||||
With such `Access-Control-Expose-Headers` header, the script is allowed to access `Content-Length` and `Authorization` headers of the response.
|
||||
|
||||
```
|
||||
Cache-Control
|
||||
Content-Language
|
||||
Content-Type
|
||||
Expires
|
||||
Last-Modified
|
||||
Pragma
|
||||
```
|
||||
|
||||
...То есть, `Content-Type` получить всегда можно, а доступ к специфическим заголовкам нужно открывать явно.
|
||||
## "Non-simple" requests
|
||||
|
||||
## Запросы от имени пользователя
|
||||
We can use any HTTP-method: not just `GET/POST`, but also `PATCH`, `DELETE` and others.
|
||||
|
||||
По умолчанию браузер не передаёт с запросом куки и авторизующие заголовки.
|
||||
Some time ago no one could even assume that a webpage is able to do such requests. So there may exist webservices that treat a non-standard method as a signal: "That's not a browser". They can take it into account when checking access rights.
|
||||
|
||||
Чтобы браузер передал вместе с запросом куки и HTTP-авторизацию, нужно поставить запросу `xhr.withCredentials = true`:
|
||||
So, to avoid misunderstandings, any "non-simple" request -- that couldn't be done in the old times, the browser does not make such requests right away. Before it sends a preliminary, so-called "preflight" request, asking for permission.
|
||||
|
||||
```js
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.withCredentials = true;
|
||||
A preflight request uses method `OPTIONS` and has no body:
|
||||
- `Access-Control-Request-Method` header has the requested method.
|
||||
- `Access-Control-Request-Headers` header provides a comma-separated list of non-simple HTTP-headers.
|
||||
|
||||
xhr.open('POST', 'http://anywhere.com/request', true)
|
||||
...
|
||||
```
|
||||
If the server agrees to serve the requests, then it should respond with status 200, no body and:
|
||||
|
||||
Далее -- всё как обычно, дополнительных действий со стороны клиента не требуется.
|
||||
|
||||
Такой `XMLHttpRequest` с куками, естественно, требует от сервера больше разрешений, чем "анонимный".
|
||||
|
||||
**Поэтому для запросов с `withCredentials` предусмотрено дополнительное подтверждение со стороны сервера.**
|
||||
|
||||
При запросе с `withCredentials` сервер должен вернуть уже не один, а два заголовка:
|
||||
|
||||
- `Access-Control-Allow-Origin: домен`
|
||||
- `Access-Control-Allow-Credentials: true`
|
||||
|
||||
Пример заголовков:
|
||||
|
||||
```js no-beautify
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
Access-Control-Allow-Credentials: true
|
||||
*/!*
|
||||
```
|
||||
|
||||
Использование звёздочки `*` в `Access-Control-Allow-Origin` при этом запрещено.
|
||||
|
||||
Если этих заголовков не будет, то браузер не даст JavaScript'у доступ к ответу сервера.
|
||||
|
||||
## "Непростые" запросы
|
||||
|
||||
В кросс-доменном `XMLHttpRequest` можно указать не только `GET/POST`, но и любой другой метод, например `PUT`, `DELETE`.
|
||||
|
||||
Когда-то никто и не думал, что страница сможет сделать такие запросы. Поэтому ряд веб-сервисов написаны в предположении, что "если метод -- нестандартный, то это не браузер". Некоторые веб-сервисы даже учитывают это при проверке прав доступа.
|
||||
|
||||
Чтобы пресечь любые недопонимания, браузер использует предзапрос в случаях, когда:
|
||||
|
||||
- Если метод -- не GET / POST / HEAD.
|
||||
- Если заголовок `Content-Type` имеет значение отличное от `application/x-www-form-urlencoded`, `multipart/form-data` или `text/plain`, например `application/xml`.
|
||||
- Если устанавливаются другие HTTP-заголовки, кроме `Accept`, `Accept-Language`, `Content-Language`.
|
||||
|
||||
...Любое из условий выше ведёт к тому, что браузер сделает два HTTP-запроса.
|
||||
|
||||
Первый запрос называется "предзапрос" (английский термин "preflight"). Браузер делает его целиком по своей инициативе, из JavaScript мы о нём ничего не знаем, хотя можем увидеть в инструментах разработчика.
|
||||
|
||||
Этот запрос использует метод `OPTIONS`. Он не содержит тела и содержит название желаемого метода в заголовке `Access-Control-Request-Method`, а если добавлены особые заголовки, то и их тоже -- в `Access-Control-Request-Headers`.
|
||||
|
||||
Его задача -- спросить сервер, разрешает ли он использовать выбранный метод и заголовки.
|
||||
|
||||
На этот запрос сервер должен ответить статусом 200, без тела ответа, указав заголовки `Access-Control-Allow-Method: метод` и, при необходимости, `Access-Control-Allow-Headers: разрешённые заголовки`.
|
||||
|
||||
Дополнительно он может указать `Access-Control-Max-Age: sec`, где `sec` -- количество секунд, на которые нужно закэшировать разрешение. Тогда при последующих вызовах метода браузер уже не будет делать предзапрос.
|
||||
- The response header `Access-Control-Allow-Method` must have the allowed method.
|
||||
- The response header `Access-Control-Allow-Headers` must have a list of allowed headers.
|
||||
- Additionally, the header `Access-Control-Max-Age` may specify a number of seconds to cache the permissions. So the browser won't have to send a preflight for subsequent requests that satisfy given permissions.
|
||||
|
||||

|
||||
|
||||
Давайте рассмотрим предзапрос на конкретном примере.
|
||||
|
||||
### Пример запроса COPY
|
||||
|
||||
Рассмотрим запрос `COPY`, который используется в протоколе [WebDAV](http://www.webdav.org/specs/rfc2518.html) для управления файлами через HTTP:
|
||||
Let's see how it works step-by-step for a cross-domain `PATCH` request:
|
||||
|
||||
```js
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('COPY', 'http://site.com/~ilya', true);
|
||||
xhr.setRequestHeader('Destination', 'http://site.com/~ilya.bak');
|
||||
|
||||
xhr.onload = ...
|
||||
xhr.onerror = ...
|
||||
|
||||
xhr.send();
|
||||
let response = await fetch('https://site.com/service.json', {
|
||||
method: 'PATCH', // this method is often used to update data
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'API-Key': 'secret'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Этот запрос "непростой" по двум причинам (достаточно было бы одной из них):
|
||||
There are three reasons why the request is not simple (one is enough):
|
||||
- Method `PATCH`
|
||||
- `Content-Type` is not one of: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`.
|
||||
- Custom `API-Key` header.
|
||||
|
||||
1. Метод `COPY`.
|
||||
2. Заголовок `Destination`.
|
||||
### Step 1 (preflight request)
|
||||
|
||||
Поэтому браузер, по своей инициативе, шлёт предварительный запрос `OPTIONS`:
|
||||
The browser, on its own, sends a preflight request that looks like this:
|
||||
|
||||
```
|
||||
OPTIONS /~ilya HTTP/1.1
|
||||
OPTIONS /service.json
|
||||
Host: site.com
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
Accept-Encoding: gzip,deflate
|
||||
Connection: keep-alive
|
||||
*!*
|
||||
Origin: http://javascript.ru
|
||||
Access-Control-Request-Method: COPY
|
||||
Access-Control-Request-Headers: Destination
|
||||
*/!*
|
||||
Origin: https://javascript.info
|
||||
Access-Control-Request-Method: PATCH
|
||||
Access-Control-Request-Headers: Content-Type,API-Key
|
||||
```
|
||||
|
||||
Обратим внимание на детали:
|
||||
- Method: `OPTIONS`.
|
||||
- The path -- exactly the same as the main request: `/service.json`.
|
||||
- Cross-origin special headers:
|
||||
- `Origin` -- the source origin.
|
||||
- `Access-Control-Request-Method` -- requested method.
|
||||
- `Access-Control-Request-Headers` -- a comma-separated list of "non-simple" headers.
|
||||
|
||||
- Адрес -- тот же, что и у основного запроса: `http://site.com/~ilya`.
|
||||
- Стандартные заголовки запроса `Accept`, `Accept-Encoding`, `Connection` присутствуют.
|
||||
- Кросс-доменные специальные заголовки запроса:
|
||||
- `Origin` -- домен, с которого сделан запрос.
|
||||
- `Access-Control-Request-Method` -- желаемый метод.
|
||||
- `Access-Control-Request-Headers` -- желаемый "непростой" заголовок.
|
||||
### Step 2 (preflight response)
|
||||
|
||||
На этот запрос сервер должен ответить статусом 200, указав заголовки `Access-Control-Allow-Method: COPY` и `Access-Control-Allow-Headers: Destination`.
|
||||
The server can respond with status 200 and headers:
|
||||
- `Access-Control-Allow-Method: PATCH`
|
||||
- `Access-Control-Allow-Headers: Content-Type,API-Key`.
|
||||
|
||||
Но в протоколе WebDav разрешены многие методы и заголовки, которые имеет смысл сразу перечислить в ответе:
|
||||
...But if it expects other methods and headers, makes sense to list them all at once:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain
|
||||
*!*Access-Control-Allow-Methods*/!*: PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST
|
||||
*!*Access-Control-Allow-Headers*/!*: Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control
|
||||
*!*Access-Control-Max-Age*/!*: 86400
|
||||
200 OK
|
||||
Access-Control-Allow-Methods: PUT,PATCH,DELETE
|
||||
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
|
||||
Access-Control-Max-Age: 86400
|
||||
```
|
||||
|
||||
Ответ должен быть без тела, то есть только заголовки.
|
||||
Now the browser can see that `PATCH` is an allowed method, and both headers are in the list too, so it sends the main request.
|
||||
|
||||
Браузер видит, что метод `COPY` -- в числе разрешённых и заголовок `Destination` -- тоже, и дальше он шлёт уже основной запрос.
|
||||
Besides, the preflight response is cached for 86400 seconds (one day), so subsequent requests will not cause a preflight. Assuming that they fit the allowances, they will be sent directly, without `OPTIONS`.
|
||||
|
||||
При этом ответ на предзапрос он закэширует на 86400 сек (сутки), так что последующие аналогичные вызовы сразу отправят основной запрос, без `OPTIONS`.
|
||||
### Step 3 (actual request)
|
||||
|
||||
Основной запрос браузер выполняет уже в "обычном" кросс-доменном режиме:
|
||||
When the preflight is successful, the browser now makes the real request.
|
||||
|
||||
It has `Origin` header (because it's cross-origin):
|
||||
|
||||
```
|
||||
COPY /~ilya HTTP/1.1
|
||||
PATCH /service.json
|
||||
Host: site.com
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
*!*
|
||||
Destination: http://site.com/~ilya.bak
|
||||
Origin: http://javascript.ru
|
||||
*/!*
|
||||
Content-Type: application/json
|
||||
API-Key: secret
|
||||
Origin: https://javascript.info
|
||||
```
|
||||
|
||||
Ответ сервера, согласно спецификации [WebDav COPY](http://www.webdav.org/specs/rfc2518.html#rfc.section.8.8.8), может быть примерно таким:
|
||||
### Step 4 (actual response)
|
||||
|
||||
The server should not forget `Accept-Control-Allow-Origin` in the response. A successful preflight does not relieve from that:
|
||||
|
||||
```
|
||||
HTTP/1.1 207 Multi-Status
|
||||
Content-Type: text/xml; charset="utf-8"
|
||||
Content-Length: ...
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
*/!*
|
||||
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
...
|
||||
</d:multistatus>
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
```
|
||||
|
||||
Так как `Access-Control-Allow-Origin` содержит правильный домен, то браузер вызовет `xhr.onload` и запрос будет завершён.
|
||||
Now it's done. Because of `Access-Control-Max-Age: 86400`, preflight permissions are cached for 86400 seconds (1 day).
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
A cross-origin request by default does not bring any cookies.
|
||||
|
||||
for example, `fetch('http://another.com')` does not send any cookies, even those that belong to `another.com` domain.
|
||||
|
||||
To send cookies, we need to add the option `credentials: "include"`.
|
||||
|
||||
Like this:
|
||||
|
||||
```js
|
||||
fetch('http://another.com', {
|
||||
credentials: "include"
|
||||
});
|
||||
```
|
||||
|
||||
Now `fetch` sends cookies originating from `another.com` with the request.
|
||||
|
||||
A request with credentials is much more powerful than an anonymouse one. Does the server trust Javascript to work on behalf of a user? Then it will access sensitive information, these permissions are not for everyone.
|
||||
|
||||
The server should add a new header `Access-Control-Allow-Credentials: true` to the response, if it allows requests with credentials.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
200 OK
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
Access-Control-Allow-Credentials: true
|
||||
```
|
||||
|
||||
Please note: using a start `*` in `Access-Control-Allow-Origin` is forbidden in such case. The server must explicitly put the trusted origin there. That's an additional safety measure, to ensure that the server really knows who it trusts.
|
||||
|
||||
|
||||
## Итого
|
||||
|
||||
|
|
31
7-network/2-fetch-crossorigin/demo.view/server.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const Koa = require('koa');
|
||||
const app = new Koa();
|
||||
|
||||
const Router = require('koa-router');
|
||||
|
||||
let router = new Router();
|
||||
|
||||
router.get('/script', async (ctx) => {
|
||||
let callback = ctx.query.callback;
|
||||
|
||||
if (!callback) {
|
||||
ctx.throw(400, 'Callback required!');
|
||||
}
|
||||
|
||||
ctx.type = 'application/javascript';
|
||||
ctx.body = `${callback}({
|
||||
temperature: 25,
|
||||
humidity: 78
|
||||
})`;
|
||||
});
|
||||
|
||||
app
|
||||
.use(router.routes())
|
||||
.use(router.allowedMethods());
|
||||
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(app.callback()).listen(8080);
|
||||
} else {
|
||||
exports.accept = app.callback();
|
||||
}
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 131 KiB |
|
@ -16,7 +16,7 @@ let promise = fetch(url, {
|
|||
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
|
||||
referrer: "about:client", // "" for no-referrer, or an url from the current origin
|
||||
referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin...
|
||||
mode: "cors", // same-origin, no-cors, navigate, or websocket
|
||||
mode: "cors", // same-origin, no-cors
|
||||
credentials: "same-origin", // omit, include
|
||||
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
|
||||
redirect: "follow", // manual, error
|
||||
|
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |