espressif / esp-mqtt

ESP32 mqtt component
Apache License 2.0
603 stars 255 forks source link

MQTT Client: Added support for receiving empty payload (IDFGH-946) #110

Closed gregjesl closed 5 years ago

gregjesl commented 5 years ago

Closes https://github.com/espressif/esp-idf/issues/3088

gregjesl commented 5 years ago

@david-cermak I've made the changes, please let me know if there is anything else.

david-cermak commented 5 years ago

@gregjesl LGTM, thanks.

However Travis has failed, will have to fix it and then ask for a rebase from your side.

david-cermak commented 5 years ago

@gregjesl Could you please rebase now? Build would probably still fail as it's also validated against IDF master (which might not currently build on some platforms: linux with newer python packages), but there's not an issue in this repo and changes can be reviewed internally in the meantime.

gregjesl commented 5 years ago

@david-cermak You'd like me to rebase the idf branch onto this pull request, correct?

david-cermak commented 5 years ago

@gregjesl Yes, please rebase your branch (in this pull request) on top of idf. That's then easier for us to test and review, plus don't have to modify your commits

gregjesl commented 5 years ago

@david-cermak I've rebased the branch.

david-cermak commented 5 years ago

@gregjesl There seems to be an issue with receiving empty messages under some conditions (e.g. single message with empty payload in a buffer).

Sorry I haven't noticed during code review and haven't had time to test it before. Conditional NULL == (mqtt_data = mqtt_get_publish_data(message, &mqtt_data_length) seems to be a culprit as this could be null for empty messages.

gregjesl commented 5 years ago

@david-cermak From the MQTT 3.1.1 spec:

The length of the payload can be calculated by subtracting the length of the variable header from the Remaining Length field that is in the Fixed Header.

From what I can tell the proper solution is to check for the existence of a payload by subtracting the Remaining Length field of the Fixed Header from the length of the variable header. If this value is greater than 0, then NULL == (mqtt_data = mqtt_get_publish_data(message, &mqtt_data_length) must be true for the full message to be received. If this value value is 0, then the full message has been received.

The length of the variable header is simply the topic length + 4. I can't find the part of the code that returns the Remaining Length field.

david-cermak commented 5 years ago

Generally, I agree that would be the correct solution. There's no API which directly returns "Remaining Length" (also note the variable header length also depends on QOS).

With the given codebase however, I might prefer to modify mqtt_get_publish_data (and maybe the topic function) to indicate if a starting payload ptr could be resolved. Something like:

bool mqtt_get_publish_data(uint8_t* buffer, uint32_t* length, char** mqtt_data);
gregjesl commented 5 years ago

@david-cermak The following function should return true if buffer contains a complete message:

bool mqtt_message_complete(uint8_t* buffer, uint32_t blength)
{
    int i;
    int remainlen = 0;

    // Read out the Remaining Length (which does not include the length of the fixed header)
    for (i = 1; i < 5; ++i)
    {
        if(i >= blength)
            return false;
        remainlen += (buffer[i] & 0x7f) << (7 * (i - 1));
        if ((buffer[i] & 0x80) == 0)
        {
            ++i;
            break;
        }
    }

    // The buffer length must be at least the Remaining Length 
    // plus the length of the fixed header (i in this case)
    return blength >= remainlen + i;
}

The test

if(NULL == (mqtt_topic = mqtt_get_publish_topic(message, &mqtt_topic_length)) || 
    NULL == (mqtt_data = mqtt_get_publish_data(message, &mqtt_data_length)))

can be replaced with

if(!mqtt_message_complete(message, length))

The buffer contains a complete message without a payload if

mqtt_message_complete(message, length) == true

and

NULL == (mqtt_data = mqtt_get_publish_data(message, &mqtt_data_length))

Would these changes work for you?

gregjesl commented 5 years ago

@david-cermak Disregard my last comment, it will fail for large messages.

This function should return true if the header is completely loaded in the buffer:

bool mqtt_header_complete(uint8_t* buffer, uint32_t blength)
{
    int i;
    int topiclen;

    for (i = 1; i < 5; ++i)
    {
        if(i >= blength)
            return false;
        if ((buffer[i] & 0x80) == 0)
        {
            ++i;
            break;
        }
    }
    // i is now the length of the fixed header

    if (i + 2 >= blength)
        return false;
    topiclen = buffer[i++] << 8;
    topiclen |= buffer[i++];

    if (i + topiclen > blength)
        return false;

    i += topiclen;

    if (mqtt_get_qos(buffer) > 0)
    {
        i += 2;
    }
    // i is now the length of the fixed + variable header
    return blength >= i;
}
david-cermak commented 5 years ago

@gregjesl Sounds good, that should work!

I was just writing about the issue with longer messages 😄

gregjesl commented 5 years ago

@david-cermak I've made the updates.

david-cermak commented 5 years ago

@gregjesl Thanks! This works fine and doesn't break existing tests. Have moved to internal repo for a review internally.

Meantime, could you please correct:

gregjesl commented 5 years ago

@david-cermak I've made the formatting changes to mqtt_client.c and fixed the semicolon issue. I did not adjust the formatting in mqtt_msg.c as it seems to match the formatting in that specific document.

Is there a way for me to test my changes before committing them besides altering the git submodule in the IDF? I didn't realize until today that the examples in the examples folder use the MQTT library in my IDF and not the ones in the local repository.

david-cermak commented 5 years ago

@gregjesl Thanks, yes I meant only the related changes to mqtt_client.c.

The easiest way to test it is to modify the submodule in IDF. You can then run the python tests in example folder. (there are other ways to include an external component, such as EXTRA_COMPONENT_DIR -- you can check .travis file in this repo to see how it is built, but I do not recommend)

david-cermak commented 5 years ago

Just two minor comments arose from the review

@gregjesl I would prefer if you updated your changes, as it could be merged directly without me amending the commit.

gregjesl commented 5 years ago

@david-cermak That sounds good; I'll make the changes later today.

gregjesl commented 5 years ago

@david-cermak I've made the changes.

david-cermak commented 5 years ago

@gregjesl Many thanks for your contribution!