Что делать, если хочется повзаимодействовать с приложением-мессенджером, но его издатель такой опции в виде API для нас не предусмотрел?

Конечно же стоит попробовать себя в качестве джуниор-минус реверс-инженера - всего лишь на уровне перехвата HTTP-запросов с их последующим воспроизведением.

Зачем и почему?

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

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

Но надежду на простое решение данного вопроса нам дает опция самого приложения "Использовать прокси-сервер".

Используем Charles

С помощью указанной настройки сможем перенаправить трафик приложения в Charles и посмотреть на его общение с сервером

Для этого указываем в приложении прокси-сервер на адрес 127.0.0.1 и порт 8888

Далее идём в Charles и устанавливаем его SSL-сертификат - иначе прочитать защищенные сообщения у нас не выйдет:

После можем включить SSL-проксирование и перезапустить мессенджер:

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

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

Кроме того, у нас появились сведения о содержании тела запроса - помимо логина и пароля серверу нужно передать имя приложения и его id.

В ответ сервер вернул новые логин и пароль - запомним их.

Теперь можем попровать внутри приложения отправить сообщение в чат и получить в Charles новый запрос с опять же очевидным названием /send

При изучении этого запроса на вкладке Authentication уже увидели новые логин и пароль, полученные на этапе регистрации, а не исходные. Также получили данные по оформлению тела запроса от приложения и id конкретного чата.

Теперь можно отправиться проверять гипотезу о порядке взаимодействия с сервером.

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

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

Пишем прослойку для API

Имея все данные на руках, можем написать свой сервис, который будет обеспечивать прием простых HTTP-запросов от наших ботов, их преобразование и направление к реальному API. Регистрация у сервера также находится в зоне ответственности планируемого API proxy.
Сделаем минимальную версию - с отправкой всех входящих сообщений только в один чат, чей id получили ранее через Charles.

Напишем его на python с использованием aiohttp.
Эта библиотека вполне подходит в силу своей простоты - у сервиса, который планируем, будет реализовываться всего один метод на вход.
Также потребуется requests - для разового синхронного запроса.
Для установки зависимостей используем poetry.

Само приложение завернем в Docker через docker-compose.
В файле docker-compose.yml для приложения укажем переменные окружения - логины, пароли и т.д.:

...
environment:
  MSG_USERNAME: "ЛОГИН ПРИЛОЖЕНИЯ"
  MSG_PASSWORD: "ПАРОЛЬ ПРИЛОЖЕНИЯ"
  MSG_CHANNEL: "ID ЧАТА ДЛЯ ОТПРАВКИ СООБЩЕНИЙ"
  MSG_SIZE: "ОГРАНИЧИТЕЛЬ РАЗМЕРА СООБЩЕНИЯ"
  SERVER_NAME: "НАШЕ ИМЯ ПРИЛОЖЕНИЯ"
  SERVER_PORT: "ПОРТ ПРИЛОЖЕНИЯ"
  SERVER_TOKEN: "ТОКЕН ДОСТУПА К НАШЕМУ ПРИЛОЖЕНИЮ"

Чтобы приложению иметь возможность подтягивать вышеуказанные переменные из env создадим в settings.py класс с функцией получения значений из окружения.

Также для удобства создаем файл templates.py - с классами-шаблонами запросов к изначальному API, например:

class RegisterRequest:
    REGISTER_URL = "*****/register"
    REGISTER_HEADERS = {
        "Content-Type": "application/json",
        "Authorization": None
    }
    REGISTER_BODY = {
        "device_name": None,
        "device_id": None
    }

Можем перейти к основному файлу приложения, в котором создадим основной цикл приложения, настроим пути к API - в нашем случае это будет init.py:

from aiohttp import web
from app.config.settings import EnvironmentVariables

def main():
    app = web.Application()
    app.add_routes([web.post("/", ФУНКЦИЯ)])
    
    web.run_app(
        app,
        port=int(EnvironmentVariables.SERVER_PORT.get_env())
    )

Сразу реализуем некоторую защиту сервиса от сторонних посягательств - для этого в aiohttp есть middlewares.
Напишем свой слой защиты - функция будет проверять токен, передаваемый в заголовках:

@web.middleware
async def auth_middleware(request, handler):
    request_token = request.headers.get("Authorization")
    server_token = EnvironmentVariables.SERVER_TOKEN.get_env()
    if (request_token is None) or (request_token != server_token):
        raise web.HTTPUnauthorized()
    response = await handler(request)
    return response

Чтобы она заработала - необходимо добавить middleware в основную функцию:

app = web.Application(middlewares=[auth_middleware])

Добавим приложению возможность при запуске зарегистрироваться у сервера-мессенджера.
Создадим отдельный класс Register, где функция register_chat_api будет выполнять запрос к серверу и создавать переменную с итоговым полем авторизации - оно будет использоваться для отправки сообщения:

class Register:
    def __init__(self, username, password, hostname, hostport):
        ...
        self._chat_api_key = None
        self.register_chat_api()

    def register_chat_api(self):
        register_request = post(self.url, headers=self.headers, json=self.body)
        response = register_request.json()
        chat_username = response["chat_username"]
        chat_password = response["chat_password"]
        self._chat_api_key = self.basic_auth(chat_username, chat_password)

Другой класс будет отвечать за отправку полученного сообщения к реальному API.
Назовем его Sender и при вызове будем передавать экземпляр Register - для получения доступа к ключу авторизации:

class Sender:
    def __init__(self, register, msg_channel, msg_size):
        ...
        self.register = register

    async def send(self, request):
        request_body = await request.json()
        authorization_key = self.register.chat_api_key
        send_template = SendRequest
        url = send_template.SEND_URL
        headers = self.prepare_headers(send_template.SEND_HEADERS, authorization_key)
        body = self.prepare_body(send_template.SEND_BODY, request_body)
        async with ClientSession() as session:
            async with session.post(url, headers=headers, json=body) as send_request:
                response = await send_request.json()
                return web.Response(text="200: Ok")

Используем полученные модули приложения в основной функции и получаем свой API proxy, который будет принимать от наших технических пользователей HTTP-запросы и транслировать их в реальный API.

Останется собрать и запустить контейнер - docker-compose подтянет с помощью poetry необходимые зависимости и запустит приложение на установленном порте.

Полностью посмотреть шаблон такого приложения можно здесь: aiohttp-api-proxy

Вывод

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

А реализация взаимодействия с помощью создания API proxy позволяет максимально упростить его использование для собственных приложений-клиентов.

Комментарии (0)