Search code examples
tcpc++builderindy

Why do I need to send a message twice to trigger Indy's OnExecute event?


I am working on an application that works as a "man in the middle" to analyze a protocol (ISO 8583) sent over TCP/IP.

The main idea is to get the raw binary data and convert it to a string for parsing and decoding the protocol.

For this, I am using the TIdMappedPortTCP component.

I am testing with Hercules.

I am working with:

Windows 11 Home

Embarcadero® C++Builder 10.4 Version 27.0.40680.4203

Delphi and C++ Builder 10.4 Update 2

Indy 10.6.2.0

More context can be found in these questions:

Where can I find a fully working example of a TCP Client and Server for Indy in C++Builder?

Parsing bytes as BCD with Indy C++ Builder

The problem is that I have to send the message twice to trigger the OnExecute event. I think this might be length related but I haven't found the issue. Other than that the program does what is expected from it.

If I use this data in Hercules:

00 04 60 02

equivalent to:

"\x00\x04\x60\x02"

image

My program processes everything correctly:

image

Here is the code:

void __fastcall TForm1::MITMProxyExecute(TIdContext *AContext)
{
    static int index;
    TIdBytes ucBuffer;
    UnicodeString usTemp1;
    UnicodeString usTemp2;
    int calculated_length;

    // getting the length in Hexa
    calculated_length = ReadMessageLength(AContext);
    // reads data
    AContext->Connection->IOHandler->ReadBytes(ucBuffer, calculated_length);

    // displays string with calculated length and size of the data
    usTemp2 = UnicodeString("calculated length = ");
    usTemp2 += IntToStr(calculated_length);
    usTemp2 += " ucBuffer.Length = ";
    usTemp2 += IntToStr(ucBuffer.Length);
    Display->Lines->Add(usTemp2);
    // converts the binary data into a a Hex String for visualization
    usTemp1 = BytesToHexString(ucBuffer);

    // adds an index to distinguish from previous entries.
    usTemp2 = IntToStr(index);
    usTemp2 += UnicodeString(": ");
    usTemp2 += usTemp1;
    Display->Lines->Add(usTemp2);
    index++;
}

Here is the code for the functions called there. By the way, is there a better way to convert the bytes to a hex string?

// Convert an array of bytes to a hexadecimal string
UnicodeString BytesToHexString(const TBytes& bytes)
{
    // Create an empty UnicodeString to store the hexadecimal representation of the bytes
    UnicodeString hexString;

    // Iterate through each byte in the array
    for (int i = 0; i < bytes.Length; i++)
    {
        // Convert the byte to a hexadecimal string and append it to the result string
        hexString += IntToHex(bytes[i], 2);
    }

    // Return the hexadecimal string
    return hexString;
}

// Read the first two bytes of an incoming message and interpret them as the length of the message
int ReadMessageLength(TIdContext *AContext)
{
    int calculated_length;

    // Use the 'ReadSmallInt' method to read the length of the message from the first two bytes
    calculated_length = AContext->Connection->IOHandler->ReadSmallInt();
    // converting from hex binary to hex string
    UnicodeString bcdLength = UnicodeString().sprintf(L"%04x", calculated_length);
    // converting from hex string to int
    calculated_length = bcdLength.ToInt();
    // decrease length
    calculated_length -= 2;
    return calculated_length;
}

UPDATE I have created a class to update the TEditRich control. But the problem persist, I need to send the message twice to be processed and the application freezes when trying to close it. This is my class:

class TAddTextToDisplay : public TIdSync {
private:
    UnicodeString textToAdd;

public:
    __fastcall TAddTextToDisplay(UnicodeString str) {
        // Store the input parameters in member variables.
        textToAdd = str;
    }

    virtual void __fastcall DoSynchronize() {
        if (textToAdd != NULL) {
            // Use the input parameters here...
            Form1->Display->Lines->Add(textToAdd);
        }
    }

    void __fastcall setTextToAdd(UnicodeString str) {
        textToAdd = str;
    }
};

And this is how my new OnExecute event looks:

void __fastcall TForm1::MITMProxyExecute(TIdContext *AContext) {
    static int index;
    TIdBytes ucBuffer;
    UnicodeString usTemp1;
    UnicodeString usTemp2;
    int calculated_length;
    int bytes_remaining;

    // getting the length in Hexa
    calculated_length = ReadMessageLength(AContext);
    if (!AContext->Connection->IOHandler->InputBufferIsEmpty()) {

        // reads data
        AContext->Connection->IOHandler->ReadBytes(ucBuffer, calculated_length);

        // displays string with calculated length and size of the data
        usTemp2 = UnicodeString("calculated length = ");
        usTemp2 += IntToStr(calculated_length);
        usTemp2 += " ucBuffer.Length = ";
        usTemp2 += IntToStr(ucBuffer.Length);
        TAddTextToDisplay *AddTextToDisplay = new TAddTextToDisplay(usTemp2);
        AddTextToDisplay->Synchronize();

        // converts the binary data into a a Hex String for visualization
        usTemp1 = BytesToHexString(ucBuffer);
        // adds an index to distinguish from previous entries.
        usTemp2 = IntToStr(index);
        usTemp2 += UnicodeString(": ");
        usTemp2 += usTemp1;
        AddTextToDisplay->setTextToAdd(usTemp2);
        AddTextToDisplay->Synchronize();
        delete AddTextToDisplay;
        index++;
    }

}

Solution

  • You really should not be reading from the IOHandler directly at all. You are getting your communication out of sync. TIdMappedPortTCP internally reads from the client before firing the OnExecute event, and reads from the target server before firing the OnOutboundData event. In both cases, the bytes received are made available in the TIdMappedPortContext::NetData property, which you are not processing at all.

    You need to do all of your parsing using just the NetData only, iterating through its bytes looking for complete messages, and saving incomplete messages for future events to finish.

    Try something more like this instead:

    #include <IdGlobal.hpp>
    #include <IdBuffer.hpp>
    
    bool ReadMessageData(TIdBuffer *Buffer, int &Offset, TIdBytes &Data)
    {
        // has enough bytes?
        if ((Offset + 2) > Buffer->Size)
            return false;
    
        // read the length of the message from the first two bytes
        UInt16 binLength = Buffer->ExtractToUInt16(Offset);
    
        // converting from hex binary to hex string
        String bcdLength = String().sprintf(_D("%04hx"), binLength);
    
        // converting from hex string to int
        int calculated_length = bcdLength.ToInt() - 2;
    
        // has enough bytes?
        if ((Offset + 2 + calculated_length) > Buffer->Size)
            return false;
    
        // reads data
        Data.Length = calculated_length;
        Buffer->ExtractToBytes(Data, calculated_length, false, Offset + 2);
    
        Offset += (2 + calculated_length);
        return true;
    }
    
    void __fastcall TForm1::MITMProxyConnect(TIdContext *AContext)
    {
        AContext->Data = new TIdBuffer;
    }
    
    void __fastcall TForm1::MITMProxyDisconnect(TIdContext *AContext)
    {
        delete static_cast<TIdBuffer*>(AContext->Data);
        AContext->Data = NULL;
    }
    
    void __fastcall TForm1::MITMProxyExecute(TIdContext *AContext)
    {
        static int index = 0;
    
        TIdBuffer *Buffer = static_cast<TIdBuffer*>(AContext->Data);
        Buffer->Write(static_cast<TIdMappedPortContext*>(AContext)->NetData);
        Buffer->CompactHead();
    
        TAddTextToDisplay *AddTextToDisplay = NULL;
        TIdBytes ucBuffer;
        int offset = 0;
    
        while (ReadMessageData(Buffer, offset, ucBuffer))
        {
            String sTemp = String().sprintf(_D("%d: ucBuffer.Length = %d ucBuffer = %s"), index, ucBuffer.Length, ToHex(ucBuffer).c_str());
    
            if (AddTextToDisplay)
                AddTextToDisplay->setTextToAdd(sTemp);
            else
                AddTextToDisplay = new TAddTextToDisplay(sTemp);
    
            AddTextToDisplay->Synchronize();
    
            ++index;
        }
    
        delete AddTextToDisplay;
    
        if (offset > 0)
            Buffer->Remove(offset);
    }
    

    Otherwise, if you want to do your own socket I/O, then you will have to use TIdTCPServer and TIdTCPClient directly instead of using TIdMappedPortTCP.