Содержание
По работе попалась интересная задача по автоматизации instagram, а именно надо было просто провести розыгрыш. Сервисов для организации этой затеи достаточно, есть даже бесплатные. Но были дополнительные (читай премиум) условия, к тому же мне очень захотелось самому посмотреть что там внутри этой популярной инстаграмы и быть может набраться опыта в построении API :)
Первым делом пошел смотреть что там говорят интернеты. Чтение официальных доков по API instagram дали четко понять, что владельцы не хотят давать доступ к неограниченной автоматизации, можно автоматизировать работу со своим аккаунтом в базовом варианте, но это не подходило под мою задачу, а бизнес вариант API требовал верификациии компании, что естественно мне не подходит. (Может быть уже что-то изменилось ...)
Тогда я пошел смотреть что говорят интернеты по поводу работы с тем API что есть на сайте instagram. Все было радужно и ничего не предвещало проблем. На github были даже проекты на php предоставляющие API для автоматизации вплоть до постинга. Статьи на хабре гласили о легкости автоматизации. Многие из источников были нормальной свежести (пару месяцев, а то и недель). Однако ...
Начинаем снифать (fiddler + waterfox) запросы на сайте instagram и смотреть что там. Понятное дело принимаем куки и устанавливаем их куда следует. Этот этап пропустим.
Далее, надо авторизироваться.
При беглом осмотре стало понятно - за авторизацию отвечает POST запрос https://www.instagram.com/accounts/login/ajax/
с интерсным набором параметров. Интерес вызывает именно enc_password
- склееная строка, логически разделяемая : и состоящая (судя по всему, на основании этих данных) из:
PWD_INSTAGRAM_BROWSER
)10
)unixtime
1591030811)unixtime
принимает участие в шифровани, так как шифрованная строка при каждом запросе разная.Погуглив, можно найти варианты авторизации без шифрования пароля, однако эти методы на момент написания данного материала уже не работали. На просторах github можно найти вариант шифрования пароля на nodejs. Обнадежил этот вариант, но оказался не рабочим.
После долгих поисков было решено пройтись отладчиком по коллстеку шифрования пароля. Товарищ по одному из проектов подсказал, что возможно на клиенте instagram используется эта библиотека криптографического шифрования. Сравнивая минифицированный код из отладчика (тот еще изврат) и исходный код библиотеки стало ясно, что скорее всего это так. Но решение задачи это не облегчило.
Разбирая стек я понял, что исходные данные кодировались в нужную форму, а затем отправлялись на шифрование дальше в js. Через несколько часов гуляния по стеку с минифицированным ассинхронным js (лютая хрень) я заблудился ... и решил не принимать этот вызов (самоятотельного разбора этого блудняка), а найти альтернативный путь, потому что неизвестно в какой момент владельцам инсты стрельнет в голову сменить алгоритм шифрования.
Прежде чем окончательно забросить идею авторизации в instagram на php, я решил проверить как там обстоят дела с авторизацией на основании сгенерированных данных на j
s. Для этого взял эти данные из перехваченного трафика (можно из консоли где xhr
запросы, но я взял из fiddler
) и попробовал авторизироваться через php
на локальном сервере. Наконец-то получил заветный позитивный ответ об успешной авторизации и токен.
plaintext{"authenticated": true, "user": true, "userId": "30821314326", "oneTapPrompt": true, "status": "ok"}
Перекинул эти данные на хосинг ... получилось авторизироваться. Запустил авторизация с этими данными через 7 дней - авторизация прошла, то есть эти критически важные данные никаким образом не привязаны ни к чему-либо (ни к геолокации, ни к ip
), и перехватив их злоумышленник без труда получит доступ к аккаунту.
Тогда я вспомнил мое недавнее знакомство с nodejs и puppeteer и решил попробовать скинуть этап авторизации на указанные технологии. И получилось :)
jsconst puppeteer = require('puppeteer'); const fs = require("fs"); //************************************************************************** console.log(process.argv); const g_sLogin = process.argv[2]; const g_sCookieFile = g_sLogin+".txt"; const g_sPassword = process.argv[3]; //************************************************************************** const g_sStartUrl = "https://www.instagram.com/direct/inbox/"; const g_sLoginPage = "accounts/login"; //************************************************************************** async function auth() { var browser = await puppeteer.launch({headless: false}); var page = await browser.newPage(); if(fs.existsSync(g_sCookieFile)) { let sCookie = fs.readFileSync(g_sCookieFile, "utf8"); let aCookie = JSON.parse(sCookie); await page.setCookie(...aCookie); } const response = await page.goto(g_sStartUrl); await response; if(response.url().indexOf(g_sLoginPage) != -1) { console.log("login ..."); await page.waitFor('input[name="username"]'); await page.focus('input[name="username"]'); await page.keyboard.type(g_sLogin); await page.focus('input[name="password"]'); await page.keyboard.type(g_sPassword); await page.click('button[type="submit"]'); await new Promise(r => setTimeout(r, 2000)); } await new Promise(r => setTimeout(r, 3000)); let aCookie = await page.cookies(); fs.writeFileSync(g_sCookieFile, JSON.stringify(aCookie)); await browser.close(); return JSON.stringify(aCookie); } //************************************************************************** auth();
Тут все просто, открываем страницу директа, если перенаправляет (другого способа узнать о перенаправлении я не смог подобрать) значит надо авторизироваться. После авторизации немного ждем, забираем куки (там уже есть все что нам надо для автоматической авторизации) и сохраняем в файл. Теперь распарсив эти куки и подсунув в заголовки для отправки на сервер инсты - можно наблюдать успешно пройденную авторизацию. Более того, судя по всему, эти куки никак не привязаны к геолокации непосредственной авторизации, потому что с ними можно гулять по миру и авторизироваться :)
Перехватив один раз заголовки после авторизации, последующую неделю удавалось их спокойно использовать (есесно совместно с куками):
plaintext'sec-fetch-mode: cors' 'x-ig-www-claim: hmac.AR0Ql6c4RLfhpLxeA4h6-XzrIh5PScH_pVUWaRq7kBoqxshH' 'user-agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3882.0 Safari/537.36' 'accept: */*' "referer: https://www.instagram.com/" 'x-requested-with: XMLHttpRequest' 'x-csrftoken: 19ccPDQ5PoxYyNIp5lLH8L8HtGd9HnYH' 'x-ig-app-id: 936619743392459'
Нести на php хостинг node.js затея так себе, есть вариант запускать node.js на локальной машине по расписанию, и пусть данные после отработки автоматически отсылаются на целевой сервер, но это так, мысли в слух.
UPDATE: а можно просто заюзать vps :)
Теперь настало время сборки необходимх данных. Первым делом нужно достать информацию о посте. Она находится в html
ответе на запрос вида https://www.instagram.com/p/shortcode/
, где вместо shortcode
нужно написать символьный идентификатор нужного поста. Незамысловатой регуляркой /(\{\"graphql\".*\}\}\}\})/ui
вытаскиваем json
со страницы и выдергиваем из него нужные данные.
Примерно таким же образом достается инфа о странице пользователя, по адресу https://www.instagram.com/username/
и регулярка /window\._sharedData \= (\{.*\})\;\<\/script\>/ui
А теперь самое интересное :)
Нужно вытащить все комментарии, лайк, подписчиков. Опять идем и анализируем запросы ...
Instagram API
использует graphql (что-то типа свой rust-full со своими приколами). Запрос выглядит следующим образом
plaintexthttps://www.instagram.com/graphql/query/?query_hash=HASH&variables={...}
где:
query_hash
- это хэш запрашиваемых данных:d5d763b1e2acf209d62d22d184488e57
bc3296d1ce80a24b1b6e40b1e72903f5
ff260833edf142911047af6024eb634a
variables
- это json
(urlencode
естественно), данные внутри для каждого запроса могут немного отличаться, однако, в каждом запросе, который запрашивает массив данных и предполагает постраничную навигацию есть:first
- количество запрашиваемых данных (даже если запросить больше 50 вернется только 50)afte
r - предположительно это хэш записи, от которой отсчитывается ffirst
. after
не нужен в первом запросе, и возвращается в каждом запросе, если есть еще данные для чтения (иначе null)php$hCurl = curl_init(); curl_setopt($hCurl, CURLOPT_HEADER, TRUE); curl_setopt($hCurl, CURLOPT_FOLLOWLOCATION, 2); curl_setopt($hCurl, CURLOPT_REFERER, "https://www.instagram.com/p/$sShortcode/"); curl_setopt($hCurl, CURLOPT_TIMEOUT, 10); curl_setopt($hCurl, CURLOPT_HTTPHEADER, GetArrHeaders()); curl_setopt($hCurl, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($hCurl, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($hCurl, CURLOPT_RETURNTRANSFER, 1); $hasNext = false; $sAfter = null; $iCountLikes = 0; $aLikes = []; do { $aQuery = []; $aQuery["shortcode"] = $sShortcode; $aQuery["include_reel"] = true; $aQuery["first"] = 50; if($sAfter) $aQuery["after"] = $sAfter; $sUrl = "https://www.instagram.com/graphql/query/?query_hash=d5d763b1e2acf209d62d22d184488e57&variables=".urlencode(json_encode($aQuery)); curl_setopt($hCurl, CURLOPT_URL, $sUrl); $sResponse = curl_exec($hCurl); $sResponse = str_replace("\r\n", "\n", $sResponse); $aResponse = explode("\n\n", $sResponse); $aResponse = json_decode($aResponse[1], true); $aLikesInfo = $aResponse["data"]["shortcode_media"]["edge_liked_by"]; foreach($aLikesInfo["edges"] as $aLike) { $aLike = $aLike["node"]; $aLikes[] = [ "id" => $aLike["id"], "username" => $aLike["username"], "fullname" => $aLike["full_name"], "ava" => $aLike["profile_pic_url"], "is_private" => $aLike["is_private"], ]; } $hasNext = $aLikesInfo["page_info"]["has_next_page"]; $sAfter = $aLikesInfo["page_info"]["end_cursor"]; $iCountLikes = $aLikesInfo["count"]; usleep(500 * 1000); } while($hasNext);
Да, usleep обязателен, иначе доступ по этому запросу при частом обращении будет ограничен (даже если попытаться юзать как обычный пользователь через веб версию). Особенно это актуально для больших обьемов данных. Например для сборки ~300 лайков и ~1000 комментариев, такая пауза вполне нормальная, но на сборке ~5000 комментариев мой фейковый акк не раз получал бан, из-за чего пришлось увеличить паузу между запросами до 3-5 секунд. И то в некоторых случаях (видимо все зависело от звезд), инстаграм выдавал бан на этот запрос.
Не трудно увидеть что ответ содержит 3 лишних уровня вложенности (это каждый из перечисленных видов запросов), видимо это издержки graphql
:)
В итоге на организацию коленочного API ушло около 20 часов.
Качеством instagram API
в веб версии сайта я разочарован, не думал что будет так посредственно, вроде на генерации данных для авторизации все закручено под максимум, но потом все спущено и образуется большая дыра в безопасности - увел куки с заголовками и получил доступ.
Instagram заставил попотеть и испытать различные эмоции от использования web-версии API
, но поставленная цель была достигнута в полном обьеме :)