Перехват и изменение ответов с помощью Chrome

Puppeteer - это абстракция высокого уровня над протоколом Chrome Devtools, которая дает вам удобный API для управления средами на основе Chromium (или Blink). Разработчики создают высокоуровневые абстракции, такие как Puppeteer, с намерением сделать обычные варианты использования тривиальными, и по мере того, как вы все дальше и дальше отклоняетесь от этих общих случаев, не маловероятно, что вам придется прыгать мимо этих абстракций. К счастью, вы все еще можете получить доступ к протоколу Chrome Devtools непосредственно в Puppeteer, чтобы получить лучшее из обоих миров.

Перехват и изменение ответов

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

Вы можете найти введение в перехват и изменение ресурсов с помощью протокола Chrome Devtools Protocol (CDP) в блоге Shape Security или посетите YouTube, чтобы найти версию, которая использует концепции Puppeteer.

Базовый сценарий кукловода

Это базовый сценарий кукловода, с которого мы начинаем. Это дает нам браузер и страницу по умолчанию, которая загружается при загрузке Chrome или Chromium.

Использование протокола Devtools с Puppeteer

Первый шаг - заставить Puppeteer запустить сеанс CDP с целевой страницей. Взаимодействие с CDP происходит через веб-сокеты, и вам нужно будет создать это соединение для каждой вкладки или «страницы» в лексиконе Puppeteer. Важно отметить, что соединение сохраняется даже после перехода на другой веб-сайт, пока вкладка остается открытой.

const client = await page.target().createCDPSession();

Инициализация сеанса CDP на всех вкладках

Не знаю, как у вас, но у меня постоянно открыто как минимум 30 вкладок - Gmail и музыка или календарь постоянно прикреплены к передней части, 15 в середине для сайтов, которые я открывал в прошлом, которые я никогда не буду смотреть снова, но возможно, чтобы вкладки оставались открытыми. 18-я вкладка - это недавний поиск Google, 19–29 - новые вкладки, открытые из этого поиска Google или его результатов, а на 30-й и далее я открываю новые сайты.

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

const page = (await browser.pages())[0];
browser.on('targetcreated', async (target) => {
  const page = await target.page();
});

Объединив то, что мы узнали до сих пор, мы можем создать базу, которая позволяет нам получать сеанс CDP для каждой страницы, которую мы открываем.

Пример использования CDP: перехват трафика

Протокол Chrome Devtools Protocol богат функциями, но, безусловно, единственное, что я делаю снова и снова, - это манипулирую ресурсами на лету. Может быть, я просто неплохо печатаю JavaScript, чтобы отлаживать без специального форматирования, но возможность перехватывать и изменять ответы - уже давно изношенный инструмент в моем наборе инструментов.

Для этого нам нужно указать Chrome, какие URL-адреса мы хотим перехватить, с помощью шаблона URL, типа ресурса и стадии, на которой мы хотим перехватить. Шаблон URL-адреса - это глобальный шаблон для сопоставления URL-адресов, а тип ресурса - это то, как Chrome намеревается использовать этот ресурс. Если вы откроете файл JavaScript непосредственно в новой вкладке, вы можете быть удивлены, обнаружив, что он обрабатывается как документ, тогда как если вы связываете ресурс через элемент <script>, Chrome выполняет ресурс как скрипт. Разница имеет смысл, но не является обычным нюансом, с которым приходится сталкиваться изо дня в день.

Этап перехвата важен в нашем случае, потому что мы хотим перехватить фактический ответ от сервера, поэтому мы перехватываем на этапе «HeadersReceived». Это позволяет нам проверять заголовки, определять, нужно ли нам тело, а затем при необходимости запрашивать тело и манипулировать им.

await client.send('Network.enable');
await client.send('Network.setRequestInterception', { patterns: [
  { 
    urlPattern: '*', 
    resourceType: 'Script', 
    interceptionStage: 'HeadersReceived' 
  }
]});

Многие домены CDP требуют, чтобы вы сначала их включили. Строго говоря, то, что мы делаем здесь, все еще работает без этой строки, но я встречал случаи, когда другие домены не работали, поэтому я рекомендую это для будущей совместимости, особенно потому, что методы, которые мы используем, являются экспериментальными и могут быть изменены.

После настройки нашего перехвата мы можем прослушивать событие Network.requestIntercepted, чтобы подключиться к трафику. Одним из свойств этого объекта события является interceptionId, который позволяет нам получить тело перехвата, а затем продолжить или прервать перехваченный запрос с помощью Network.continueInterceptedRequest.

client.on('Network.requestIntercepted', ({ interceptionId }) => {
  client.send('Network.continueInterceptedRequest', {
    interceptionId,
  });
});

Получение тела ответа

Поскольку мы перехватили ответ на этапе HeadersReceived, у нас нет доступного основного содержимого, и нам нужно позвонить в Network.getResponseBodyForInterception с нашим идентификатором перехвата, чтобы получить его.

const response = await client.send('Network.getResponseBodyForInterception',{
  interceptionId 
});

Этот объект ответа имеет два свойства: body и base64Encoded. base64Encoded - это логическое значение, обозначающее, находится ли body в сырой или закодированной форме.

const originalBody = response.base64Encoded ? atob(response.body) : response.body;

Доставка измененного ответа

Для доставки измененного ответа необходимо создать полный необработанный HTTP-ответ и отправить его вместе с идентификатором перехвата. Это означает, что вам нужны код ответа HTTP, версия, заголовки, возврат каретки + разделители новой строки (\r\n) и пустая строка между заголовками и телом. Этот необработанный ответ необходимо перекодировать в base64.

const httpResponse = [
  'HTTP/1.1 200 OK',
  'Date: ' + (new Date()).toUTCString(),
  'Connection: closed',
  'Content-Length: ' + newBody.length,
  'Content-Type: application/javascript',
  '', // Do not delete
  newBody
].join('\r\n');
client.send('Network.continueInterceptedRequest', {
  interceptionId,
  rawResponse: btoa(httpResponse)
});

Завершая все это

Ниже приведен полный сценарий, который перехватывает каждый сценарий с каждой вкладки и пропускает контент через prettier, инструмент форматирования исходного кода.

Ознакомьтесь с другими доступными доменами и методами через протокол Chrome Devtools. Освоение кукловода и CDP может превратить обычные рутинные дела в автоматизированные сценарии и позволит автоматизировать создание сред разработки, специфичных для определенных сценариев.

Если у вас есть какие-либо вопросы, напишите мне в комментариях или через Twitter по адресу @jsoverson. Спасибо!