Как выполнить проверку подлинности веб-сайтов, не прошедших проверку подлинности, в ответ на недавние изменения частного API?

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

Многие из нас обратились к частному веб-API Instagram, чтобы реализовать функции, которые у нас были ранее. Один выдающийся ping/instagram_private_api позволяет перестроить большую часть прежней функциональности, однако, с публично объявленными изменениями на этой неделе, Instagram также внес основные изменения в свой частный API, требуя магических переменных, пользовательских агентов и хеширования MD5, чтобы сделать просмотр веб-страниц запросы возможны. Это можно увидеть по последним выпускам в ранее связанном git-репозитории, а точные изменения, необходимые для продолжения извлечения данных, можно увидеть здесь.

Эти изменения включают в себя:

  • Сохранение агента пользователя и токена CSRF между запросами.
  • Первоначальный запрос к https://instagram.com/ для rhx_gis магического ключа rhx_gis из тела ответа.
  • Установка заголовка X-Instagram-GIS, который формируется путем магической конкатенации ключа rhx_gis и переменных запроса перед передачей их через хеш MD5.

Все, что меньше этого, приведет к ошибке 403. Эти изменения были успешно реализованы в вышеупомянутом репозитории, однако моя попытка в JS продолжает терпеть неудачу. В приведенном ниже коде я пытаюсь получить первые 9 сообщений с временной шкалы пользователя. Параметры запроса, которые определяют это:

  • query_hash из 42323d64886122307be10013ad2dcc44 (получить носитель с временной шкалы пользователя).
  • variables.id любого идентификатора пользователя в виде строки (пользователь, из которого нужно получить медиафайл).
  • variables.first, количество сообщений, которые нужно извлечь, в виде целого числа.

Ранее этот запрос мог быть выполнен без каких-либо вышеуказанных изменений, просто получая с https://www.instagram.com/graphql/query/?query_hash=42323d64886122307be10013ad2dcc44&variables=%7B%22id%22%3A%225380311726%22%2C%22first%22%3A1%7D, так как URL был незащищенным.

Тем не менее, моя попытка реализовать функциональность для успешного написания в вышеупомянутом хранилище не работает, и я получаю только 403 ответа из Instagram. Я использую суперагент в качестве библиотеки запросов в среде узла.

/*
** Retrieve an arbitrary cookie value by a given key.
*/
const getCookieValueFromKey = function(key, cookies) {
        const cookie = cookies.find(c => c.indexOf(key) !== -1);
        if (!cookie) {
            throw new Error('No key found.');
        }
        return (RegExp(key + '=(.*?);', 'g').exec(cookie))[1];
    };

/*
** Calculate the value of the X-Instagram-GIS header by md5 hashing together the rhx_gis variable and the query variables for the request.
*/
const generateRequestSignature = function(rhxGis, queryVariables) {
    return crypto.createHash('md5').update('${rhxGis}:${queryVariables}', 'utf8').digest("hex");
};

/*
** Begin
*/
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5';

// Make an initial request to get the rhx_gis string
const initResponse = await superagent.get('https://www.instagram.com/');
const rhxGis = (RegExp('"rhx_gis":"([a-f0-9]{32})"', 'g')).exec(initResponse.text)[1];

const csrfTokenCookie = getCookieValueFromKey('csrftoken', initResponse.header['set-cookie']);

const queryVariables = JSON.stringify({
    id: "123456789",
    first: 9
});

const signature = generateRequestSignature(rhxGis, queryVariables);

const res = await superagent.get('https://www.instagram.com/graphql/query/')
    .query({
        query_hash: '42323d64886122307be10013ad2dcc44',
        variables: queryVariables
    })
    .set({
        'User-Agent': userAgent,
        'X-Instagram-GIS': signature,
        'Cookie': 'rur=FRC;csrftoken=${csrfTokenCookie};ig_pr=1'
    }));

Что еще я должен попробовать? Что делает мой код неудачным, и предоставленный код в репозитории выше работает нормально?

Обновление (2018-04-17)

Как минимум 3 раза в неделю Instagram снова обновляет свой API. Для изменения больше не требуется, чтобы токен CSRF являлся частью хешированной подписи.

Вопрос выше был обновлен, чтобы отразить это.

Обновление (2018-04-14)

Instagram снова обновил свой частный API GraphQl. Насколько каждый может понять:

  • Пользовательский агент больше не нужно включать в расчет X-Instagram-Gis md5.

Вопрос выше был обновлен, чтобы отразить это.

Ответ 1

Значения для сохранения

Вы не сохраняете агент пользователя (требование) в первом запросе Instagram:

const initResponse = await superagent.get('https://www.instagram.com/');

Должно быть:

const initResponse = await superagent.get('https://www.instagram.com/')
                     .set('User-Agent', userAgent);

Это должно сохраняться в каждом запросе вместе с cookie csrftoken.

Генерация заголовков X-Instagram-GIS

Как показывает ваш ответ, вы должны сгенерировать заголовок X-Instagram-GIS из двух свойств, значения rhx_gis которые находятся в вашем первоначальном запросе, и переменных запроса в следующем запросе. Они должны быть хешированы md5, как показано в приведенной выше функции:

const generateRequestSignature = function(rhxGis, queryVariables) {
    return crypto.createHash('md5').update('${rhxGis}:${queryVariables}', 'utf8').digest("hex");
};

Ответ 2

Поэтому, чтобы вызвать запрос instagram, вам нужно создать заголовок x-instagram-gis.

Чтобы сгенерировать этот заголовок, вам нужно вычислить хеш md5 следующей строки "{rhx_gis}: {path}". Значение rhx_gis хранится в исходном коде страницы instagram в переменной global.ssh.sharedData.

Пример:
Если вы попытаетесь получить запрос информации о пользователе, например, https://www.instagram.com/ {имя_пользователя}/? __ a = 1
Вам нужно добавить http-заголовок x-instagram-gis, чтобы запросить, какое значение
MD5("{rhx_gis}:/{username}/")

Это проверено и работает на 100%, поэтому не стесняйтесь спрашивать, что-то пошло не так.

Ответ 3

Uhm... У меня нет Узел, установленного на моей машине, поэтому я не могу точно убедиться, но похоже, что вам не хватает важной части параметров в querystring, то есть поле after:

const queryVariables = JSON.stringify({
    id: "123456789",
    first: 4,
    after: "YOUR_END_CURSOR"
});

Из этих queryVariables зависит ваш хэш MD5, который затем не соответствует ожидаемому. Попробуйте: я ожидаю, что это сработает.

РЕДАКТИРОВАТЬ:

Внимательно прочитайте свой код, к сожалению, он не имеет особого смысла. Я делаю вывод, что вы пытаетесь получить полный поток изображений из пользовательского фида.

Затем вам не нужно начинать домашнюю страницу Instagram (superagent.get('https://www.instagram.com/')), а пользовательский поток (superagent.get('https://www.instagram.com/your_user')).

Остерегайтесь: вам нужно жестко закодировать тот же самый пользовательский агент, который вы собираетесь использовать ниже (и это не похоже на то, что вы...).

Затем вам нужно извлечь идентификатор запроса (он не жестко закодирован, он меняется каждые несколько часов, иногда минут, hardcoding - глупо, однако для этого POC вы можете сохранить его жестко запрограммированным) и end_cursor. Для конечного курсора я бы пошел на что-то вроде этого:

const endCursor = (RegExp('end_cursor":"([^"]*)"', 'g')).exec(initResponse.text)[1];

Теперь у вас есть все необходимое, чтобы сделать второй запрос:

const queryVariables = JSON.stringify({
    id: "123456789",
    first: 9,
    after: endCursor
});

const signature = generateRequestSignature(rhxGis, csrfTokenCookie, queryVariables);

const res = await superagent.get('https://www.instagram.com/graphql/query/')
    .query({
        query_hash: '42323d64886122307be10013ad2dcc44',
        variables: queryVariables
    })
    .set({
        'User-Agent': userAgent,
        'Accept': '*/*',
        'Accept-Language': 'en-US',
        'Accept-Encoding': 'gzip, deflate',
        'Connection': 'close',
        'X-Instagram-GIS': signature,
        'Cookie': 'rur=${rurCookie};csrftoken=${csrfTokenCookie};mid=${midCookie};ig_pr=1'
    }).send();

Ответ 4

query_hash не является постоянным и со временем меняется.

Например, скрипты ProfilePage включали эти скрипты:

https://www.instagram.com/static/bundles/base/ConsumerCommons.js/9e645e0f38c3.js https://www.instagram.com/static/bundles/base/Consumer.js/1c9217689868.js

Хэш находится в одном из приведенных выше сценариев, например, для edge_followed_by:

const res = await fetch(scriptUrl, { credentials: 'include' });
const rawBody = await res.text();
const body = rawBody.slice(0, rawBody.lastIndexOf('edge_followed_by'));
const hashes = body.match(/"\w{32}"/g);
// hashes[hashes.length - 2]; = edge_followed_by
// hashes[hashes.length - 1]; = edge_follow

Ответ 5

Проверьте мое последнее рабочее решение здесь fooobar.com/info/48451/... Работает на стороне клиента без какого-либо программирования на стороне сервера