AMP

CORS в AMP

Многие компоненты и расширения AMP используют удаленные конечные точки с помощью запросов Cross-Origin Resource Sharing («совместное использование ресурсов между разными источниками», CORS). В этом документе описываются ключевые аспекты применения CORS в AMP. Чтобы подробнее узнать о самом механизме CORS, обратитесь к спецификации CORS на сайте W3.

Зачем применять CORS в пределах моего собственного источника?

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

Компоненты AMP, которые получают динамические данные (например, amp-form, amp-list и т. п.), отправляют запросы CORS к удаленным конечным точкам для получения данных. Если ваша AMP-страница содержит такие компоненты, вам необходимо предусмотреть обработку CORS, чтобы эти запросы не завершались ошибкой.

Проиллюстрируем это на примере.

Допустим, у вас есть AMP-страница, на которой перечислены продукты с ценами. Чтобы обновить цены на странице, пользователь нажимает кнопку, которая извлекает актуальные цены из конечной точки JSON (с помощью компонента amp-list). JSON-данные находятся в вашем домене.

Ну вот, страница находится в моем домене, а JSON — тоже в моем домене. Не вижу проблем!

Да, но как пользователь попал на вашу AMP-страницу? Что, если он имеет дело с кешированной страницей? Вполне вероятно, что пользователь не открыл вашу AMP-страницу напрямую, а нашел ее через другую платформу. Например, Поиск Google использует Google AMP Cache для быстрого рендеринга AMP-страниц; кешированные страницы, которые выдаются из Google AMP Cache, загружаются со стороннего домена. Когда пользователь нажимает кнопку, чтобы обновить цены на вашей странице, кешированная AMP-страница отправляет запрос на ваш исходный домен для получения цен, что является несоответствием между источниками (кеш -> исходный домен). Чтобы сделать возможными подобные запросы, вам необходимо предусмотреть обработку CORS; в противном случае запрос не будет выполнен.


Хорошо, что мне делать?

  1. Для AMP-страниц, загружающих динамические данные, — убедитесь, что вы протестировали кешированную версию страницы; не ограничивайтесь тестированием на своем собственном домене (см. раздел Тестирование CORS в AMP ниже).
  2. Следуйте приведенным в данном документе инструкциям по обработке CORS-запросов и CORS-ответов.

Большинство компонентов AMP, которые используют CORS-запросы, либо автоматически устанавливают режим учетных данных, либо позволяют автору при желании включить его. Например, компонент amp-list получает динамическое содержимое из конечной точки CORS JSON и позволяет автору устанавливать режим учетных данных с помощью атрибута credentials.

Пример: включение персонализированного контента в amp-list с помощью файлов cookie

<amp-list
credentials="include"
src="<%host%>/json/product.json?clientId=CLIENT_ID(myCookieId)"

>   <template type="amp-mustache">

    Your personal offer: ${{price}}
  </template>
</amp-list>

Указывая режим учетных данных, источник может включать cookie в CORS-запрос и также устанавливать cookie в ответе (с учетом ограничений для сторонних файлов cookie).

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

Безопасность CORS в AMP

Чтобы обеспечить корректность и безопасность запросов с ваших AMP-страниц и ответов на них, вы должны:

  1. Проверять запросы.
  2. Отправлять соответствующие заголовки в ответах.

Если вы используете в своем бэкенде Node, вы можете использовать промежуточное ПО AMP CORS, которое является частью AMP Toolbox.

Проверка CORS-запросов

Когда ваша конечная точка получает CORS-запрос:

  1. Убедитесь, что заголовок CORS-запроса Origin указывает на разрешенный источник (домен издателя + AMP-кеши).
  2. Если заголовок Origin отсутствует, убедитесь, что источник запроса не отличается от домена его получения (с помощью AMP-Same-Origin).

1) Разрешайте запросы с определенных источников CORS

Конечные точки CORS получают источник запроса посредством HTTP-заголовка Origin. Конечные точки должны разрешать запросы только от: (1) собственного источника издателя; и (2) всех источников cacheDomain, указанных в файле https://cdn.ampproject.org/caches.json.

Например, конечные точки должны разрешать запросы от:

  • Поддомена Google AMP Cache: https://<домен издателя>.cdn.ampproject.org
    (например, https://nytimes-com.cdn.ampproject.org)

Сведения о форматах URL AMP-кешей можно получить по следующим ссылкам:

2) Разрешайте запросы в пределах одного источника

Если в запросах в рамках одного источника отсутствует заголовок Origin, AMP устанавливает специальный заголовок:

AMP-Same-Origin: true

Этот специальный заголовок отправляется средой выполнения AMP, когда запрос XHR не выходит за рамки одного источника (то есть если он выполняется из некешированного документа). Запросы, в которых есть заголовок AMP-Same-Origin:true, следует разрешать.

Отправка заголовков CORS-ответа

Отправляемый после проверки CORS-запроса HTTP-ответ должен содержать следующие заголовки:

Access-Control-Allow-Origin: <origin>

Этот заголовок является требованием спецификации CORS от W3, где origin обозначает источник запроса, который был разрешен через заголовок CORS-запроса Origin (например, "https://<publisher's subdomain>.cdn.ampproject.org").

Хотя спецификация CORS от W3 позволяет возвращать в ответе значение *, для повышения безопасности следует выполнять следующее:

  • Если заголовок Origin присутствует, проверьте корректность значения заголовка Origin, после чего продублируйте его в ответе.

Обработка запросов, меняющих состояние

Выполните эти проверки перед обработкой запроса. Данная процедура помогает обеспечить защиту от атак CSRF и позволяет избежать обработки запросов из ненадежных источников.

Перед обработкой запросов, которые могут изменить состояние вашей системы (например, когда пользователь подписывается на список рассылки или отписывается от него), проверьте следующее:

Если установлен заголовок Origin:

  1. Остановитесь и верните ответ с ошибкой, если источник не соответствует одному из следующих значений:
  • <publisher's domain>.cdn.ampproject.org
  • источник издателя («ваш источник»)

где * обозначает совпадение с подстановочным знаком, а не сам символ звездочки (*).

  1. В противном случае обработайте запрос.

Если заголовок Origin НЕ установлен:

  1. Убедитесь, что запрос содержит заголовок AMP-Same-Origin: true. Если запрос не содержит этого заголовка, остановитесь и верните ответ с ошибкой.
  2. В противном случае обработайте запрос.

Примеры обработки CORS-запросов и CORS-ответов

Есть два сценария поступления CORS-запросов к вашей конечной точке:

  1. Протестируйте запрос из того же источника
  2. Запрос из кешированного источника (из AMP-кеша).

Давайте рассмотрим эти сценарии на примере. В нашем примере мы управляем сайтом example.com, на котором размещена AMP-страница под названием article-amp.html. AMP-страница содержит список amp-list для извлечения динамических данных из файла data.json, который также размещен на example.com. Мы хотим обрабатывать запросы к нашему файлу data.json, поступающие с нашей AMP-страницы. Эти запросы могут поступать с AMP-страницы из того же источника (без кеширования) или с AMP-страницы из другого источника (из кеша).


Разрешенные источники

Основываясь на том, что мы знаем о CORS и AMP (из раздела Проверка CORS-запросов выше), в нашем примере мы разрешим запросы из следующих доменов:

  • example.com --- домен издателя
  • example-com.cdn.ampproject.org --- поддомен Google AMP Cache

Заголовки ответов на разрешенные запросы

Наш ответ на запросы из разрешенных источников будет содержать следующие заголовки:

Access-Control-Allow-Origin: <origin>

Вот дополнительные заголовки ответа, которые мы можем включить в наш CORS-ответ:

Access-Control-Allow-Credentials: true
Content-Type: application/json
Access-Control-Max-Age: <delta-seconds>
Cache-Control: private, no-cache

Псевдологика для работы с CORS

Наша логика обработки CORS-запросов и CORS-ответов может быть упрощена до следующего псевдокода:

IF CORS header present
IF origin IN allowed-origins
allow request & send response
ELSE
deny request
ELSE
IF "AMP-Same-Origin: true"
allow request & send response
ELSE
deny request

Пример кода CORS

Ниже представлен пример функции JavaScript, которую мы могли бы использовать для обработки CORS-запросов и CORS-ответов:

function assertCors(req, res, opt_validMethods, opt_exposeHeaders) {
var unauthorized = 'Unauthorized Request';
var origin;
var allowedOrigins = [
'https://example.com',
'https://example-com.cdn.ampproject.org',
'https://cdn.ampproject.org',
];
var allowedSourceOrigin = 'https://example.com'; //publisher's origin
// If same origin
if (req.headers['amp-same-origin'] == 'true') {
origin = sourceOrigin;
// If allowed CORS origin & allowed source origin
} else if (
allowedOrigins.indexOf(req.headers.origin) != -1 &&
sourceOrigin == allowedSourceOrigin
) {
origin = req.headers.origin;
} else {
res.statusCode = 403;
res.end(JSON.stringify({message: unauthorized}));
throw unauthorized;
}

res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', origin);
}

Примечание. Образец рабочего кода можно увидеть в файле amp-cors.js.

Сценарий 1. GET-запрос с AMP-страницы из того же источника

В следующем сценарии страница article-amp.html запрашивает файл data.json; источники не отличаются.


Если мы изучим запрос, то обнаружим следующее:

Request URL: https://example.com/data.json
Request Method: GET
AMP-Same-Origin: true

Поскольку этот запрос исходит из того же источника, заголовок Origin отсутствует, но присутствует специальный заголовок запроса AMP-Same-Origin: true. Мы можем разрешить этот запрос, поскольку он получен из того же источника ( https://example.com).

Заголовки нашего ответа будут такими:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com

Сценарий 2: GET-запрос с кешированной AMP-страницы

В следующем сценарии страница article-amp.html, кешированная в Google AMP Cache, запрашивает файл data.json; источники различаются.


Изучив этот запрос, мы обнаружим следующее:

Request URL: https://example.com/data.json
Request Method: GET
Origin: https://example-com.cdn.ampproject.org

Поскольку этот запрос содержит заголовок Origin, мы можем удостовериться, что он поступил из разрешенного источника, и, следовательно, разрешить его.

Заголовки нашего ответа будут такими:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example-com.cdn.ampproject.org

Работа с кешированными шрифтами

Google AMP Cache кеширует документы AMP HTML, изображения и шрифты для оптимизации скорости AMP-страницы. Хотя это желаемый результат, нам также следует предпринимать меры по обеспечению безопасности кешированных ресурсов. Мы вносим изменения в механизм выдачи кешированных ресурсов из AMP-кеша (как правило, это шрифты), так чтобы он использовал полученное из источника значение Access-Control-Allow-Origin.

Предыдущее поведение (до октября 2019 г.)

Когда AMP-страница загружала https://example.com/some/font.ttf из атрибута @font-face src, AMP-кеш кешировал файл шрифта и выдавал его, как показано ниже (заголовок Access-Control-Allow-Origin имеет значение символа подстановки).

  • URL https://example-com.cdn.ampproject.org/r/s/example.com/some/font.tff
  • Access-Control-Allow-Origin: *

Новое поведение (октябрь 2019 г. и позже)

Предыдущая реализация являлась пермиссивной, что могло приводить к непредвиденному использованию шрифтов со сторонних источников. Благодаря внесенным изменениям AMP-кеш теперь отвечает тем же значением Access-Control-Allow-Origin, что и исходный сервер. Чтобы правильно загружать шрифты из кешированного документа AMP, вам нужно принимать AMP-кеш в качестве одного из допустимых источников в заголовке.

Пример реализации:

function assertFontCors(req, res, opt_validMethods, opt_exposeHeaders) {
var unauthorized = 'Unauthorized Request';
var allowedOrigins = [
'https://example.com',
'https://example-com.cdn.ampproject.org',
];
// If allowed CORS origin
if (allowedOrigins.indexOf(req.headers.origin) != -1) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
} else {
res.statusCode = 403;
res.end(JSON.stringify({message: unauthorized}));
throw unauthorized;
}
}

Например, если вы хотите загрузить /some/font.ttf в https://example.com/amp.html, ответ исходного сервера должен содержать заголовок Access-Control-Allow-Origin, как показано ниже.


Если вы не против, чтобы файл вашего шрифта был доступен из любого источника, вы можете отвечать с подстановочным значением заголовка Access-Control-Allow-Origin; AMP-кеш продублирует это значение — то есть будет отвечать с заголовком Access-Control-Allow-Origin: *. Если вы уже используете этот параметр, ничего менять не нужно.

Мы планируем внести это изменение примерно в середине октября 2019 года. Предлагаем всем издателям AMP-контента, хранящим шрифты на собственном сервере, проверить, не повлияло ли на них данное обновление.

План внедрения

  • 2019-09-30: более точный контроль над списком доменов, к которым будет применяться это изменение. Данная сборка должна выйти в течение этой недели.
  • 2019-10-07: возможность использования тестовых доменов для ручного тестирования.
  • 2019-10-14: общее внедрение функциональности (возможны изменения в зависимости от результатов тестирования).

Следите за соответствующей задачей здесь.

Тестирование CORS в AMP

Когда вы тестируете свои AMP-страницы, не забывайте также тестировать их кешированные версии.

Проверяйте страницу через URL-адрес кеша

Чтобы обеспечить правильное отображение и работу кэшированной AMP-страницы:

  1. В браузере откройте URL-адрес, который будет использовать AMP-кеш для доступа к вашей AMP-странице. Формат URL-адреса кеша можно определить с помощью этого инструмента из курса AMP By Example.

Например:

  • URL: https://amp.dev/documentation/guides-and-tutorials/start/create/
  • Формат URL AMP-кеша: https://www-ampproject-org.cdn.ampproject.org/c/s/www.ampproject.org/docs/tutorials/create.html
  1. Откройте инструменты разработчика в браузере и убедитесь, что ошибок нет и все ресурсы загружены корректно.

Проверяйте заголовки ответа сервера

Вы можете использовать команду curl, чтобы убедиться, что ваш сервер отправляет правильные заголовки HTTP-ответа. В команде curl укажите URL запроса и все специальные заголовки, которые хотите добавить.

Синтаксис: curl <request-url> -H <custom-header> - I

Протестируйте запрос из того же источника

К запросам из того же источника система AMP добавляет специальный заголовок AMP-Same-Origin:true.

Вот наша команда curl для тестирования запроса из https://ampbyexample.com к файлу examples.json (в том же домене):

curl 'https://amp.dev/static/samples/json/examples.json' -H 'AMP-Same-Origin: true' -I

Результаты выполнения команды содержат правильные заголовки ответа (примечание: ненужная информация убрана):

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://ampbyexample.com
access-control-allow-methods: POST, GET, OPTIONS

Протестируйте запрос с кешированной AMP-страницы

В CORS-запросах с отличающегося домена (т. е. из кеша) заголовок origin является частью запроса.

Вот наша команда curl для тестирования запроса с кешированной в Google AMP Cache AMP-страницы к файлу examples.json:

curl 'https://amp.dev/static/samples/json/examples.json' -H 'origin: https://ampbyexample-com.cdn.ampproject.org' -I

Результаты выполнения команды содержат правильные заголовки ответа:

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://ampbyexample-com.cdn.ampproject.org
access-control-allow-methods: POST, GET, OPTIONS