We configure interaction with the internal API of the application through our API proxy

What should we do if we want to interact with a messenger application, but its publisher has not provided such an option for us in the form of an API?

Of course, it’s worth trying yourself as a junior-minus reverse engineer – just at the level of intercepting HTTP requests and then reproducing them.

Why and why?

Let's assume that we have a desktop corporate messenger. And also there was a need for non-humans to send chat messages – bots, monitoring systems, etc.
But the publisher does not provide an open API.

It is logical to assume that desktop applications for ordinary users interact with the provider’s server, including in terms of requests to send messages. You shouldn’t just rely on reading the application’s network traffic – it’s obvious that the traffic will be closed by TLS, etc.

But the option of the application itself gives us hope for a simple solution to this issue”Use a proxy server“.

Using Charles

Using this setting, we can redirect application traffic to Charles and watch its communication with the server

To do this, specify the proxy server in the application at the address 127.0.0.1 and port 8888

Next, go to Charles and install its SSL certificate – otherwise we won’t be able to read protected messages:

Afterwards we can enable SSL proxying and restart the messenger:

As a result, we get a list of addresses that the application contacted at startup, and therefore authorization.
To understand the order of requests, go to the Sequence tab and look in it:

Among the first requests we see an appeal to the speaking address /register
Having examined the contents of the outgoing request, we find on the Authentication tab the authorization header, in which the user and password match those entered in the application interface.

In addition, we now have information about the contents of the request body – in addition to the login and password, the server needs to pass the application name and its id.

In response the server returned new login and password – let's remember them.

Now we can try to send a chat message inside the application and receive a new request in Charles with, again, an obvious name /send

When studying this request, on the Authentication tab we already saw the new login and password received at the registration stage, and not the original ones. We also received data on the design of the request body from the application and the id of a specific chat.

Now you can go test your hypothesis about the order of interaction with the server.

As a result, with the help of Postman we were able to reproduce the procedure for registering and sending a message and, through tests, understand what each of the request fields is responsible for, what types of data are expected in them, etc.

Also, during the testing process, it became clear that if you save the registration parameters, the keys for interacting with the chat can be obtained once and reused. At least, there are no timestamps in the response – which could be regarded as the validity period of the token.

Writing a layer for the API

Having all the data in hand, we can write our own service that will receive simple HTTP requests from our bots, convert them and direct them to the real API. Registration with the server is also the responsibility of the planned API proxy.
Let's make a minimal version – with all incoming messages sent to only one chat, whose id was previously received through Charles.

Let's write it in python using aiohttp.
This library is quite suitable due to its simplicity – the service we are planning will implement only one method per input.
You will also need requests – for a one-time synchronous request.
We use poetry to install dependencies.

We will wrap the application itself in Docker via docker-compose.
In the docker-compose.yml file for the application, we will indicate the environment variables – logins, passwords, etc.:

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

In order for the application to be able to pull up the above variables from env, we will create a class in settings.py with a function for receiving values ​​from the environment.

Also, for convenience, we create a file templates.py – with template classes for requests to the original API, for example:

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

We can go to the main application file, in which we will create the main application loop and configure the paths to the API – in our case it will be 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())
    )

We will immediately implement some protection of the service from third-party attacks – for this, aiohttp has middlewares.
Let's write our own protection layer – the function will check the token transmitted in the headers:

@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

For it to work, you need to add middleware to the main function:

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

Let's add the ability for the application to register with the messenger server upon startup.
Let's create a separate Register class, where the register_chat_api function will make a request to the server and create a variable with the final authorization field – it will be used to send a message:

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)

Another class will be responsible for sending the received message to the real API.
Let's call it Sender and when called we will pass an instance of Register – to gain access to the authorization key:

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")

We use the resulting application modules in the main function and get our own API proxy, which will accept HTTP requests from our technical users and translate them into a real API.

All that remains is to build and run the container – docker-compose will pull up the necessary dependencies using poetry and launch the application on the installed port.

You can view the full template for such an application here: aiohttp-api-proxy

Conclusion

By being able to proxy and read application traffic, it is relatively easy to be able to interact with the application's internal API to use the desired functionality.

And the implementation of interaction by creating an API proxy allows you to simplify its use as much as possible for your own client applications.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *