Pull to refresh

HTTP MITMProxy — помощник в разработке и тестировании

Level of difficultyEasy
Reading time6 min
Views4.7K

Наверное каждый разработчик и QA-инженер, в рабочем процессе сталкивался с необходимостью подмены отправляемых/принимаемых данных. Когда эта задача касается данных, гуляющих между клиентом и сервером, особых проблем она не приносит. Запрос можно модифицировать и отправить ручками, к примеру через Postman, а для модификации ответа можно использовать инструменты вроде Burp Suite, Charles и т.д., но что делать если целевой запрос отправляется с сервера?

Рассмотрим простую схему процесса оплаты:

  1. Пользователь заполняет платежные данные

  2. Фронт отправляет их на endpoint API

  3. Бэкэнд общается с другими системами по HTTP, выполняет различные действия и отправляет ответ на фронт

  4. Фронт отображает сообщение для клиента

Предположим, перед QA стоит задача протестировать фикс плавающего, крайне редко встречающегося бага, возникающего из-за получения бэкэндом некорректных данных от сторонней системы.

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

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

Рассмотрим простую схему процесса бронирования:

  1. Пользователь заполняет данные о пассажирах

  2. Фронт отправляет их на endpoint API

  3. Бэкэнд запрашивает у поставщика актуальную информацию о рейсе

  4. Далее формирует запрос на бронирование с учетом полученной ранее актуальной информации о рейсе и отправляет ответ на фронт

  5. Фронт отображает сообщение для клиента

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

UPD: После пары коротких дискуссий в коментариях, мне показалось что не все читатели "правильно" понимают описываемую мной проблему. Поэтому решил добавить максимально конкретный и однозначный пример ситуации, в которой был бы полезен инструмент, о котором дальше и пойдет речь.

Реальный пример из жизни

Допустим есть программная система, реализующая полный цикл процессов продажи каких-либо билетов (поиск, бронирование, оплата, обмен, возврат и т.д.). Естественно, эта система берет билеты не из собственной БД. Она интегрирована с множеством различных поставщиков, которые их предлагают.
За продажу билета система берет какую-то динамически определяемую наценку, которая включется в стоимость билета еще на этапе его поиска.

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

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

Допустим программист написал логику, которая реализует все описанные выше требования. Задача уходит на тестирование в QA-отдел. Но т.к. подобная ситуация (изменения цены или условий при актуализации) происходит крайне редко, задача зависает в тестировании надолго. Рано или поздно к разработчику приходит QA-специалист и говорит: "Хоть убей, не могу воспроизвести случай, когда изменятся условия или цена.. Помоги, а?".

И тут разработчик становится перед выбором:
- брать на себя ответственность и давать добро лить правки в прод без апрува от QA отдела, т.к. они этот кейс проверить не могут
- начинает придумывать какие-то костыли на тестовом окружении, для того чтоб имитировать возникновение этой ситуации

Инструмент, о котором дальше пойдет речь, нужен как-раз для того, чтоб разработчик не вставал перед таким выбором. Чтоб он вообще не был вовлечен в процесс тестирования, спокойно продолжал заниматься своими задачами и не отвлекался от работы, а QA могли бы сами, без его помощи, имитировать любые трудно-воспроизводимые ситуации.

Если вы пишете на PHP и вам знакома эта боль, добро пожаловать под кат.


Дальнейшая часть статьи является мануалом к библиотеке https://github.com/Chetkov/http-client-mitmproxy, которую предлагаю в качестве решения описанной выше проблемы.

Библиотека предоставляет набор инструментов для клиентской и серверной стороны: 

  1. Консольный клиент для пользователя

  2. Декоратор над \Psr\Http\Client\ClientInterface, который через канал коммуникации общается с консольным клиентом и дает возможность модифицировать запросы и ответы в интерактивном режиме

Для наглядности я подготовил demo-проект https://github.com/Chetkov/http-client-mitmproxy-example. Сильно изощряться не стал, т.к. считаю что это было бы избыточно, но для демонстрации работы http-client-mitmproxy его вполне достаточно.

В нем доступны:

  1. artisan команда currency-rates:show {code} (возвращает список курсов валют)

  2. endpoint /currency-rates (возвращает список курсов валют)

  3. endpoint /currency-rates/{code} (возвращает курс конкретной валюты)

  4. endpoint /calculate (делает несколько запросов к currency-rates/{code}, получает курсы и рассчитывает суммы в разных валютах, на основе суммы в рублях)

Установка http-client-mitmproxy

composer require v.chetkov/http-client-mitmproxy

Настройка http-client-mitmproxy

Общее

В данный момент, канал коммуникации \Chetkov\HttpClientMitmproxy\Communication\CommunicationChannelInterface между декоратором и пользовательским клиентом, реализован на базе Redis, поэтому на клиентской и серверной стороне, в конфиге необходимо указать идентичные данные для подключения к нему:

<?php

declare(strict_types=1);

return [
   // For redis based communication channels
   'redis' => [
       'host' => 'redis-host',
       'port' => 6379,
       'timeout' => 0,
   ],
];

Настройка бэкэнда

Также, на стороне бэкэнда, для нужного окружения, в случае обнаружения ProxyUID, в качестве реализации \Psr\Http\Client\ClientInterface необходимо использовать декоратор, поставляемый библиотекой:

<?php 

// ...

$this->app->bind(ClientInterface::class, function (Container $container) {
    $client = new Client(['allow_redirects' => true]);


    if ($proxyUid = ProxyUID::detect()) {
        $config = require dirname(__DIR__, 2) . '/config/mitmproxy.config.php';
        $mitmproxyFactory = new DefaultFactory($config);


        $client = $mitmproxyFactory->createHttpClientDecorator($proxyUid, $client);
    }

    return $client;
});

Использование

Запускаем PHP web-сервер

cd http-client-mitmproxy-example
php -S localhost:8000 -t public/

Запускаем консольный клиент

С помощью опций можно задать:

  • --config путь к файлу конфигурации

  • --temp-dir путь к временной директории (должна быть доступна для записи)

  • --app-mode режим работы целевого приложения (cli, web)

  • --format предпочитаемый формат для редактирования данных (yaml, json, php)

  • --editor предпочитаемый редактор (nano, vim, gedit)

В случае отсутствия в списке опций последних трех (app-mode, format, editor), клиент запросит их в интерактивном режиме

Затем будет выведено сообщение с дальнейшими инструкциями.

Для WEB mode:

Для CLI mode:

Запускаем целевое приложение

Следуя предложенным инструкциям выполним artisan команду

export MITM_PROXY_UID=0ff6b1f2a9ebc702ea9b84b0fe019f6b &&
php artisan currency-rates:show USD

И подменим дату в отправляемом к API центробанка запросе на 2010 год

Затем согласимся с редактированием полученного от центробанка ответа

И изменим название доллара США на "Зелёный"

Затем откажемся от продолжения редактирования других полей и всего ответа целиком и увидим сообщение о завершении текущей сессии

Теперь клиент ожидает новых соединений (т.е. процессов, запущенных с его ProxyUID), а запущенная artisan команда думает, что USD на сайте центробанка называется “Зелёный” и его актуальный курс “30”

Думаю, на этом статью можно закончить. Спасибо за внимание.

Если после прочтения у вас возникли вопросы или предложения, я с радостью готов обсудить их в комментариях.

UPD 2: Огромная просьба к читателям, оставляющим свой голос в опросе - пожалуйста, аргументируйте ваш выбор хотя-бы коротким коментарием. Особенно, если вы считаете инструмент бесполезным, расскажите почему. Еще не сталкивались с подобной проблемой? Сталкивались, но решали её подругому?

Only registered users can participate in poll. Log in, please.
Считаете ли вы инструмент полезным?
37.5% Безусловно, полезная штука6
25% Затрудняюсь ответить4
37.5% Абсолютно бесполезная вещь6
16 users voted. 4 users abstained.
Only registered users can participate in poll. Log in, please.
Встречались ли вы с подобной проблемой в вашей работе?
33.33% Да, регулярно4
66.67% Иногда бывало8
0% Нет, никогда0
12 users voted. 4 users abstained.
Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments13

Articles