Настраиваем взаимодействие с внутренним API приложения через свой API-proxy
Что делать, если хочется повзаимодействовать с приложением-мессенджером, но его издатель такой опции в виде 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 позволяет максимально упростить его использование для собственных приложений-клиентов.