diff --git a/scripts/broker.test b/scripts/broker.test index 80c7a9e93..2d5985398 100755 --- a/scripts/broker.test +++ b/scripts/broker.test @@ -385,7 +385,9 @@ if ! wait_for_file "${TMP_DIR}/t6b_watcher.ready" 5; then FAIL=1 t6b_setup_ok=0 fi -./$sub_bin -T -h 127.0.0.1 -p $port -n "test/ka2_trigger" -i "ka2_client" -l -k 4 \ +# -s (persistent session, Session Expiry != 0) so the v5 Will Delay is honored; +# an expiry-0 session ends at disconnect and publishes the Will immediately. +./$sub_bin -T -h 127.0.0.1 -p $port -n "test/ka2_trigger" -i "ka2_client" -l -s -k 4 \ -R "${TMP_DIR}/t6b_client.ready" >"${TMP_DIR}/t6b_client.log" 2>&1 & T6B_CLIENT_PID=$! TEST_PIDS+=($T6B_CLIENT_PID) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index bbc6fedd0..d3187c780 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -85,6 +85,11 @@ static void MqttBroker_ForceZero(void* mem, word32 len) #endif #endif +/* The sweep math relies on unsigned wraparound and a ~0 saturation ceiling, + * so an override to a signed type would break Will Delay and orphan expiry. */ +typedef char wolfmqtt_broker_time_t_must_be_unsigned[ + ((WOLFMQTT_BROKER_TIME_T)-1 > 0) ? 1 : -1]; + /* -------------------------------------------------------------------------- */ /* Default sleep abstraction */ /* -------------------------------------------------------------------------- */ @@ -1614,6 +1619,62 @@ static void BrokerInboundQos2_Clear(BrokerClient* bc) bc->qos2_pending_count = 0; #endif } + +/* Returns 1 if the client holds any inbound QoS2 dedup state. */ +static int BrokerInboundQos2_HasPending(const BrokerClient* bc) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + int i; + for (i = 0; i < BROKER_MAX_INBOUND_QOS2; i++) { + if (bc->qos2_pending[i] != 0) { + return 1; + } + } + return 0; +#else + return (bc->qos2_pending_count > 0); +#endif +} + +/* Move the old client's inbound QoS2 dedup state to the new client on a live + * same-ClientId takeover, but only if the old session survives: a v5 session + * with Session Expiry 0 (or a v3.1.1 CleanSession=1 session) ends at takeover, + * so its packet ids must not carry into the new session (they would suppress a + * fresh QoS2 message as a duplicate). Returns 1 if state carried, so the caller + * reports Session Present. The dropped case leaves old's state to be freed with + * the old client. */ +static int BrokerInboundQos2_Takeover(BrokerClient* new_bc, BrokerClient* old) +{ + int old_persists = (old->clean_session == 0); +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) + if (old->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + old_persists = (old->session_expiry_sec != 0); + } +#elif defined(WOLFMQTT_V5) && defined(WOLFMQTT_STATIC_MEMORY) + /* Static BrokerClient has no session_expiry_sec, so a v5 Session Expiry 0 + * (ends the Session at disconnect [MQTT-3.1.2.11.2]) is indistinguishable + * from a surviving one. Conservatively do not carry inbound QoS2 dedup + * state across a v5 takeover rather than risk suppressing a later PUBLISH + * that legitimately reuses a freed packet id. */ + if (old->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + old_persists = 0; + } +#endif + if (!old_persists || !BrokerInboundQos2_HasPending(old)) { + return 0; + } +#ifdef WOLFMQTT_STATIC_MEMORY + XMEMCPY(new_bc->qos2_pending, old->qos2_pending, + sizeof(new_bc->qos2_pending)); + XMEMSET(old->qos2_pending, 0, sizeof(old->qos2_pending)); +#else + new_bc->qos2_pending = old->qos2_pending; + new_bc->qos2_pending_count = old->qos2_pending_count; + old->qos2_pending = NULL; + old->qos2_pending_count = 0; +#endif + return 1; +} #endif /* WOLFMQTT_MAX_QOS >= 2 */ #ifndef WOLFMQTT_STATIC_MEMORY @@ -1628,7 +1689,107 @@ static void BrokerInboundQos2_Clear(BrokerClient* bc) * gets unblocked promptly. */ /* -------------------------------------------------------------------------- */ -/* Free a single queue entry (topic, payload, the entry itself). */ +#ifdef WOLFMQTT_V5 +/* Deep-copy props so they outlive rx_buf; free via BrokerProps_FreeClone, + * not MqttProps_Free (this bypasses the shared fixed-size pool). */ +static void BrokerProps_FreeClone(MqttProp* head); + +/* Returns NULL on OOM (never a partial list): a truncated v5 property + * list would go out on the wire silently short, so any allocation + * failure frees what was built and fails the whole clone. */ +static MqttProp* BrokerProps_Clone(const MqttProp* src) +{ + MqttProp* head = NULL; + MqttProp* tail = NULL; + + for (; src != NULL; src = src->next) { + MqttProp* dst = (MqttProp*)WOLFMQTT_MALLOC(sizeof(MqttProp)); + if (dst == NULL) { + BrokerProps_FreeClone(head); + return NULL; + } + XMEMSET(dst, 0, sizeof(*dst)); + dst->type = src->type; + dst->data_byte = src->data_byte; + dst->data_short = src->data_short; + dst->data_int = src->data_int; + dst->data_str.len = 0; + dst->data_str.str = NULL; + dst->data_str2.len = 0; + dst->data_str2.str = NULL; + dst->data_bin.len = 0; + dst->data_bin.data = NULL; + if (src->data_str.len > 0 && src->data_str.str != NULL) { + dst->data_str.str = (char*)WOLFMQTT_MALLOC(src->data_str.len); + if (dst->data_str.str == NULL) { + BrokerProps_FreeClone(dst); + BrokerProps_FreeClone(head); + return NULL; + } + XMEMCPY(dst->data_str.str, src->data_str.str, src->data_str.len); + dst->data_str.len = src->data_str.len; + } + if (src->data_str2.len > 0 && src->data_str2.str != NULL) { + dst->data_str2.str = (char*)WOLFMQTT_MALLOC(src->data_str2.len); + if (dst->data_str2.str == NULL) { + BrokerProps_FreeClone(dst); + BrokerProps_FreeClone(head); + return NULL; + } + XMEMCPY(dst->data_str2.str, src->data_str2.str, + src->data_str2.len); + dst->data_str2.len = src->data_str2.len; + } + if (src->data_bin.len > 0 && src->data_bin.data != NULL) { + dst->data_bin.data = (byte*)WOLFMQTT_MALLOC(src->data_bin.len); + if (dst->data_bin.data == NULL) { + BrokerProps_FreeClone(dst); + BrokerProps_FreeClone(head); + return NULL; + } + XMEMCPY(dst->data_bin.data, src->data_bin.data, + src->data_bin.len); + dst->data_bin.len = src->data_bin.len; + } + dst->next = NULL; + if (tail == NULL) { + head = dst; + } + else { + tail->next = dst; + } + tail = dst; + } + return head; +} + +/* Free a property list allocated by BrokerProps_Clone(). */ +static void BrokerProps_FreeClone(MqttProp* head) +{ + while (head != NULL) { + MqttProp* next = head->next; + /* Cloned Correlation Data / User Properties / Content Type can carry + * application secrets; scrub before free, matching the queue cleanup. */ + if (head->data_str.str != NULL) { + BROKER_FORCE_ZERO(head->data_str.str, head->data_str.len); + WOLFMQTT_FREE(head->data_str.str); + } + if (head->data_str2.str != NULL) { + BROKER_FORCE_ZERO(head->data_str2.str, head->data_str2.len); + WOLFMQTT_FREE(head->data_str2.str); + } + if (head->data_bin.data != NULL) { + BROKER_FORCE_ZERO(head->data_bin.data, head->data_bin.len); + WOLFMQTT_FREE(head->data_bin.data); + } + BROKER_FORCE_ZERO(head, sizeof(*head)); + WOLFMQTT_FREE(head); + head = next; + } +} +#endif /* WOLFMQTT_V5 */ + +/* Free a single queue entry (topic, payload, props, the entry itself). */ static void BrokerOutPub_Free(BrokerOutPub* e) { if (e == NULL) { @@ -1644,15 +1805,25 @@ static void BrokerOutPub_Free(BrokerOutPub* e) WOLFMQTT_FREE(e->payload); e->payload = NULL; } +#ifdef WOLFMQTT_V5 + if (e->props != NULL) { + BrokerProps_FreeClone(e->props); + e->props = NULL; + } +#endif WOLFMQTT_FREE(e); } -/* Allocate a new entry holding a deep copy of topic + payload. Returns - * NULL on allocation failure (caller decides whether that means drop or - * close). All fields are zero-initialized; caller fills qos / packet_id / - * etc. and links into out_q via BrokerClient_EnqueueOutPub. */ +/* Allocate a new entry holding a deep copy of topic + payload (+ props if + * src_props is non-NULL). Returns NULL on allocation failure (caller + * decides whether that means drop or close). Fields are zero-initialized; + * caller fills qos / packet_id / etc. and links into out_q. */ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, - const byte* payload, word32 payload_len) + const byte* payload, word32 payload_len +#ifdef WOLFMQTT_V5 + , const MqttProp* src_props +#endif + ) { BrokerOutPub* e; size_t topic_len; @@ -1685,6 +1856,17 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, XMEMCPY(e->payload, payload, payload_len); e->payload_len = payload_len; } +#ifdef WOLFMQTT_V5 + if (src_props != NULL) { + e->props = BrokerProps_Clone(src_props); + if (e->props == NULL) { + /* Clone failed (OOM): drop the whole entry rather than + * queue a PUBLISH silently missing its v5 properties. */ + BrokerOutPub_Free(e); + return NULL; + } + } +#endif return e; } @@ -1812,6 +1994,7 @@ static void BrokerClient_DrainOutQueue(BrokerClient* bc) out_pub.total_len = cur->payload_len; #ifdef WOLFMQTT_V5 out_pub.protocol_level = cur->protocol_level; + out_pub.props = cur->props; #endif enc_rc = MqttEncode_Publish(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), @@ -2316,6 +2499,18 @@ static void BrokerOrphan_FreeContents(BrokerOrphanSession* o) o->out_q_tail = NULL; o->out_q_count = 0; o->out_q_inflight = 0; +#if WOLFMQTT_MAX_QOS >= 2 + { + BrokerInboundQos2* q2cur = o->qos2_pending; + while (q2cur != NULL) { + BrokerInboundQos2* q2next = q2cur->next; + WOLFMQTT_FREE(q2cur); + q2cur = q2next; + } + o->qos2_pending = NULL; + o->qos2_pending_count = 0; + } +#endif if (o->client_id != NULL) { WOLFMQTT_FREE(o->client_id); o->client_id = NULL; @@ -2492,6 +2687,16 @@ static BrokerOrphanSession* BrokerOrphan_Take(MqttBroker* broker, bc->out_q_count = 0; bc->out_q_inflight = 0; +#if WOLFMQTT_MAX_QOS >= 2 + /* Move QoS 2 dedup state too, so a retransmit after reconnect is + * still recognized instead of re-fanned-out. Same move-not-copy + * pattern as out_q above. */ + o->qos2_pending = bc->qos2_pending; + o->qos2_pending_count = bc->qos2_pending_count; + bc->qos2_pending = NULL; + bc->qos2_pending_count = 0; +#endif + /* Link at head; orphan_session_count tracks size. */ o->next = broker->orphan_sessions; broker->orphan_sessions = o; @@ -2548,6 +2753,13 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) if (new_bc->session_expiry_sec == 0xFFFFFFFFu) { new_bc->session_expiry_sec = o->session_expiry_sec; } +#if WOLFMQTT_MAX_QOS >= 2 + /* Move QoS 2 dedup state back before any new PUBLISH is processed. */ + new_bc->qos2_pending = o->qos2_pending; + new_bc->qos2_pending_count = o->qos2_pending_count; + o->qos2_pending = NULL; + o->qos2_pending_count = 0; +#endif /* MQTT-4.4.0-1: any message that was previously in-flight on the old * session is re-sent on resume. PUBLISH_SENT -> QUEUED with * retransmit_dup so the drain re-sends the PUBLISH with DUP=1. @@ -2602,7 +2814,11 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) * messages live in the offline queue. */ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, const char* topic, const byte* payload, word32 payload_len, - MqttQoS qos, byte retain) + MqttQoS qos, byte retain +#ifdef WOLFMQTT_V5 + , const MqttProp* src_props +#endif + ) { BrokerOutPub* e; if (broker == NULL || o == NULL || topic == NULL || @@ -2638,7 +2854,11 @@ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, BrokerOutPub_Free(head); } - e = BrokerOutPub_Alloc(topic, payload, payload_len); + e = BrokerOutPub_Alloc(topic, payload, payload_len +#ifdef WOLFMQTT_V5 + , src_props +#endif + ); if (e == NULL) { WBLOG_ERR(broker, "broker: orphan enqueue alloc failed client_id=%s", @@ -2688,6 +2908,39 @@ static void BrokerOrphan_FreeAll(MqttBroker* broker) broker->orphan_sessions = NULL; broker->orphan_session_count = 0; } + +/* Drop orphan sessions whose finite Session Expiry has elapsed. */ +static void BrokerOrphan_ExpireSweep(MqttBroker* broker) +{ + BrokerOrphanSession* cur; + WOLFMQTT_BROKER_TIME_T now; + int dropped; + if (broker == NULL) { + return; + } + now = WOLFMQTT_BROKER_GET_TIME_S(); + /* Re-scan from head after each removal; DropFull mutates the list. */ + do { + dropped = 0; + for (cur = broker->orphan_sessions; cur != NULL; cur = cur->next) { + /* Compare in WOLFMQTT_BROKER_TIME_T (may be wider than word32) + * rather than narrowing the elapsed delta down to compare + * against session_expiry_sec. */ + if (cur->session_expiry_sec != 0xFFFFFFFFu && + now >= cur->orphan_since && + (now - cur->orphan_since) >= + (WOLFMQTT_BROKER_TIME_T)cur->session_expiry_sec) { + WBLOG_INFO(broker, + "broker: orphan session expired client_id=%s", + BrokerLog_Sanitize(BROKER_STR_VALID(cur->client_id) + ? cur->client_id : "(null)")); + BrokerOrphan_DropFull(broker, cur); + dropped = 1; + break; + } + } + } while (dropped); +} #endif /* !WOLFMQTT_STATIC_MEMORY */ /* Forward declaration; orphan-take-failure rollback in @@ -2703,6 +2956,7 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) int i; #else BrokerSub *cur; + BrokerOrphanSession* orphan_exp = NULL; #endif int count = 0; @@ -2726,9 +2980,26 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) cur = cur->next; } #endif +#ifndef WOLFMQTT_STATIC_MEMORY + /* [MQTT-3.1.2.11.2] Session Expiry 0 (or absent) ends the Session at + * disconnect: remove any subscriptions and drop a pre-existing carrier + * rather than orphaning. v3.1.1 persistent clients carry 0xFFFFFFFF here, + * so this fires only for v5 zero-expiry sessions. */ + if (bc->session_expiry_sec == 0) { + if (count > 0) { + BrokerSubs_RemoveClient(broker, bc); + } + orphan_exp = BrokerOrphan_Find(broker, bc->client_id); + if (orphan_exp != NULL) { + BrokerOrphan_Remove(broker, orphan_exp); + } + return; + } +#else if (count == 0) { return; } +#endif #ifndef WOLFMQTT_STATIC_MEMORY /* Stage a persistent-session record in broker->orphan_sessions. @@ -2746,6 +3017,10 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) BrokerSubs_RemoveClient(broker, bc); return; } + if (count == 0) { + /* No subs to detach; orphan record alone preserves the Session. */ + return; + } #endif /* Second pass: detach. Safe to mutate now - the carrier exists @@ -2944,6 +3219,8 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, bc->sub_count++; WBLOG_INFO(broker, "broker: sub add sock=%d filter=%s qos=%d", (int)bc->sock, BrokerLog_Sanitize(sub->filter), qos); + /* 1 = newly created (vs. 0 = updated), for Retain Handling = 1. */ + return 1; } return rc; } @@ -3562,6 +3839,9 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) { #ifdef WOLFMQTT_STATIC_MEMORY int i; +#else + BrokerPendingWill* wcur; + int wcount = 0; #endif BrokerPendingWill* pw = NULL; @@ -3605,10 +3885,21 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) } } #else - pw = (BrokerPendingWill*)WOLFMQTT_MALLOC(sizeof(BrokerPendingWill)); - if (pw == NULL) { + /* Bound the dynamic list: repeated abnormal closes with unique client IDs + * must not grow pending_wills without limit. When full, fail so the caller + * publishes the Will immediately instead of retaining it. */ + for (wcur = broker->pending_wills; wcur != NULL; wcur = wcur->next) { + wcount++; + } + if (wcount >= BROKER_MAX_PENDING_WILLS) { rc = MQTT_CODE_ERROR_MEMORY; } + if (rc == MQTT_CODE_SUCCESS) { + pw = (BrokerPendingWill*)WOLFMQTT_MALLOC(sizeof(BrokerPendingWill)); + if (pw == NULL) { + rc = MQTT_CODE_ERROR_MEMORY; + } + } if (rc == MQTT_CODE_SUCCESS) { int id_len = (int)XSTRLEN(bc->client_id); int t_len = (int)XSTRLEN(bc->will_topic); @@ -3661,12 +3952,33 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) #endif if (rc == MQTT_CODE_SUCCESS) { + word32 delay_sec = bc->will_delay_sec; + WOLFMQTT_BROKER_TIME_T max_time = (WOLFMQTT_BROKER_TIME_T)~(WOLFMQTT_BROKER_TIME_T)0; pw->qos = bc->will_qos; pw->retain = bc->will_retain; - pw->publish_time = now + (WOLFMQTT_BROKER_TIME_T)bc->will_delay_sec; + #if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) + /* [MQTT-3.1.3.2.2] Publish the Will at the earlier of the Will Delay or + * Session end. Any finite Session Expiry shorter than the Will Delay + * wins - including 0 (or absent), which ends the Session at disconnect + * and so publishes immediately. Only 0xFFFFFFFF (never expires) leaves + * the full Will Delay authoritative. */ + if (bc->session_expiry_sec != 0xFFFFFFFFu && + bc->session_expiry_sec < delay_sec) { + delay_sec = bc->session_expiry_sec; + } + #endif + /* Saturate rather than wrap: on a 32-bit WOLFMQTT_BROKER_TIME_T a + * huge Will Delay could push now+delay below now and make the sweep + * fire the Will immediately. */ + if ((WOLFMQTT_BROKER_TIME_T)delay_sec > max_time - now) { + pw->publish_time = max_time; + } + else { + pw->publish_time = now + (WOLFMQTT_BROKER_TIME_T)delay_sec; + } WBLOG_DBG(broker, "broker: will deferred sock=%d client_id=%s delay=%u", (int)bc->sock, BrokerLog_Sanitize(bc->client_id), - (unsigned)bc->will_delay_sec); + (unsigned)delay_sec); } return rc; } @@ -3849,6 +4161,10 @@ static int BrokerPendingWill_Process(MqttBroker* broker) } #endif /* WOLFMQTT_BROKER_WILL */ +#ifdef WOLFMQTT_V5 +static int BrokerSend_Disconnect(BrokerClient* bc, byte reason_code); +#endif + #ifdef WOLFMQTT_BROKER_RETAINED static void BrokerRetained_DeliverToClient(MqttBroker* broker, BrokerClient* bc, const char* filter, MqttQoS sub_qos) @@ -3969,40 +4285,82 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, continue; } if (rm->topic != NULL && BrokerTopicMatch(filter, rm->topic)) { - MqttPublish out_pub; MqttQoS eff_qos = (rm->qos < sub_qos) ? rm->qos : sub_qos; - int enc_rc, wr_rc; - XMEMSET(&out_pub, 0, sizeof(out_pub)); - out_pub.topic_name = rm->topic; - out_pub.qos = eff_qos; - out_pub.retain = 1; - out_pub.duplicate = 0; - out_pub.buffer = (rm->payload_len > 0) ? rm->payload : NULL; - out_pub.total_len = rm->payload_len; if (eff_qos >= MQTT_QOS_1) { - out_pub.packet_id = BrokerNextPacketId(broker); + /* Route QoS 1/2 through out_q so it survives reconnect. A full + * queue must not silently drop a required retained delivery: + * disconnect the slow subscriber with Quota Exceeded, matching + * the live PUBLISH fan-out policy. */ + if (bc->out_q_count >= BROKER_MAX_QUEUED_MSGS_PER_SUB) { + WBLOG_ERR(broker, + "broker: retained out_q full (%d) -> disconnect sock=%d", + bc->out_q_count, (int)bc->sock); + #ifdef WOLFMQTT_V5 + (void)BrokerSend_Disconnect(bc, MQTT_REASON_QUOTA_EXCEEDED); + #endif + if (bc->sock != BROKER_SOCKET_INVALID) { + broker->net.close(broker->net.ctx, bc->sock); + bc->sock = BROKER_SOCKET_INVALID; + } + bc->connected = 0; + break; + } + else { + BrokerOutPub* e = BrokerOutPub_Alloc(rm->topic, + (rm->payload_len > 0) ? rm->payload : NULL, + rm->payload_len + #ifdef WOLFMQTT_V5 + , NULL + #endif + ); + if (e == NULL) { + WBLOG_ERR(broker, + "broker: retained alloc failed sock=%d topic=%s", + (int)bc->sock, BrokerLog_Sanitize(rm->topic)); + } + else { + e->qos = eff_qos; + e->packet_id = BrokerNextPacketId(broker); + e->retain = 1; + e->state = BROKER_OUTQ_QUEUED; + #ifdef WOLFMQTT_V5 + e->protocol_level = bc->protocol_level; + #endif + BrokerClient_EnqueueOutPub(bc, e); + WBLOG_DBG(broker, + "broker: retained enq sock=%d topic=%s qos=%d", + (int)bc->sock, BrokerLog_Sanitize(rm->topic), + (int)eff_qos); + BrokerClient_DrainOutQueue(bc); + } + } } + else { + MqttPublish out_pub; + int enc_rc, wr_rc; + XMEMSET(&out_pub, 0, sizeof(out_pub)); + out_pub.topic_name = rm->topic; + out_pub.qos = eff_qos; + out_pub.retain = 1; + out_pub.duplicate = 0; + out_pub.buffer = (rm->payload_len > 0) ? rm->payload : NULL; + out_pub.total_len = rm->payload_len; #ifdef WOLFMQTT_V5 - out_pub.protocol_level = bc->protocol_level; -#endif - enc_rc = MqttEncode_Publish(bc->tx_buf, - BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); - if (enc_rc > 0) { - WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " - "len=%u qos=%d", (int)bc->sock, - BrokerLog_Sanitize(rm->topic), - (unsigned)rm->payload_len, (int)eff_qos); - wr_rc = MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); - /* Scrub after a completed write and after a hard failure - both - * leave bc->tx_buf idle. Skip only the in-progress case: in - * non-blocking / TLS-async mode MqttPacket_Write returns - * MQTT_CODE_CONTINUE with the send still referencing bc->tx_buf, - * so zeroing then would corrupt it (that residue is cleared by - * the next full write or by BrokerClient_Free). */ - if (wr_rc != MQTT_CODE_CONTINUE) { - /* Scrub the retained (possibly retained-will) payload from - * the subscriber tx_buf, mirroring the will fan-out. */ - BROKER_FORCE_ZERO(bc->tx_buf, enc_rc); + out_pub.protocol_level = bc->protocol_level; +#endif + enc_rc = MqttEncode_Publish(bc->tx_buf, + BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); + if (enc_rc > 0) { + WBLOG_DBG(broker, + "broker: retained deliver sock=%d topic=%s " + "len=%u qos=%d", (int)bc->sock, + BrokerLog_Sanitize(rm->topic), + (unsigned)rm->payload_len, (int)eff_qos); + wr_rc = MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); + /* Scrub tx_buf unless still in-progress (CONTINUE). */ + if (wr_rc != MQTT_CODE_CONTINUE) { + BROKER_FORCE_ZERO(bc->tx_buf, enc_rc); + } } } } @@ -4136,36 +4494,87 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, if (sub->client != NULL && sub->client->protocol_level != 0 && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { - MqttPublish out_pub; - MqttQoS eff_qos; - int enc_rc, wr_rc; - XMEMSET(&out_pub, 0, sizeof(out_pub)); - out_pub.topic_name = (char*)topic; - eff_qos = (qos < sub->qos) ? qos : sub->qos; - out_pub.qos = eff_qos; - out_pub.retain = 0; - out_pub.duplicate = 0; - out_pub.buffer = (payload_len > 0) ? (byte*)payload : NULL; - out_pub.total_len = payload_len; + MqttQoS eff_qos = (qos < sub->qos) ? qos : sub->qos; +#ifndef WOLFMQTT_STATIC_MEMORY if (eff_qos >= MQTT_QOS_1) { - out_pub.packet_id = BrokerNextPacketId(broker); + /* Route QoS 1/2 through out_q so it survives reconnect. A full + * queue must not silently drop an accepted Will: disconnect the + * slow subscriber with Quota Exceeded, matching the live PUBLISH + * fan-out policy. Gate on connected (not sock) so a WebSocket + * client - which uses ws_ctx with sock == INVALID - is still + * torn down, and a client with several matching subscriptions is + * not disconnected twice. Clearing connected lets the reaper + * close the transport (BrokerClient_Remove -> ws disconnect). */ + if (sub->client->out_q_count >= + BROKER_MAX_QUEUED_MSGS_PER_SUB) { + BrokerClient* c = sub->client; + if (c->connected) { + WBLOG_ERR(broker, + "broker: will out_q full (%d) -> disconnect sock=%d", + c->out_q_count, (int)c->sock); + #ifdef WOLFMQTT_V5 + (void)BrokerSend_Disconnect(c, + MQTT_REASON_QUOTA_EXCEEDED); + #endif + if (c->sock != BROKER_SOCKET_INVALID) { + broker->net.close(broker->net.ctx, c->sock); + c->sock = BROKER_SOCKET_INVALID; + } + c->connected = 0; + } + } + else { + BrokerOutPub* e = BrokerOutPub_Alloc(topic, + (payload_len > 0) ? payload : NULL, payload_len + #ifdef WOLFMQTT_V5 + , NULL + #endif + ); + if (e == NULL) { + WBLOG_ERR(broker, + "broker: will alloc failed sock=%d", + (int)sub->client->sock); + } + else { + e->qos = eff_qos; + e->packet_id = BrokerNextPacketId(broker); + e->retain = 0; + e->state = BROKER_OUTQ_QUEUED; + #ifdef WOLFMQTT_V5 + e->protocol_level = sub->client->protocol_level; + #endif + BrokerClient_EnqueueOutPub(sub->client, e); + BrokerClient_DrainOutQueue(sub->client); + } + } } -#ifdef WOLFMQTT_V5 - out_pub.protocol_level = sub->client->protocol_level; + else #endif - enc_rc = MqttEncode_Publish(sub->client->tx_buf, - BROKER_CLIENT_TX_SZ(sub->client), &out_pub, 0); - if (enc_rc > 0) { - wr_rc = MqttPacket_Write(&sub->client->client, - sub->client->tx_buf, enc_rc); - /* Scrub after a completed write and after a hard failure - both - * leave tx_buf idle. Skip only the in-progress case: in - * non-blocking / TLS-async mode MqttPacket_Write returns - * MQTT_CODE_CONTINUE with the send still referencing tx_buf, so - * zeroing then would corrupt it. Scrubbing on the error path - * keeps the will payload from lingering after a failed send. */ - if (wr_rc != MQTT_CODE_CONTINUE) { - BROKER_FORCE_ZERO(sub->client->tx_buf, enc_rc); + { + MqttPublish out_pub; + int enc_rc, wr_rc; + XMEMSET(&out_pub, 0, sizeof(out_pub)); + out_pub.topic_name = (char*)topic; + out_pub.qos = eff_qos; + out_pub.retain = 0; + out_pub.duplicate = 0; + out_pub.buffer = (payload_len > 0) ? (byte*)payload : NULL; + out_pub.total_len = payload_len; + if (eff_qos >= MQTT_QOS_1) { + out_pub.packet_id = BrokerNextPacketId(broker); + } +#ifdef WOLFMQTT_V5 + out_pub.protocol_level = sub->client->protocol_level; +#endif + enc_rc = MqttEncode_Publish(sub->client->tx_buf, + BROKER_CLIENT_TX_SZ(sub->client), &out_pub, 0); + if (enc_rc > 0) { + wr_rc = MqttPacket_Write(&sub->client->client, + sub->client->tx_buf, enc_rc); + /* Scrub tx_buf unless still in-progress (CONTINUE). */ + if (wr_rc != MQTT_CODE_CONTINUE) { + BROKER_FORCE_ZERO(sub->client->tx_buf, enc_rc); + } } } } @@ -4505,13 +4914,16 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, bc->last_rx = WOLFMQTT_BROKER_GET_TIME_S(); #ifndef WOLFMQTT_STATIC_MEMORY - /* Default Session Expiry. Set BEFORE the v5 property parse below - * so that a v5 client carrying MQTT_PROP_SESSION_EXPIRY_INTERVAL - * overrides this default rather than being silently clobbered: - * - v3.1.1 persistent (clean_session=0): 0xFFFFFFFF (server - * policy decides eviction; MQTT 3.1.1 sec 3.1.2.4). - * - clean_session=1 or v5 client without the property: 0 - * (expire on disconnect, per MQTT v5 sec 3.1.2.11.2). */ + /* Default Session Expiry, overridden below by an explicit v5 property: + * - v5: 0 regardless of Clean Start [MQTT-3.1.2.11.2]. + * - v3.1.1 clean_session=0: 0xFFFFFFFF (MQTT 3.1.1 sec 3.1.2.4). + * - v3.1.1 clean_session=1: 0. */ +#ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + bc->session_expiry_sec = 0; + } + else +#endif if (!mc.clean_session) { bc->session_expiry_sec = 0xFFFFFFFFu; } @@ -4520,39 +4932,55 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #endif +#ifdef WOLFMQTT_V5 + /* Protocol-error rejections must run even under static memory; they set + * no persistent-session field, unlike the capture block below. */ + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + mc.props != NULL) { + MqttProp* rm_prop = BrokerProps_Find(mc.props, + MQTT_PROP_RECEIVE_MAX); + MqttProp* am_prop = BrokerProps_Find(mc.props, + MQTT_PROP_AUTH_METHOD); + /* [MQTT-3.1.2.11.3] Receive Maximum 0 is a Protocol Error. */ + if (rm_prop != NULL && rm_prop->data_short == 0) { + WBLOG_ERR(broker, + "broker: Receive Maximum 0 is a Protocol Error sock=%d " + "[MQTT-3.1.2.11.3]", (int)bc->sock); + ack.return_code = MQTT_REASON_PROTOCOL_ERR; + goto send_connack; + } + /* No Enhanced Authentication support; refuse an Auth Method. */ + if (am_prop != NULL) { + WBLOG_ERR(broker, + "broker: Authentication Method unsupported sock=%d", + (int)bc->sock); + ack.return_code = MQTT_REASON_BAD_AUTH_METHOD; + goto send_connack; + } + } +#endif #if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) - /* [MQTT-3.1.2.11.3] v5 Receive Maximum. If present and non-zero, the - * client is telling us not to exceed this many outbound QoS 1/2 - * PUBLISHes in flight to it. Absent property means 65535 (no - * client-imposed cap). 0 is a protocol error, but tolerate it as - * "unset" rather than disconnecting, to stay friendly to mildly - * non-conforming clients - the actual cap then comes from - * BROKER_MAX_INFLIGHT_PER_SUB alone. */ + /* Capture Receive Maximum and Session Expiry into the persistent-session + * fields, which exist only in the dynamic-memory build. */ if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && mc.props != NULL) { MqttProp* rm_prop = BrokerProps_Find(mc.props, MQTT_PROP_RECEIVE_MAX); - if (rm_prop != NULL && rm_prop->data_short > 0) { + MqttProp* se_prop = BrokerProps_Find(mc.props, + MQTT_PROP_SESSION_EXPIRY_INTERVAL); + if (rm_prop != NULL) { bc->client_receive_max = rm_prop->data_short; WBLOG_DBG(broker, "broker: client Receive Maximum sock=%d value=%u", (int)bc->sock, (unsigned)bc->client_receive_max); } - /* [MQTT-3.1.2.11.2] v5 Session Expiry Interval. If present, - * carry it onto bc->session_expiry_sec so the disconnect - * path stamps it into the orphan record. Absent property - * means the default set above stands (0 for clean_session=1, - * 0xFFFFFFFF for clean_session=0 to honor v3.1.1 persistence - * semantics when a v5 client opts in without the property). */ - { - MqttProp* se_prop = BrokerProps_Find(mc.props, - MQTT_PROP_SESSION_EXPIRY_INTERVAL); - if (se_prop != NULL) { - bc->session_expiry_sec = se_prop->data_int; - WBLOG_DBG(broker, - "broker: client Session Expiry sock=%d value=%u", - (int)bc->sock, (unsigned)bc->session_expiry_sec); - } + /* [MQTT-3.1.2.11.2] v5 Session Expiry Interval. Absent means the + * default set above stands (always 0 for v5). */ + if (se_prop != NULL) { + bc->session_expiry_sec = se_prop->data_int; + WBLOG_DBG(broker, + "broker: client Session Expiry sock=%d value=%u", + (int)bc->sock, (unsigned)bc->session_expiry_sec); } } #endif @@ -4740,6 +5168,26 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #endif /* WOLFMQTT_BROKER_AUTH */ +#ifndef WOLFMQTT_BROKER_WILL + /* Refuse a Will-bearing CONNECT before any session takeover/reclaim so a + * rejected connection does not first destroy the client's prior session. */ + if (mc.enable_lwt) { + WBLOG_ERR(broker, + "broker: Will not supported (WOLFMQTT_BROKER_WILL disabled) " + "sock=%d", (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_IMPL_SPECIFIC_ERR; + } + else + #endif + { + ack.return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL; + } + goto send_connack; + } +#endif + if (BROKER_STR_VALID(bc->client_id)) { BrokerClient* old; @@ -4778,12 +5226,30 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, ((BrokerWsCtx*)old->ws_ctx)->processing = 0; } #endif - if (!mc.clean_session) { + /* Reassociate only if the old session survives the takeover. + * [MQTT-3.1.2.11.2] a zero Session Expiry ends the old session when + * its connection closes - which the takeover is doing - so its subs + * and QoS2 state must not carry into the new client. Matches the + * old_persists check the QoS2 takeover helper already applies. */ + if (!mc.clean_session + #ifndef WOLFMQTT_STATIC_MEMORY + && old->session_expiry_sec != 0 + #endif + ) { /* Reassociate old client's subs to new client */ if (BrokerSubs_ReassociateClient(broker, bc->client_id, bc) > 0) { session_present = 1; } + #if WOLFMQTT_MAX_QOS >= 2 + /* Carry inbound QoS2 dedup state so a retransmit after the + * takeover is still recognized, but only from a surviving + * session (see helper). Transferred state is Session state, so + * report Session Present even when there are no subs. */ + if (BrokerInboundQos2_Takeover(bc, old)) { + session_present = 1; + } + #endif } BrokerSubs_RemoveClient(broker, old); BrokerClient_Remove(broker, old); @@ -4882,9 +5348,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, MqttProp* prop = BrokerProps_Find(mc.lwt_msg->props, MQTT_PROP_WILL_DELAY_INTERVAL); if (prop != NULL) { - /* Clamp to a sane maximum so a client advertising a huge - * delay (e.g. UINT32_MAX) cannot monopolize a pending-will - * slot indefinitely. */ + /* [MQTT-3.1.2.11.5] Honor the negotiated value, but clamp to a + * sane maximum so a client advertising a huge delay cannot + * hold a pending-will slot indefinitely. */ if (prop->data_int > BROKER_MAX_WILL_DELAY_SEC) { bc->will_delay_sec = BROKER_MAX_WILL_DELAY_SEC; } @@ -5133,7 +5599,7 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, { sub_rc = BrokerSubs_Add(broker, bc, f, flen, topic_qos); } - if (sub_rc != MQTT_CODE_SUCCESS) { + if (sub_rc < 0) { granted_qos = (MqttQoS)fail_code; #ifdef WOLFMQTT_V5 /* A capacity rejection (per-client cap or full table) maps to @@ -5146,9 +5612,27 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, } #ifdef WOLFMQTT_BROKER_RETAINED else { - /* Deliver retained messages matching this filter. */ + /* [MQTT-3.3.1-9..11] Retain Handling: 0 always, 1 only + * if new (sub_rc == 1), 2 never. */ + byte deliver_retained = 1; char filter_z[BROKER_MAX_FILTER_LEN]; word16 copy_len = flen; + #ifdef WOLFMQTT_V5 + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + byte rh = sub.topics[i].sub_options & + (MQTT_SUBSCRIBE_RETAIN_HANDLING_0 | + MQTT_SUBSCRIBE_RETAIN_HANDLING_1 | + MQTT_SUBSCRIBE_RETAIN_HANDLING_2); + if (rh == MQTT_SUBSCRIBE_RETAIN_HANDLING_2) { + deliver_retained = 0; + } + else if (rh == MQTT_SUBSCRIBE_RETAIN_HANDLING_1 && + sub_rc != 1) { + deliver_retained = 0; + } + } + #endif + if (deliver_retained) { #ifndef WOLFMQTT_STATIC_MEMORY /* Dynamic builds store filters longer than the stack buffer in * full; use a heap copy so retained matching uses the same @@ -5172,6 +5656,7 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, BrokerRetained_DeliverToClient(broker, bc, filter_z, topic_qos); } + } } #endif } @@ -5642,7 +6127,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, } else { BrokerOutPub* e = BrokerOutPub_Alloc(topic, payload, - pub.total_len); + pub.total_len + #ifdef WOLFMQTT_V5 + , pub.props + #endif + ); if (e == NULL) { WBLOG_ERR(broker, "broker: PUBLISH fwd alloc failed sock=%d " @@ -5691,7 +6180,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, BrokerOrphan_Find(broker, sub->client_id); if (o != NULL) { BrokerOrphan_Enqueue(broker, o, topic, payload, - pub.total_len, eff_qos, 0); + pub.total_len, eff_qos, 0 + #ifdef WOLFMQTT_V5 + , pub.props + #endif + ); } } } @@ -6027,12 +6520,14 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) #ifndef WOLFMQTT_STATIC_MEMORY { MqttPublishResp ack_resp; + int ack_rc; XMEMSET(&ack_resp, 0, sizeof(ack_resp)); #ifdef WOLFMQTT_V5 ack_resp.protocol_level = bc->protocol_level; #endif - if (MqttDecode_PublishResp(bc->rx_buf, rc, - MQTT_PACKET_TYPE_PUBLISH_ACK, &ack_resp) >= 0) { + ack_rc = MqttDecode_PublishResp(bc->rx_buf, rc, + MQTT_PACKET_TYPE_PUBLISH_ACK, &ack_resp); + if (ack_rc >= 0) { BrokerClient_OnPubAck(bc, ack_resp.packet_id); } #ifdef WOLFMQTT_V5 @@ -6040,6 +6535,10 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) (void)MqttProps_Free(ack_resp.props); } #endif + if (BrokerRcIsFatal(ack_rc)) { + BrokerClient_AbnormalClose(broker, bc); + return 0; + } } #endif break; @@ -6073,12 +6572,14 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) #ifndef WOLFMQTT_STATIC_MEMORY { MqttPublishResp comp_resp; + int comp_rc; XMEMSET(&comp_resp, 0, sizeof(comp_resp)); #ifdef WOLFMQTT_V5 comp_resp.protocol_level = bc->protocol_level; #endif - if (MqttDecode_PublishResp(bc->rx_buf, rc, - MQTT_PACKET_TYPE_PUBLISH_COMP, &comp_resp) >= 0) { + comp_rc = MqttDecode_PublishResp(bc->rx_buf, rc, + MQTT_PACKET_TYPE_PUBLISH_COMP, &comp_resp); + if (comp_rc >= 0) { BrokerClient_OnPubComp(bc, comp_resp.packet_id); } #ifdef WOLFMQTT_V5 @@ -6086,6 +6587,10 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) (void)MqttProps_Free(comp_resp.props); } #endif + if (BrokerRcIsFatal(comp_rc)) { + BrokerClient_AbnormalClose(broker, bc); + return 0; + } } #endif break; @@ -6132,23 +6637,54 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) BrokerClient_AbnormalClose(broker, bc); return 0; } - #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) - /* [MQTT-3.14.4-3] A v5 DISCONNECT with Reason Code 0x04 - * (Disconnect with Will Message) asks the broker to publish - * the Will rather than discard it. */ + #ifdef WOLFMQTT_V5 + /* Always decode v5 DISCONNECT props, not only with Will. */ if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && bc->client.packet.remain_len > 0) { MqttDisconnect disc; + int disc_rc; XMEMSET(&disc, 0, sizeof(disc)); disc.protocol_level = bc->protocol_level; - if (MqttDecode_Disconnect(bc->rx_buf, rc, &disc) >= 0 && - disc.reason_code == + disc_rc = MqttDecode_Disconnect(bc->rx_buf, rc, &disc); + if (disc_rc >= 0) { + #ifndef WOLFMQTT_STATIC_MEMORY + /* [MQTT-3.14.2-1] 0-to-nonzero here is a Protocol + * Error; any other change is permitted. */ + MqttProp* se_prop = (disc.props != NULL) ? + BrokerProps_Find(disc.props, + MQTT_PROP_SESSION_EXPIRY_INTERVAL) : NULL; + if (se_prop != NULL) { + if (bc->session_expiry_sec == 0 && + se_prop->data_int != 0) { + WBLOG_ERR(broker, + "broker: DISCONNECT Session Expiry " + "0->nonzero is a Protocol Error sock=%d " + "[MQTT-3.14.2-1]", (int)bc->sock); + if (disc.props != NULL) { + (void)MqttProps_Free(disc.props); + } + BrokerClient_AbnormalClose(broker, bc); + return 0; + } + bc->session_expiry_sec = se_prop->data_int; + } + #endif + #ifdef WOLFMQTT_BROKER_WILL + /* [MQTT-3.14.4-3] Reason 0x04 requests Will publish. */ + if (disc.reason_code == MQTT_REASON_DISCONNECT_W_WILL_MSG) { - BrokerClient_PublishWill(broker, bc); + BrokerClient_PublishWill(broker, bc); + } + else { + BrokerClient_ClearWill(bc); + } + #endif } + #ifdef WOLFMQTT_BROKER_WILL else { BrokerClient_ClearWill(bc); } + #endif /* Free any decoded v5 DISCONNECT properties. */ if (disc.props != NULL) { (void)MqttProps_Free(disc.props); @@ -6308,6 +6844,18 @@ int MqttBroker_Step(MqttBroker* broker) return MQTT_CODE_SUCCESS; } +#ifndef WOLFMQTT_STATIC_MEMORY + /* Orphan expiry sweep, rate-limited to once per second. */ + { + WOLFMQTT_BROKER_TIME_T now = WOLFMQTT_BROKER_GET_TIME_S(); + if (now < broker->orphan_last_expire_check || + (now - broker->orphan_last_expire_check) >= 1) { + BrokerOrphan_ExpireSweep(broker); + broker->orphan_last_expire_check = now; + } + } +#endif + /* 1. Try to accept new connections (non-blocking) */ /* Plain (non-TLS) listener */ diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 987fa5db8..ef92d1612 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -397,6 +397,57 @@ WOLFMQTT_LOCAL void MqttReadStop(MqttClient* client, MqttMsgStat* stat) } } +#ifdef WOLFMQTT_V5 +/* Serialize the v5 Receive Maximum counter: the reserve runs on the send + * path (lockSend) and the release on the read path (lockRecv), so the shared + * word16 is guarded by lockClient to avoid a lost-update race. The per-message + * recvQuotaHeld flag pairs each reserve with exactly one release so a stray, + * duplicate, or unmatched ack cannot credit the quota twice. */ +/* Atomically test-and-reserve one quota unit under lockClient (fixes the + * unlocked exhaustion race). Returns 1 if a unit is held for this publish + * (already held, or newly reserved), 0 if the quota is exhausted. */ +static int MqttClient_RecvQuotaReserve(MqttClient* client, + MqttPublish* publish) +{ + int reserved = 0; +#ifdef WOLFMQTT_MULTITHREAD + if (wm_SemLock(&client->lockClient) != 0) { + return 1; /* lock failure: do not fail the publish on a quota race */ + } +#endif + if (publish->stat.recvQuotaHeld) { + reserved = 1; + } + else if (client->server_recv_max > 0) { + client->server_recv_max--; + publish->stat.recvQuotaHeld = 1; + reserved = 1; + } +#ifdef WOLFMQTT_MULTITHREAD + wm_SemUnlock(&client->lockClient); +#endif + return reserved; +} + +static void MqttClient_RecvQuotaRelease(MqttClient* client, MqttMsgStat* stat) +{ +#ifdef WOLFMQTT_MULTITHREAD + if (wm_SemLock(&client->lockClient) != 0) { + return; + } +#endif + if (stat->recvQuotaHeld) { + if (client->server_recv_max < client->server_recv_max_negotiated) { + client->server_recv_max++; + } + stat->recvQuotaHeld = 0; + } +#ifdef WOLFMQTT_MULTITHREAD + wm_SemUnlock(&client->lockClient); +#endif +} +#endif /* WOLFMQTT_V5 */ + #ifdef WOLFMQTT_MULTITHREAD /* These RespList functions assume caller has locked client->lockClient mutex */ @@ -567,8 +618,9 @@ int MqttClient_RespList_Find(MqttClient *client, /* Populate client fields from CONNACK server properties so that the * publish/packet-size guards are effective without requiring the * application to register a property callback. */ -static void Handle_ConnectAck_Props(MqttClient* client, MqttProp* props) +static int Handle_ConnectAck_Props(MqttClient* client, MqttProp* props) { + int rc = MQTT_CODE_SUCCESS; MqttProp* prop; for (prop = props; prop != NULL; prop = prop->next) { @@ -610,7 +662,21 @@ static void Handle_ConnectAck_Props(MqttClient* client, MqttProp* props) client->keep_alive_from_server = 1; } #endif + else if (prop->type == MQTT_PROP_RECEIVE_MAX) { + /* [MQTT-3.1.2.11.3]: 0 is a Protocol Error, not clamped. */ + if (prop->data_short == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + break; + } + client->server_recv_max = prop->data_short; + client->server_recv_max_negotiated = prop->data_short; + } + else if (prop->type == MQTT_PROP_TOPIC_ALIAS_MAX) { + /* MQTT v5 [3.1.2.11.8] */ + client->topic_alias_max = prop->data_short; + } } + return rc; } static int Handle_Props(MqttClient* client, MqttProp* props, byte use_cb, @@ -708,7 +774,10 @@ static int MqttClient_DecodePacket(MqttClient* client, byte* rx_buf, * mutate long-lived MqttClient state. */ if (p_connect_ack->return_code == MQTT_CONNECT_ACK_CODE_ACCEPTED) { - Handle_ConnectAck_Props(client, p_connect_ack->props); + tmp = Handle_ConnectAck_Props(client, p_connect_ack->props); + if (tmp != MQTT_CODE_SUCCESS) { + rc = tmp; + } } tmp = Handle_Props(client, p_connect_ack->props, (packet_obj != NULL), 1); @@ -1158,6 +1227,17 @@ static int MqttClient_HandlePacket(MqttClient* client, return rc; } +/* [MQTT-4.13.1] Malformed/protocol-invalid data requires disconnect. */ +static int MqttClient_IsFatalProtoError(int rc) +{ + return (rc == MQTT_CODE_ERROR_MALFORMED_DATA || + rc == MQTT_CODE_ERROR_PACKET_TYPE || + rc == MQTT_CODE_ERROR_PACKET_ID || + rc == MQTT_CODE_ERROR_PROPERTY || + rc == MQTT_CODE_ERROR_PROPERTY_MISMATCH || + rc == MQTT_CODE_ERROR_SERVER_PROP); +} + static inline int MqttIsPubRespPacket(int packet_type) { return (packet_type == MQTT_PACKET_TYPE_PUBLISH_ACK /* Acknowledgment */ || @@ -1302,6 +1382,7 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, #endif MqttMsgStat* mms_stat; int waitMatchFound; + int recvFatal = 0; void* use_packet_obj = NULL; if (client == NULL || packet_obj == NULL) { @@ -1561,6 +1642,10 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, } } /* switch (mms_stat->read) */ + /* Record whether the failure came from decoding/handling received data; + * a local ack-encode failure below must not tear down a healthy link. */ + recvFatal = (rc < 0); + switch (mms_stat->ack) { case MQTT_MSG_BEGIN: @@ -1674,6 +1759,17 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, MqttClient_ReturnCodeToString(rc), rc); } #endif + /* Keep IS_CONNECTED honest after a fatal error so the caller sees a + * dead connection. Only clear the flag; the application tears the + * transport down via MqttClient_NetDisconnect. Freeing it here would + * disconnect twice (with curl, double curl_global_cleanup) and could + * race a concurrent publisher inside wolfSSL_write in MT builds. Gated + * on recvFatal so a local ack-encode failure (e.g. an out-of-table + * Reason Code) does not disconnect an otherwise healthy peer. */ + if (recvFatal && MqttClient_IsFatalProtoError(rc) && + (client->flags & MQTT_CLIENT_FLAG_IS_CONNECTED) != 0) { + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_IS_CONNECTED, 0); + } return rc; } @@ -1735,6 +1831,11 @@ int MqttClient_Init(MqttClient *client, MqttNet* net, client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS; client->retain_avail = 1; client->protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL; + /* [MQTT-3.1.2.11.3]: absent Receive Maximum means 65535. */ + client->server_recv_max = 65535; + client->server_recv_max_negotiated = 65535; + /* [MQTT-3.1.2.11.8]: absent Topic Alias Maximum means none accepted. */ + client->topic_alias_max = 0; rc = MqttProps_Init(); #endif @@ -1868,6 +1969,9 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS; client->retain_avail = 1; client->packet_sz_max = 0; + client->server_recv_max = 65535; + client->server_recv_max_negotiated = 65535; + client->topic_alias_max = 0; #endif /* Encode the connect packet */ @@ -2335,6 +2439,18 @@ static int MqttClient_Publish_WritePayload(MqttClient *client, return rc; } +#ifdef WOLFMQTT_V5 +/* Give back the Receive Maximum unit reserved for a QoS>0 v5 PUBLISH when it + * terminates without a matching PUBACK/PUBCOMP (write failure, cancel, + * ack-wait timeout, PUBREC rejection). Clamped to the negotiated ceiling so a + * later stray ack for the same id cannot inflate the quota past it. */ +static void MqttClient_RestoreRecvQuota(MqttClient* client, + MqttPublish* publish) +{ + MqttClient_RecvQuotaRelease(client, &publish->stat); +} +#endif + static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, MqttPublishCb pubCb, int writeOnly) { @@ -2356,6 +2472,21 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); } + + /* [MQTT-3.3.2.3.4]: reject a Topic Alias over CONNACK's maximum. */ + if (client->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + MqttProp* prop; + for (prop = publish->props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_TOPIC_ALIAS) { + if ((prop->data_short == 0) || + (prop->data_short > client->topic_alias_max)) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + } + break; + } + } + } + #endif switch (publish->stat.write) @@ -2367,6 +2498,23 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, return rc; } + #ifdef WOLFMQTT_V5 + /* [MQTT-3.1.2.11.3]: atomically reserve one flow-control unit, + * refusing once the quota is exhausted. Released when the publish + * terminates. Only reached in MQTT_MSG_BEGIN, so once per logical + * publish. Write-only publishes are excluded: a reader thread owns + * their ack, so a per-object reservation cannot be released + * reliably across that hand-off. */ + if (!writeOnly && + client->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + publish->qos > MQTT_QOS_0 && + !MqttClient_RecvQuotaReserve(client, publish)) + { + MqttWriteStop(client, &publish->stat); + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + } + #endif + /* Encode the publish packet */ rc = MqttEncode_Publish(client->tx_buf, client->tx_buf_len, publish, pubCb ? 1 : 0); @@ -2379,6 +2527,9 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, #endif if (rc <= 0) { MqttWriteStop(client, &publish->stat); + #ifdef WOLFMQTT_V5 + MqttClient_RestoreRecvQuota(client, publish); + #endif return rc; } client->write.len = rc; @@ -2398,6 +2549,9 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, } if (rc != 0) { MqttWriteStop(client, &publish->stat); + #ifdef WOLFMQTT_V5 + MqttClient_RestoreRecvQuota(client, publish); + #endif return rc; /* Error locking client */ } } @@ -2429,6 +2583,9 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, if (rc != xfer) { MqttWriteStop(client, &publish->stat); MqttClient_CancelMessage(client, (MqttObject*)publish); + #ifdef WOLFMQTT_V5 + MqttClient_RestoreRecvQuota(client, publish); + #endif return rc; } @@ -2443,10 +2600,18 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, #ifdef WOLFMQTT_NONBLOCK if (rc == MQTT_CODE_CONTINUE || rc == MQTT_CODE_PUB_CONTINUE) return rc; + #else + /* Chunked publish requests the next payload chunk; not terminal, + * so return without releasing the reserved quota. */ + if (rc == MQTT_CODE_PUB_CONTINUE) + return rc; #endif MqttWriteStop(client, &publish->stat); if (rc < 0) { MqttClient_CancelMessage(client, (MqttObject*)publish); + #ifdef WOLFMQTT_V5 + MqttClient_RestoreRecvQuota(client, publish); + #endif break; } @@ -2490,6 +2655,18 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, publish->packet_id, client->cmd_timeout_ms); #ifdef WOLFMQTT_V5 + /* Replenish the reserved quota unit only on a real + * acknowledgement. rc is still SUCCESS here for both a clean + * PUBACK/PUBCOMP and a rejecting reason code (reclassified to + * PUBLISH_REJECTED just below), which both free the unit + * [MQTT-4.9]. A timeout leaves the PUBLISH unacknowledged on + * a still-open connection - the server keeps counting it + * against Receive Maximum - so crediting here would let the + * client exceed the negotiated quota. */ + if (rc == MQTT_CODE_SUCCESS) { + MqttClient_RestoreRecvQuota(client, publish); + } + /* A v5 broker can acknowledge a QoS>0 PUBLISH at the * protocol layer yet still reject the message via a * PUBACK/PUBCOMP reason code >= 0x80 (e.g. not authorized, @@ -3338,6 +3515,13 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) mms_stat->write = MQTT_MSG_BEGIN; mms_stat->read = MQTT_MSG_BEGIN; + /* Do not credit the reserved Receive Maximum unit here. Cancelling an + * abandoned QoS>0 publish that already reached the wire must retain the + * unit while the connection stays open - the server still counts it against + * Receive Maximum [MQTT-4.9], and crediting it would let the client exceed + * the quota. The genuinely-unsent write-failure paths release explicitly via + * MqttClient_RestoreRecvQuota, so no quota leaks there. */ + #ifdef WOLFMQTT_MULTITHREAD /* Remove any pending responses expected */ rc = wm_SemLock(&client->lockClient); diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 7b7e766c9..4dee99107 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -269,6 +269,39 @@ static int MqttPacket_UnsubAckReasonCodeValid(byte code) } return 0; } + +/* [MQTT-3.4.2.1/3.6.2.1/3.7.2.1] Reason Code allow-list per packet type. */ +static int MqttPacket_PublishRespReasonCodeValid(byte type, byte code) +{ + if (type == MQTT_PACKET_TYPE_PUBLISH_REL || + type == MQTT_PACKET_TYPE_PUBLISH_COMP) { + switch (code) { + case MQTT_REASON_SUCCESS: /* 0x00 */ + case MQTT_REASON_PACKET_ID_NOT_FOUND: /* 0x92 */ + return 1; + default: + break; + } + return 0; + } + + /* PUBACK / PUBREC */ + switch (code) { + case MQTT_REASON_SUCCESS: /* 0x00 */ + case MQTT_REASON_NO_MATCH_SUB: /* 0x10 */ + case MQTT_REASON_UNSPECIFIED_ERR: /* 0x80 */ + case MQTT_REASON_IMPL_SPECIFIC_ERR: /* 0x83 */ + case MQTT_REASON_NOT_AUTHORIZED: /* 0x87 */ + case MQTT_REASON_TOPIC_NAME_INVALID: /* 0x90 */ + case MQTT_REASON_PACKET_ID_IN_USE: /* 0x91 */ + case MQTT_REASON_QUOTA_EXCEEDED: /* 0x97 */ + case MQTT_REASON_PAYLOAD_FORMAT_INVALID: /* 0x99 */ + return 1; + default: + break; + } + return 0; +} #endif /* Validate an MQTT Topic Filter against the syntax rules from @@ -553,7 +586,6 @@ int MqttEncode_Int(byte* buf, word32 len) return MQTT_DATA_INT_SIZE; } -#ifndef WOLFMQTT_NO_UTF8_VALIDATION /* MQTT 3.1.1 section 1.5.3 / v5 section 1.5.4: validate that the given byte sequence * is a well-formed MQTT UTF-8 encoded string. This combines: * [MQTT-1.5.3-1] RFC 3629 well-formedness (no overlongs, no surrogate @@ -630,6 +662,7 @@ static int Utf8WellFormed(const byte* s, word16 len) return 1; } +#ifndef WOLFMQTT_NO_UTF8_VALIDATION /* [MQTT-1.5.3-1] Returns 1 if an MQTT UTF-8 string field is well-formed * (RFC 3629) and therefore safe for the encoder to emit, 0 otherwise. Empty * strings are well-formed. Encoders call this in their length-computation pass @@ -659,12 +692,10 @@ int MqttDecode_String(byte *buf, const char **pstr, word16 *pstr_len, word32 buf } buf += len; if (str_len > 0) { - #ifndef WOLFMQTT_NO_UTF8_VALIDATION - /* [MQTT-1.5.3-1] Reject ill-formed UTF-8 (RFC 3629). */ + /* [MQTT-1.5.3-1] Reject ill-formed UTF-8; mandatory, not gate-able. */ if (!Utf8WellFormed(buf, str_len)) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } - #endif /* [MQTT-1.5.3-2] / [MQTT-1.5.4-2]: an MQTT UTF-8 encoded string * MUST NOT include the null character (U+0000). Although U+0000 * is well-formed UTF-8, it is forbidden in MQTT string fields - @@ -796,6 +827,11 @@ int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf) } case MQTT_DATA_TYPE_INT: { + /* [MQTT-3.1.2.11.4] Maximum Packet Size 0 is a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_MAX_PACKET_SZ && + cur_prop->data_int == 0) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } tmp = MqttEncode_Int(buf, cur_prop->data_int); rc += tmp; if (buf != NULL) { @@ -922,6 +958,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, int rc = 0; int total, tmp; int prop_count = 0; + int saw_auth_method = 0, saw_auth_data = 0; word32 seen_lo = 0, seen_hi = 0; /* singleton-property duplicate guard */ word32 prop_type; MqttProp* cur_prop; @@ -973,11 +1010,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, break; } - /* [MQTT v5 2.2.2.2] Every property except User Property and - * Subscription Identifier MUST appear at most once; a duplicate is a - * Protocol Error. */ + /* [MQTT-2.2.2.2] Duplicate is a Protocol Error, except User Property + * and Subscription Identifier in PUBLISH [MQTT-3.3.2.3.8]. */ if (cur_prop->type != MQTT_PROP_USER_PROP && - cur_prop->type != MQTT_PROP_SUBSCRIPTION_ID) { + !(cur_prop->type == MQTT_PROP_SUBSCRIPTION_ID && + packet == MQTT_PACKET_TYPE_PUBLISH)) { word32 bit; if (cur_prop->type < 32) { bit = (word32)1 << cur_prop->type; @@ -997,6 +1034,14 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, } } + /* Tracked for the Auth Data/Method cross-check below the loop. */ + if (cur_prop->type == MQTT_PROP_AUTH_METHOD) { + saw_auth_method = 1; + } + else if (cur_prop->type == MQTT_PROP_AUTH_DATA) { + saw_auth_data = 1; + } + switch (gPropMatrix[cur_prop->type].data) { case MQTT_DATA_TYPE_BYTE: @@ -1060,6 +1105,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= tmp; + /* [MQTT-3.1.2.11.4] Maximum Packet Size 0 is a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_MAX_PACKET_SZ && + cur_prop->data_int == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } break; } case MQTT_DATA_TYPE_STRING: @@ -1226,6 +1276,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, } }; + /* [MQTT-3.1.2.11.9/10] Auth Data requires Auth Method. */ + if (rc >= 0 && saw_auth_data && !saw_auth_method) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } + if (rc < 0) { /* Free the property */ MqttProps_Free(*props); @@ -1489,6 +1544,20 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect) return header_len + remain_len; } +#if defined(WOLFMQTT_BROKER) && defined(WOLFMQTT_V5) +/* [MQTT-3.1.3.2] Will Properties allow-list, tighter than CONNECT's. */ +static int MqttWillProps_ValidateType(MqttPropertyType type) +{ + return (type == MQTT_PROP_WILL_DELAY_INTERVAL) || + (type == MQTT_PROP_PAYLOAD_FORMAT_IND) || + (type == MQTT_PROP_MSG_EXPIRY_INTERVAL) || + (type == MQTT_PROP_CONTENT_TYPE) || + (type == MQTT_PROP_RESP_TOPIC) || + (type == MQTT_PROP_CORRELATION_DATA) || + (type == MQTT_PROP_USER_PROP); +} +#endif /* WOLFMQTT_BROKER && WOLFMQTT_V5 */ + #ifdef WOLFMQTT_BROKER int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) { @@ -1678,6 +1747,7 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) if (mc_connect->protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_5) { word32 lwt_props_len = 0; int lwt_tmp; + MqttProp* will_prop; /* Decode Length of LWT Properties */ if (rx_buf_len < (rx_payload - rx_buf)) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); @@ -1691,7 +1761,7 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) } rx_payload += lwt_tmp; if (lwt_props_len > 0) { - /* Decode LWT Properties */ + /* Decode, then enforce the tighter Will allow-list below. */ lwt_tmp = MqttDecode_Props(MQTT_PACKET_TYPE_CONNECT, &mc_connect->lwt_msg->props, rx_payload, (word32)(rx_buf_len - (rx_payload - rx_buf)), @@ -1701,6 +1771,15 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) goto cleanup; } rx_payload += lwt_tmp; + + /* [MQTT-3.1.3.2] Reject CONNECT-only properties in Will. */ + for (will_prop = mc_connect->lwt_msg->props; + will_prop != NULL; will_prop = will_prop->next) { + if (!MqttWillProps_ValidateType(will_prop->type)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + goto cleanup; + } + } } } #endif @@ -2359,6 +2438,11 @@ int MqttEncode_PublishResp(byte* tx_buf, int tx_buf_len, byte type, #ifdef WOLFMQTT_V5 if (publish_resp->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + /* [MQTT-3.4.2.1/3.6.2.1/3.7.2.1] Reject an out-of-table Reason Code. */ + if (!MqttPacket_PublishRespReasonCodeValid(type, + publish_resp->reason_code)) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } if (publish_resp->props != NULL) { /* Determine length of properties */ props_len = MqttEncode_Props((MqttPacketType)type, diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 8b6df8d2b..2eb581356 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -48,6 +48,12 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, else { XMEMSET(p_info, 0, sizeof(SN_GwInfo)); } + /* Default to the struct's own backing storage so the decoder + * never writes through a NULL gwAddr, but honor a + * caller-supplied destination if one was set. */ + if (p_info->gwAddr == NULL) { + p_info->gwAddr = &p_info->gwAddrBuf; + } rc = SN_Decode_GWInfo(client->rx_buf, client->packet.buf_len, p_info); diff --git a/src/mqtt_sn_packet.c b/src/mqtt_sn_packet.c index cd75f533d..104832e08 100644 --- a/src/mqtt_sn_packet.c +++ b/src/mqtt_sn_packet.c @@ -352,6 +352,10 @@ int SN_Decode_GWInfo(byte *rx_buf, int rx_buf_len, SN_GwInfo *gw_info) if (addr_len > (word16)sizeof(SN_GwAddr)) { addr_len = (word16)sizeof(SN_GwAddr); } + /* No destination to write the optional address into. */ + if (gw_info->gwAddr == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } XMEMCPY(gw_info->gwAddr, rx_payload, addr_len); } } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 5721c3df1..b0e5ba699 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -3123,15 +3123,16 @@ TEST(connack_session_present_v5_set_on_resumed_session) MqttConnectAck ack; int rc; int i; - /* v5 CONNECT clean=0, level=5, props_len=0, client_id="K". remain - * = 6 + 1 + 1 + 2 + 1 + 3 = 14. */ + /* v5 CONNECT clean=0, level=5, Session Expiry=60 (persistent session so a + * same-id reconnect resumes it), client_id="K". props = 5 (0x11 + u32). + * remain = 6 + 1 + 1 + 2 + (1+5) + 3 = 19. */ static const byte connect0[] = { - 0x10, 0x0E, + 0x10, 0x13, 0x00, 0x04, 'M', 'Q', 'T', 'T', 0x05, 0x00, /* clean_start = 0 */ 0x00, 0x3C, - 0x00, /* properties length = 0 */ + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C,/* props_len=5, SessionExpiry=60 */ 0x00, 0x01, 'K' }; static const byte subscribe0[] = { @@ -3147,12 +3148,12 @@ TEST(connack_session_present_v5_set_on_resumed_session) * properties. remain = 1 + 1 = 2. */ static const byte disconnect0[] = { 0xE0, 0x02, 0x00, 0x00 }; static const byte connect1[] = { - 0x10, 0x0E, + 0x10, 0x13, 0x00, 0x04, 'M', 'Q', 'T', 'T', 0x05, 0x00, 0x00, 0x3C, - 0x00, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C,/* props_len=5, SessionExpiry=60 */ 0x00, 0x01, 'K' }; @@ -3586,111 +3587,1452 @@ TEST(retained_qos_stored_2_sub_0_delivers_qos0) } #endif /* WOLFMQTT_BROKER_RETAINED */ -/* -------------------------------------------------------------------------- */ -/* Runner */ -/* -------------------------------------------------------------------------- */ +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* QoS 2 dedup state must survive a disconnect/reconnect cycle; a + * retransmitted PUBLISH must not be re-fanned-out to the subscriber. */ +TEST(qos2_dedup_survives_disconnect_reconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int sub_pubs; + int reconnect_pubrecs; + static const byte connect_sub[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 'S', 'u', 'b' + }; + static const byte subscribe_x[] = { + 0x82, 0x06, + 0x00, 0x01, + 0x00, 0x01, 'x', + 0x02 + }; + /* Publisher CONNECT clean_session=0 (persistent), ClientId "Pub". */ + static const byte connect_pub[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x03, 'P', 'u', 'b' + }; + static const byte publish_qos2[] = { + 0x34, 0x0A, + 0x00, 0x01, 'x', + 0x00, 0x07, + 'f', 'i', 'r', 's', 't' + }; + /* Same PUBLISH, DUP=1, retransmitted after reconnect. */ + static const byte publish_qos2_dup[] = { + 0x3C, 0x0A, + 0x00, 0x01, 'x', + 0x00, 0x07, + 'f', 'i', 'r', 's', 't' + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; -int main(int argc, char** argv) + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + /* Phase 1: subscriber + publisher's first connection. Publisher + * sends the QoS 2 PUBLISH, then disconnects WITHOUT a PUBREL - the + * broker's dedup state for packet_id=7 must be preserved, not + * dropped, by the disconnect cleanup. */ + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + mock_client_input_append(1, publish_qos2, sizeof(publish_qos2)); + mock_client_input_append(1, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + sub_pubs = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + ASSERT_EQ(1, sub_pubs); /* delivered exactly once so far */ + + /* Phase 2: publisher reconnects with the SAME Client Identifier and + * retransmits the identical QoS 2 PUBLISH (DUP=1, same packet_id). + * The reconnect must reclaim the dedup state before this PUBLISH is + * processed. */ + g_clients_active = 3; + mock_client_input_append(2, connect_pub, sizeof(connect_pub)); + mock_client_input_append(2, publish_qos2_dup, sizeof(publish_qos2_dup)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + } + + reconnect_pubrecs = count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH_REC); + /* The QoS 2 handshake with the reconnected publisher must still + * complete (PUBREC sent)... */ + ASSERT_EQ(1, reconnect_pubrecs); + /* ...but the retransmit must NOT be re-fanned-out to the + * subscriber. Pre-fix, BrokerOrphan_Take never carried + * bc->qos2_pending into the orphan record (only out_q was + * transferred) and BrokerClient_Free's BrokerInboundQos2_Clear then + * discarded it, so the reconnected publisher's retransmit was + * treated as brand new: it would be fanned out a second time here, + * making this total 2. */ + sub_pubs = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + ASSERT_EQ(1, sub_pubs); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* [MQTT-3.1.2.11.3]: Receive Maximum = 0 in CONNECT must close, not "unset". */ +TEST(connect_v5_receive_max_zero_protocol_error) { - (void)argc; (void)argv; + MqttBroker broker; + MqttBrokerNet net; + /* v5 CONNECT, ClientId "A", props: Receive Maximum (0x21) = 0x0000. + * remain = 6("MQTT")+1(level)+1(flags)+2(keepalive)+1(props_len)+ + * 3(prop TLV)+2(clientid_len)+1(clientid) = 17 */ + static const byte connect[] = { + 0x10, 17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x02, /* CleanStart = 1 */ + 0x00, 0x3C, + 0x03, /* props_len = 3 */ + 0x21, 0x00, 0x00, /* Receive Maximum = 0 */ + 0x00, 0x01, 'A' + }; - TEST_RUNNER_BEGIN(); + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); - TEST_SUITE_BEGIN("broker_connect", setup, teardown); - RUN_TEST(connect_v311_emptyid_clean0_refused); - RUN_TEST(connect_v311_emptyid_clean1_accepted); - RUN_TEST(connect_v311_nonempty_clean0_accepted); - RUN_TEST(connect_v311_explicit_auto_prefix_refused); - RUN_TEST(connect_unsupported_level_3_refused); - RUN_TEST(connect_unsupported_level_6_refused); - RUN_TEST(connect_unsupported_level_127_refused); -#ifdef WOLFMQTT_BROKER_AUTH - RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); - RUN_TEST(connect_v311_binary_password_exact_match_accepted); - RUN_TEST(connect_auth_username_length_fold_repeating_byte_refused); - RUN_TEST(connect_unauth_client_id_does_not_take_over_victim); - RUN_TEST(connect_auth_user_only_start_rejected); - RUN_TEST(connect_auth_pass_only_start_rejected); - RUN_TEST(connect_auth_partial_config_fails_closed); - RUN_TEST(connect_auth_partial_pass_only_fails_closed); - RUN_TEST(broker_set_auth_pass_valid); - RUN_TEST(broker_set_auth_pass_max_len_valid); - RUN_TEST(broker_set_auth_pass_too_long_rejected); - RUN_TEST(broker_set_auth_pass_too_long_wipes_prior); - RUN_TEST(broker_set_auth_pass_shorter_second_no_residue); -#ifndef WOLFMQTT_STATIC_MEMORY - RUN_TEST(connect_credentials_scrubbed_after_accept); -#endif -#endif -#ifdef WOLFMQTT_V5 - RUN_TEST(connect_v5_emptyid_assigned_id_emitted); - RUN_TEST(connect_v5_emptyid_clean0_accepted); -#endif - RUN_TEST(qos2_duplicate_publish_dedup); - RUN_TEST(qos2_phantom_dup_publish_is_fresh); - RUN_TEST(qos2_publish_after_pubrel_is_fresh); - RUN_TEST(qos2_inbound_cap_reached_disconnects); - RUN_TEST(qos2_state_freed_on_client_disconnect); - RUN_TEST(qos2_pubrel_unknown_id_still_pubcomps); - RUN_TEST(qos2_publish_with_offline_durable_subscriber); - RUN_TEST(qos2_publish_then_abrupt_close_offline_subscriber); -#ifndef WOLFMQTT_STATIC_MEMORY -#ifdef WOLFMQTT_V5 - RUN_TEST(online_qos1_flood_disconnects_slow_v5_subscriber); -#endif /* WOLFMQTT_V5 */ - RUN_TEST(online_qos1_flood_disconnects_slow_v311_subscriber); - RUN_TEST(online_qos1_at_cap_keeps_subscriber); -#endif /* !WOLFMQTT_STATIC_MEMORY */ -#ifdef WOLFMQTT_V5 - RUN_TEST(qos2_publish_v5_props_with_offline_durable_subscriber); -#endif - RUN_TEST(pingreq_valid_emits_pingresp); - RUN_TEST(pingreq_nonzero_remain_len_closes_no_pingresp); -#ifndef WOLFMQTT_V5 - RUN_TEST(disconnect_v311_nonzero_remain_len_fires_will); -#endif - RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); -#if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) - RUN_TEST(broker_will_scrub_after_failed_write); -#endif - RUN_TEST(broker_unhandled_packet_type_closes); - RUN_TEST(broker_publish_before_connect_closes); -#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) - RUN_TEST(broker_retained_list_capped); - RUN_TEST(broker_retained_clock_rollback_not_expired); - RUN_TEST(broker_retained_scrub_after_completed_write); -#endif -#ifndef WOLFMQTT_STATIC_MEMORY - RUN_TEST(broker_per_client_subscription_cap); -#endif -#ifdef WOLFMQTT_V5 - RUN_TEST(broker_publish_with_subscription_id_closes); -#endif - RUN_TEST(broker_subscribe_packet_id_zero_closes); - RUN_TEST(connack_session_present_set_on_resumed_session); - RUN_TEST(connack_session_present_set_on_takeover); - RUN_TEST(connack_session_present_clear_on_clean_session_reconnect); -#ifdef WOLFMQTT_V5 - RUN_TEST(connack_session_present_v5_set_on_resumed_session); -#endif -#ifndef WOLFMQTT_STATIC_MEMORY - RUN_TEST(broker_suback_reserved_v311_code_rejected); - RUN_TEST(broker_suback_valid_v311_failure_code_encoded); -#endif -#ifndef WOLFMQTT_BROKER_WILDCARDS - RUN_TEST(broker_no_wildcards_suback_failure_for_wildcard_filter); - RUN_TEST(broker_no_wildcards_suback_grants_plain_filter); -#ifdef WOLFMQTT_V5 - RUN_TEST(broker_no_wildcards_suback_v5_reason_code); -#endif -#endif -#ifdef WOLFMQTT_BROKER_RETAINED - RUN_TEST(retained_qos_stored_1_sub_1_delivers_qos1); - RUN_TEST(retained_qos_stored_2_sub_1_delivers_qos1); - RUN_TEST(retained_qos_stored_1_sub_0_delivers_qos0); - RUN_TEST(retained_qos_stored_0_sub_1_delivers_qos0); - RUN_TEST(retained_qos_stored_2_sub_2_delivers_qos2); - RUN_TEST(retained_qos_stored_2_sub_0_delivers_qos0); + reset_mock_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 4); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* v5 CONNECT with Clean Start=0, no Session Expiry: must default to 0, + * not the v3.1.1 0xFFFFFFFF "never" sentinel. */ +TEST(connect_v5_clean0_no_se_prop_orphan_expiry_zero) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + /* v5 CONNECT clean=0, no props, ClientId "K". remain = 14. */ + static const byte connect0[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'K' + }; + static const byte subscribe0[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'k', + 0x00 + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect0, sizeof(connect0)); + mock_client_input_append(0, subscribe0, sizeof(subscribe0)); + mock_client_input_append(0, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + /* [MQTT-3.1.2.11.2] A v5 Clean Start=0 CONNECT without a Session Expiry + * property defaults the interval to 0, so the Session ends when the + * connection closes: no orphan is retained and a same-id reconnect gets a + * fresh session. */ + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K") != 0)) { + o = o->next; + } + ASSERT_TRUE(o == NULL); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* Zero subs + positive Session Expiry must still create an orphan. */ +TEST(disconnect_v5_zero_subs_nonzero_expiry_creates_orphan) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + /* v5 CONNECT clean=0, ClientId "K2", props: Session Expiry Interval + * (0x11) = 60. No SUBSCRIBE at all. remain = 6+1+1+2+1+5+2+2 = 20 */ + static const byte connect0[] = { + 0x10, 20, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x02, 'K', '2' + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect0, sizeof(connect0)); + mock_client_input_append(0, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + /* Pre-fix, BrokerSubs_OrphanClient returned early on count==0 before + * BrokerOrphan_Take was ever called, so no orphan exists here. */ + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K2") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); + ASSERT_EQ((word32)60, o->session_expiry_sec); + ASSERT_EQ(0, o->out_q_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* MqttBroker_Step must sweep expired orphan Sessions. Time is pinned at + * 0, so a finite expiry of 0 is already expired; nonzero never is. */ +TEST(orphan_expire_sweep_removes_zero_expiry_session) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + /* Client "K3": clean=0, no SE prop (expiry defaults to 0 per #7668), + * one sub -> orphan created via the count>0 path. remain = 15 + * (2-byte ClientId "K3"). */ + static const byte connect_k3[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x00, + 0x00, 0x02, 'K', '3' + }; + static const byte subscribe_k3[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'k', + 0x00 + }; + /* Client "K4": clean=0, SE prop = 60, no subs -> orphan created via + * the #7669 count==0 path with a nonzero expiry. remain = 20. */ + static const byte connect_k4[] = { + 0x10, 20, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x02, 'K', '4' + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_k3, sizeof(connect_k3)); + mock_client_input_append(0, subscribe_k3, sizeof(subscribe_k3)); + mock_client_input_append(0, disconnect0, sizeof(disconnect0)); + mock_client_input_append(1, connect_k4, sizeof(connect_k4)); + mock_client_input_append(1, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + ASSERT_TRUE(g_clients[1].closed); + + /* [MQTT-3.1.2.11.2] K3's Session Expiry is 0, so its Session ends when the + * connection closes: no orphan is retained at all (removed immediately at + * disconnect, not left reclaimable for the once-per-second sweep). */ + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K3") != 0)) { + o = o->next; + } + ASSERT_TRUE(o == NULL); + + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K4") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); /* expiry=60, not yet elapsed -> survives */ + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#ifdef WOLFMQTT_BROKER_WILL +/* [MQTT-3.14.2-1]: DISCONNECT setting Session Expiry 0-to-nonzero is a + * Protocol Error; confirmed via the immediate Will fired on abnormal close. */ +TEST(disconnect_v5_session_expiry_0_to_nonzero_protocol_error) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_lwt[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT publisher, ClientId "P", Will flag set (topic "lwt", + * payload "bye"), no CONNECT props, no Will props (delay=0). remain = + * 6+1+1+2+1(props_len=0)+2+1('P')+1(will_props_len=0)+2+3+2+3 = 25 */ + static const byte connect_pub[] = { + 0x10, 25, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x04, /* Will flag, CleanStart=0 */ + 0x00, 0x3C, + 0x00, /* connect props_len = 0 */ + 0x00, 0x01, 'P', + 0x00, /* will props_len = 0 */ + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 DISCONNECT, reason=Normal(0x00), props: Session Expiry Interval + * (0x11) = 30. remain = 1(reason)+1(props_len)+5(prop) = 7 */ + static const byte disconnect_se[] = { + 0xE0, 0x07, + 0x00, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x1E + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + mock_client_input_append(1, disconnect_se, sizeof(disconnect_se)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + } + + /* Pre-fix: SE update was silently ignored (or, for BROKER_WILL + * builds, only read for the Will-reason-code check), no protocol + * error was raised, and the graceful path cleared the Will without + * publishing it -> 0 PUBLISH to the subscriber. */ + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + ASSERT_TRUE(g_clients[1].closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_WILL */ + +#ifdef WOLFMQTT_BROKER_RETAINED +/* [MQTT-3.3.1-9..11]: Retain Handling = 2 must not deliver on subscribe. */ +TEST(subscribe_v5_retain_handling_2_never_delivers) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* Retained QoS 0 PUBLISH, topic "x", payload "r". */ + static const byte publish_retained[] = { + 0x31, 0x04, + 0x00, 0x01, 'x', 'r' + }; + /* v5 CONNECT subscriber, ClientId "S", props_len=0. remain = 14. */ + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE, filter "x", options = Retain Handling 2 (0x20) | + * QoS 0. remain = 2+1+2+1+1 = 7 */ + static const byte subscribe_rh2[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'x', + 0x20 + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_pub, sizeof(connect_pub)); + mock_client_input_append(0, publish_retained, sizeof(publish_retained)); + mock_client_input_append(1, connect_sub, sizeof(connect_sub)); + mock_client_input_append(1, subscribe_rh2, sizeof(subscribe_rh2)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Pre-fix, Retain Handling bits were never read and retained delivery + * was unconditional -> this would be 1. */ + ASSERT_EQ(0, count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_RETAINED */ + +/* v5 PUBLISH properties must survive QoS>=1 fan-out through out_q. */ +TEST(publish_v5_props_survive_queued_delivery) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + PublishInfo info; + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE QoS 1, filter "x", options = QoS1 (0x01). remain = 7 */ + static const byte subscribe_x[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'x', + 0x01 + }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + /* PUBLISH QoS 1, packet_id=5, topic "x", one property (Payload + * Format Indicator = 1), payload "p". remain = + * 2+1+2+1(props_len)+2(prop)+1(payload) = 9 */ + static const byte publish_v5[] = { + 0x32, 0x09, + 0x00, 0x01, 'x', + 0x00, 0x05, + 0x02, + 0x01, 0x01, + 'p' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + mock_client_input_append(1, publish_v5, sizeof(publish_v5)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + info = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); + ASSERT_TRUE(info.found); + /* Pre-fix, BrokerOutPub had no props field and out_pub.props was + * never set, so the encoder would write an empty (1-byte, value 0) + * properties block instead: remain would be 7, not 9. */ + ASSERT_EQ(9, (int)info.remain_len); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#ifdef WOLFMQTT_BROKER_RETAINED +/* An unacked QoS>=1 retained delivery must survive into the orphan's + * out_q on disconnect, same as normal PUBLISH fan-out. */ +TEST(retained_qos1_delivery_survives_via_outq) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* Retained QoS 1 PUBLISH, packet_id=1, topic "x", payload "r". */ + static const byte publish_retained[] = { + 0x33, 0x06, + 0x00, 0x01, 'x', + 0x00, 0x01, + 'r' + }; + /* Subscriber CONNECT v3.1.1, clean_session=0, ClientId "S". */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_x[] = { + 0x82, 0x06, + 0x00, 0x01, + 0x00, 0x01, 'x', + 0x01 + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_pub, sizeof(connect_pub)); + mock_client_input_append(0, publish_retained, sizeof(publish_retained)); + mock_client_input_append(1, connect_sub, sizeof(connect_sub)); + mock_client_input_append(1, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + + /* Pre-fix: retained QoS>=1 delivery was a direct MqttPacket_Write + * with no BrokerOutPub entry, so the orphan's out_q would be empty + * (out_q_count == 0) here. */ + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "S") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); + ASSERT_EQ(1, o->out_q_count); + ASSERT_EQ(1, o->out_q_inflight); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_RETAINED */ + +#ifdef WOLFMQTT_BROKER_WILL +/* A shorter Session Expiry than Will Delay must publish the Will at + * Session end, not the full delay. */ +TEST(pending_will_publish_time_uses_min_of_delay_and_session_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerPendingWill* pw; + /* v5 CONNECT, Will flag, ClientId "M". CONNECT props: Session + * Expiry Interval = 30. Will props: Will Delay Interval = 3600. + * remain = 35 (same shape as will_delay_interval_capped). */ + static const byte connect_m[] = { + 0x10, 35, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x04, + 0x00, 0x3C, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x1E, /* Session Expiry Interval = 30 */ + 0x00, 0x01, 'M', + 0x05, + 0x18, 0x00, 0x00, 0x0E, 0x10, /* Will Delay Interval = 3600 */ + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + static const byte disconnect_bad[] = { 0xE1, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect_m, sizeof(connect_m)); + mock_client_input_append(0, disconnect_bad, sizeof(disconnect_bad)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + pw = broker.pending_wills; + while (pw != NULL && (pw->client_id == NULL || + XSTRCMP(pw->client_id, "M") != 0)) { + pw = pw->next; + } + ASSERT_TRUE(pw != NULL); + /* Pre-fix, BrokerPendingWill_Add computed publish_time from + * will_delay_sec alone (now + 3600); Session Expiry was never + * consulted. */ + ASSERT_EQ((WOLFMQTT_BROKER_TIME_T)30, pw->publish_time); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* A Will Delay Interval above BROKER_MAX_WILL_DELAY_SEC is clamped so a + * client cannot monopolize a deferred-will slot indefinitely. */ +TEST(will_delay_interval_capped) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerPendingWill* pw; + /* v5 CONNECT, Will flag, ClientId "W". CONNECT props: Session + * Expiry Interval = 0xFFFFFFFF. Will props: Will Delay Interval = + * 4600 (> BROKER_MAX_WILL_DELAY_SEC's default of 3600). remain = 35 */ + static const byte connect_w[] = { + 0x10, 35, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x04, + 0x00, 0x3C, + 0x05, + 0x11, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 'W', + 0x05, + 0x18, 0x00, 0x00, 0x11, 0xF8, /* Will Delay Interval = 4600 */ + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* Malformed DISCONNECT (reserved flag bit set) drives the abnormal + * (immediate-teardown) close path, which still defers via + * BrokerPendingWill_Add when will_delay_sec > 0. */ + static const byte disconnect_bad[] = { 0xE1, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect_w, sizeof(connect_w)); + mock_client_input_append(0, disconnect_bad, sizeof(disconnect_bad)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + pw = broker.pending_wills; + while (pw != NULL && (pw->client_id == NULL || + XSTRCMP(pw->client_id, "W") != 0)) { + pw = pw->next; + } + ASSERT_TRUE(pw != NULL); + /* 4600 exceeds the 3600 cap; with mock time pinned at 0 and an infinite + * Session Expiry the publish time is the clamped delay. */ + ASSERT_EQ((WOLFMQTT_BROKER_TIME_T)BROKER_MAX_WILL_DELAY_SEC, + pw->publish_time); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_WILL */ +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#ifndef WOLFMQTT_STATIC_MEMORY +/* A fatal PUBACK decode failure must close, same as PUBLISH/PUBREC/PUBREL. */ +TEST(puback_malformed_closes_connection) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect0[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'A' + }; + /* PUBACK with remain_len = 0 (no Packet Identifier at all). */ + static const byte puback_bad[] = { 0x40, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect0, sizeof(connect0)); + mock_client_input_append(0, puback_bad, sizeof(puback_bad)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Pre-fix, the decode failure was silently discarded and the + * connection stayed open. */ + ASSERT_TRUE(g_clients[0].closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* No Enhanced Authentication support; a CONNECT with Auth Method must + * be refused, not silently accepted. */ +TEST(connect_v5_auth_method_present_rejected) +{ + MqttBroker broker; + MqttBrokerNet net; + /* props: Authentication Method (0x15) = "PLAIN" (UTF8, 5 bytes). + * prop TLV = 1(type)+2(len)+5(str) = 8. remain = 6+1+1+2+1+8+2+1 = 22 */ + static const byte connect[] = { + 0x10, 22, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x02, + 0x00, 0x3C, + 0x08, + 0x15, 0x00, 0x05, 'P', 'L', 'A', 'I', 'N', + 0x00, 0x01, 'A' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 4); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(MQTT_REASON_BAD_AUTH_METHOD, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* [MQTT-3.1.2.11.4]: Maximum Packet Size 0 in CONNECT is a Protocol Error. + * Rejected at decode time (mqtt_packet.c), so no CONNACK is ever sent - + * just a close, same as any other malformed CONNECT. */ +TEST(connect_v5_max_packet_size_zero_protocol_error) +{ + MqttBroker broker; + MqttBrokerNet net; + /* props: Maximum Packet Size (0x27) = 0x00000000 (5 bytes). remain = + * 6+1+1+2+1+5+2+1 = 19 */ + static const byte connect[] = { + 0x10, 19, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, + 0x02, + 0x00, 0x3C, + 0x05, + 0x27, 0x00, 0x00, 0x00, 0x00, /* Maximum Packet Size = 0 */ + 0x00, 0x01, 'B' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + + ASSERT_EQ(0, g_out_len); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#ifndef WOLFMQTT_STATIC_MEMORY +/* [MQTT-3.7] A malformed PUBCOMP (Remaining Length 0, no Packet Identifier) + * must fail to decode and close the connection - the PUBCOMP twin of + * puback_malformed_closes_connection. */ +TEST(pubcomp_malformed_closes_connection) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect0[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'A' + }; + /* PUBCOMP with remain_len = 0 (no Packet Identifier at all). */ + static const byte pubcomp_bad[] = { 0x70, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect0, sizeof(connect0)); + mock_client_input_append(0, pubcomp_bad, sizeof(pubcomp_bad)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[0].closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* [MQTT-3.14.4] A valid v5 DISCONNECT carrying a Session Expiry Interval must + * update the client's session expiry (any change other than 0->nonzero is + * allowed). The orphan created on close must carry the updated value. */ +TEST(disconnect_v5_session_expiry_updated_on_valid) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + /* v5 CONNECT clean=0, ClientId "U", props: Session Expiry = 60. */ + static const byte connect0[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'U' + }; + static const byte subscribe0[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'u', + 0x00 + }; + /* v5 DISCONNECT reason=Normal(0x00), props: Session Expiry = 30. */ + static const byte disconnect_se[] = { + 0xE0, 0x07, + 0x00, + 0x05, + 0x11, 0x00, 0x00, 0x00, 0x1E + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect0, sizeof(connect0)); + mock_client_input_append(0, subscribe0, sizeof(subscribe0)); + mock_client_input_append(0, disconnect_se, sizeof(disconnect_se)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "U") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); + /* Updated by the DISCONNECT from 60 to 30. */ + ASSERT_EQ((word32)30, o->session_expiry_sec); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#ifndef WOLFMQTT_STATIC_MEMORY +/* BrokerOrphan_ExpireSweep must not expire a session whose orphan_since is in + * the future relative to the (frozen at 0) clock - a backward clock jump must + * never drop a live session early. Companion to the retained clock-rollback + * guard. */ +TEST(orphan_expire_sweep_backward_clock_keeps_session) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + /* Client "K3": clean=0, Session Expiry=60 (persistent session, so a + * backward clock jump must not sweep it), one sub -> orphan. */ + static const byte connect_k3[] = { + 0x10, 0x14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x02, 'K', '3' + }; + static const byte subscribe_k3[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'k', + 0x00 + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect_k3, sizeof(connect_k3)); + mock_client_input_append(0, subscribe_k3, sizeof(subscribe_k3)); + mock_client_input_append(0, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K3") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); + + /* Stamp orphan_since in the future relative to now=0 and keep expiry=0. + * Without the now >= orphan_since guard, (now - orphan_since) underflows + * and the entry would be falsely expired. */ + o->orphan_since = 1; + o->session_expiry_sec = 0; + + broker.orphan_last_expire_check = 1; + MqttBroker_Step(&broker); + + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "K3") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); /* future-stamped -> not swept */ + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* An offline durable subscriber's queued PUBLISH must carry a deep copy of the + * v5 Application Message properties (BrokerProps_Clone). The cloned string and + * binary property values must survive after the source PUBLISH is freed. */ +TEST(orphan_offline_queue_clones_v5_publish_props) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + BrokerOrphanSession* o; + MqttProp* p; + int saw_content_type = 0; + int saw_correlation = 0; + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C,/* Session Expiry=60 -> persists */ + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE QoS 1, filter "x". */ + static const byte subscribe_x[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'x', + 0x01 + }; + static const byte disconnect0[] = { 0xE0, 0x00 }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + /* PUBLISH QoS 1, packet_id=7, topic "x", props: Content Type = "text/plain" + * and Correlation Data = DE AD BE EF, payload "p". props block = 20 bytes, + * remain = 3+2+1+20+1 = 27. */ + static const byte publish_v5[] = { + 0x32, 0x1B, + 0x00, 0x01, 'x', + 0x00, 0x07, + 0x14, + 0x03, 0x00, 0x0A, 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', + 0x09, 0x00, 0x04, 0xDE, 0xAD, 0xBE, 0xEF, + 'p' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(0, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + if (g_clients[0].closed) break; + } + ASSERT_TRUE(g_clients[0].closed); + + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + mock_client_input_append(1, publish_v5, sizeof(publish_v5)); + mock_client_input_append(1, disconnect0, sizeof(disconnect0)); + for (i = 0; i < 24; i++) { + MqttBroker_Step(&broker); + if (g_clients[1].closed) break; + } + + o = broker.orphan_sessions; + while (o != NULL && (o->client_id == NULL || + XSTRCMP(o->client_id, "S") != 0)) { + o = o->next; + } + ASSERT_TRUE(o != NULL); + ASSERT_EQ(1, o->out_q_count); + ASSERT_TRUE(o->out_q_head != NULL); + + for (p = o->out_q_head->props; p != NULL; p = p->next) { + if (p->type == MQTT_PROP_CONTENT_TYPE) { + saw_content_type = 1; + ASSERT_EQ(10, (int)p->data_str.len); + ASSERT_EQ(0, XMEMCMP(p->data_str.str, "text/plain", 10)); + } + else if (p->type == MQTT_PROP_CORRELATION_DATA) { + saw_correlation = 1; + ASSERT_EQ(4, (int)p->data_bin.len); + ASSERT_EQ(0xDE, p->data_bin.data[0]); + ASSERT_EQ(0xEF, p->data_bin.data[3]); + } + } + ASSERT_TRUE(saw_content_type); + ASSERT_TRUE(saw_correlation); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_BROKER_RETAINED) && defined(WOLFMQTT_V5) +/* [MQTT-3.3.1-9] Retain Handling = 0 must always deliver the matching retained + * message on subscribe (positive control for the Retain Handling = 2 test). */ +TEST(subscribe_v5_retain_handling_0_delivers) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + static const byte publish_retained[] = { + 0x31, 0x04, + 0x00, 0x01, 'x', 'r' + }; + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE filter "x", options = Retain Handling 0 (0x00) | QoS 0. */ + static const byte subscribe_rh0[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'x', + 0x00 + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_pub, sizeof(connect_pub)); + mock_client_input_append(0, publish_retained, sizeof(publish_retained)); + mock_client_input_append(1, connect_sub, sizeof(connect_sub)); + mock_client_input_append(1, subscribe_rh0, sizeof(subscribe_rh0)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_EQ(1, count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* [MQTT-3.3.1-10] Retain Handling = 1 delivers the retained message only if + * the subscription did not already exist. A second identical subscribe must + * not re-deliver. */ +TEST(subscribe_v5_retain_handling_1_only_if_new) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + static const byte publish_retained[] = { + 0x31, 0x04, + 0x00, 0x01, 'x', 'r' + }; + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE filter "x", options = Retain Handling 1 (0x10) | QoS 0. */ + static const byte subscribe_rh1[] = { + 0x82, 0x07, + 0x00, 0x01, + 0x00, + 0x00, 0x01, 'x', + 0x10 + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_pub, sizeof(connect_pub)); + mock_client_input_append(0, publish_retained, sizeof(publish_retained)); + mock_client_input_append(1, connect_sub, sizeof(connect_sub)); + mock_client_input_append(1, subscribe_rh1, sizeof(subscribe_rh1)); + mock_client_input_append(1, subscribe_rh1, sizeof(subscribe_rh1)); + for (i = 0; i < 20; i++) { + MqttBroker_Step(&broker); + } + + /* Two SUBSCRIBEs -> two SUBACKs, but the retained message is delivered + * only on the first (new) subscription. */ + ASSERT_EQ(2, count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_SUBSCRIBE_ACK)); + ASSERT_EQ(1, count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_RETAINED && WOLFMQTT_V5 */ + +#if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) +/* BrokerClient_PublishWillImmediate must route a QoS >= 1 Will through the + * tracked outbound queue, so a subscriber that requested QoS 1 receives the + * Will as a QoS 1 PUBLISH carrying a Packet Identifier. */ +TEST(will_qos1_routes_through_outq) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + PublishInfo info; + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE filter "lwt", QoS 1. */ + static const byte sub_subscribe[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x01 + }; + /* Publisher CONNECT with a QoS 1 Will: flags = will_flag(0x04) | + * will_qos1(0x08) | clean_session(0x02) = 0x0E. */ + static const byte pub_connect[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x0E, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* 0xE1 - DISCONNECT type with reserved bit set -> abnormal close fires + * the Will. */ + static const byte disconnect_bad[] = { 0xE1, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, sub_connect, sizeof(sub_connect)); + mock_client_input_append(0, sub_subscribe, sizeof(sub_subscribe)); + mock_client_input_append(1, pub_connect, sizeof(pub_connect)); + mock_client_input_append(1, disconnect_bad, sizeof(disconnect_bad)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + ASSERT_TRUE(g_clients[1].closed); + + /* The delivered Will must be QoS 1 (out_q route) with a Packet Id. */ + info = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); + ASSERT_TRUE(info.found); + ASSERT_EQ(MQTT_QOS_1, (int)((info.first_byte >> 1) & 0x03)); + ASSERT_NE(0, (int)info.packet_id); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_WILL && !WOLFMQTT_STATIC_MEMORY */ + +/* -------------------------------------------------------------------------- */ +/* Runner */ +/* -------------------------------------------------------------------------- */ + +int main(int argc, char** argv) +{ + (void)argc; (void)argv; + + TEST_RUNNER_BEGIN(); + + TEST_SUITE_BEGIN("broker_connect", setup, teardown); + RUN_TEST(connect_v311_emptyid_clean0_refused); + RUN_TEST(connect_v311_emptyid_clean1_accepted); + RUN_TEST(connect_v311_nonempty_clean0_accepted); + RUN_TEST(connect_v311_explicit_auto_prefix_refused); + RUN_TEST(connect_unsupported_level_3_refused); + RUN_TEST(connect_unsupported_level_6_refused); + RUN_TEST(connect_unsupported_level_127_refused); +#ifdef WOLFMQTT_BROKER_AUTH + RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); + RUN_TEST(connect_v311_binary_password_exact_match_accepted); + RUN_TEST(connect_auth_username_length_fold_repeating_byte_refused); + RUN_TEST(connect_unauth_client_id_does_not_take_over_victim); + RUN_TEST(connect_auth_user_only_start_rejected); + RUN_TEST(connect_auth_pass_only_start_rejected); + RUN_TEST(connect_auth_partial_config_fails_closed); + RUN_TEST(connect_auth_partial_pass_only_fails_closed); + RUN_TEST(broker_set_auth_pass_valid); + RUN_TEST(broker_set_auth_pass_max_len_valid); + RUN_TEST(broker_set_auth_pass_too_long_rejected); + RUN_TEST(broker_set_auth_pass_too_long_wipes_prior); + RUN_TEST(broker_set_auth_pass_shorter_second_no_residue); +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_credentials_scrubbed_after_accept); +#endif +#endif +#ifdef WOLFMQTT_V5 + RUN_TEST(connect_v5_emptyid_assigned_id_emitted); + RUN_TEST(connect_v5_emptyid_clean0_accepted); +#endif + RUN_TEST(qos2_duplicate_publish_dedup); + RUN_TEST(qos2_phantom_dup_publish_is_fresh); + RUN_TEST(qos2_publish_after_pubrel_is_fresh); + RUN_TEST(qos2_inbound_cap_reached_disconnects); + RUN_TEST(qos2_state_freed_on_client_disconnect); + RUN_TEST(qos2_pubrel_unknown_id_still_pubcomps); + RUN_TEST(qos2_publish_with_offline_durable_subscriber); + RUN_TEST(qos2_publish_then_abrupt_close_offline_subscriber); +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_V5 + RUN_TEST(online_qos1_flood_disconnects_slow_v5_subscriber); +#endif /* WOLFMQTT_V5 */ + RUN_TEST(online_qos1_flood_disconnects_slow_v311_subscriber); + RUN_TEST(online_qos1_at_cap_keeps_subscriber); +#endif /* !WOLFMQTT_STATIC_MEMORY */ +#ifdef WOLFMQTT_V5 + RUN_TEST(qos2_publish_v5_props_with_offline_durable_subscriber); +#endif + RUN_TEST(pingreq_valid_emits_pingresp); + RUN_TEST(pingreq_nonzero_remain_len_closes_no_pingresp); +#ifndef WOLFMQTT_V5 + RUN_TEST(disconnect_v311_nonzero_remain_len_fires_will); +#endif + RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); +#if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(broker_will_scrub_after_failed_write); +#endif + RUN_TEST(broker_unhandled_packet_type_closes); + RUN_TEST(broker_publish_before_connect_closes); +#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(broker_retained_list_capped); + RUN_TEST(broker_retained_clock_rollback_not_expired); + RUN_TEST(broker_retained_scrub_after_completed_write); +#endif +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(broker_per_client_subscription_cap); +#endif +#ifdef WOLFMQTT_V5 + RUN_TEST(broker_publish_with_subscription_id_closes); +#endif + RUN_TEST(broker_subscribe_packet_id_zero_closes); + RUN_TEST(connack_session_present_set_on_resumed_session); + RUN_TEST(connack_session_present_set_on_takeover); + RUN_TEST(connack_session_present_clear_on_clean_session_reconnect); +#ifdef WOLFMQTT_V5 + RUN_TEST(connack_session_present_v5_set_on_resumed_session); +#endif +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(broker_suback_reserved_v311_code_rejected); + RUN_TEST(broker_suback_valid_v311_failure_code_encoded); +#endif +#ifndef WOLFMQTT_BROKER_WILDCARDS + RUN_TEST(broker_no_wildcards_suback_failure_for_wildcard_filter); + RUN_TEST(broker_no_wildcards_suback_grants_plain_filter); +#ifdef WOLFMQTT_V5 + RUN_TEST(broker_no_wildcards_suback_v5_reason_code); +#endif +#endif +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(retained_qos_stored_1_sub_1_delivers_qos1); + RUN_TEST(retained_qos_stored_2_sub_1_delivers_qos1); + RUN_TEST(retained_qos_stored_1_sub_0_delivers_qos0); + RUN_TEST(retained_qos_stored_0_sub_1_delivers_qos0); + RUN_TEST(retained_qos_stored_2_sub_2_delivers_qos2); + RUN_TEST(retained_qos_stored_2_sub_0_delivers_qos0); +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(qos2_dedup_survives_disconnect_reconnect); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_receive_max_zero_protocol_error); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_clean0_no_se_prop_orphan_expiry_zero); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(disconnect_v5_zero_subs_nonzero_expiry_creates_orphan); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(orphan_expire_sweep_removes_zero_expiry_session); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_BROKER_WILL + RUN_TEST(disconnect_v5_session_expiry_0_to_nonzero_protocol_error); +#endif +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(subscribe_v5_retain_handling_2_never_delivers); +#endif +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(publish_v5_props_survive_queued_delivery); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(retained_qos1_delivery_survives_via_outq); +#endif +#endif +#endif +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) && \ + defined(WOLFMQTT_BROKER_WILL) + RUN_TEST(pending_will_publish_time_uses_min_of_delay_and_session_expiry); + RUN_TEST(will_delay_interval_capped); +#endif +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(puback_malformed_closes_connection); + RUN_TEST(pubcomp_malformed_closes_connection); + RUN_TEST(orphan_expire_sweep_backward_clock_keeps_session); +#endif +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(disconnect_v5_session_expiry_updated_on_valid); + RUN_TEST(orphan_offline_queue_clones_v5_publish_props); +#endif +#if defined(WOLFMQTT_BROKER_RETAINED) && defined(WOLFMQTT_V5) + RUN_TEST(subscribe_v5_retain_handling_0_delivers); + RUN_TEST(subscribe_v5_retain_handling_1_only_if_new); +#endif +#if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(will_qos1_routes_through_outq); +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_auth_method_present_rejected); +#endif +#endif +#ifdef WOLFMQTT_V5 +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_max_packet_size_zero_protocol_error); +#endif #endif TEST_SUITE_END(); diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 244b7ddf4..380da4177 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -69,6 +69,28 @@ static int mock_net_disconnect(void *context) return MQTT_CODE_SUCCESS; } +/* Counts MqttNet.disconnect invocations so a test can confirm + * MqttSocket_Disconnect actually ran. */ +static int g_disconnect_calls; +static int mock_net_disconnect_counting(void *context) +{ + (void)context; + g_disconnect_calls++; + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_NONBLOCK +/* Read side reports would-block, so a non-blocking wait defers rather than + * completing or timing out. Defined here (not under the WOLFMQTT_NO_TIME + * gate below) so the v5 flow-control tests can use it in a NO_TIME build. */ +static int mock_net_read_wouldblock(void *context, byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; (void)buf; (void)buf_len; (void)timeout_ms; + return MQTT_CODE_CONTINUE; +} +#endif + static int test_client_inited; static void setup(void) @@ -599,11 +621,13 @@ TEST(connect_v5_scrubs_connack_auth_data_from_rx_buf) int rc; int i; MqttConnect connect; - /* CONNACK v5: type=0x20, remain=0x14, flags=0x00, return_code=0x00, - * prop_len=0x11, then AUTH_DATA (0x16) binary length 14 = - * "SASLfinalPROOF". */ + /* CONNACK v5: type=0x20, remain=0x1C, flags=0x00, return_code=0x00, + * prop_len=0x19, then AUTH_METHOD (0x15)="PLAIN" and AUTH_DATA (0x16) + * binary length 14 = "SASLfinalPROOF". [MQTT-3.1.2.11.10] requires + * AUTH_METHOD whenever AUTH_DATA is present. */ static const byte connack[] = { - 0x20, 0x14, 0x00, 0x00, 0x11, + 0x20, 0x1C, 0x00, 0x00, 0x19, + 0x15, 0x00, 0x05, 'P', 'L', 'A', 'I', 'N', 0x16, 0x00, 0x0E, 'S', 'A', 'S', 'L', 'f', 'i', 'n', 'a', 'l', 'P', 'R', 'O', 'O', 'F' }; @@ -691,6 +715,481 @@ TEST(connect_refused_connack_preserves_v5_defaults) ASSERT_EQ(0, test_client.packet_sz_max); } +/* [MQTT-3.1.2.11.3]: CONNACK Receive Maximum must latch into the client. */ +TEST(connect_accepted_connack_latches_receive_max) +{ + int rc; + int i; + MqttConnect connect; + /* CONNACK v5 accepted, prop_len=3, [0x21 00 0A] = Receive Maximum 10. */ + static const byte connack[] = { + 0x20, 0x06, 0x00, 0x00, 0x03, + 0x21, 0x00, 0x0A + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "test_client"; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(10, test_client.server_recv_max); +} + +/* [MQTT-3.1.2.11.3]: Receive Maximum 0 must fail, not silently latch. */ +TEST(connect_accepted_connack_rejects_zero_receive_max) +{ + int rc; + int i; + MqttConnect connect; + /* CONNACK v5 accepted, prop_len=3, [0x21 00 00] = Receive Maximum 0. */ + static const byte connack[] = { + 0x20, 0x06, 0x00, 0x00, 0x03, + 0x21, 0x00, 0x00 + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "test_client"; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + + ASSERT_EQ(MQTT_CODE_ERROR_SERVER_PROP, rc); + /* Pre-CONNACK default (65535, absent-property default) must be intact; + * an illegal value must not be latched. */ + ASSERT_EQ(65535, test_client.server_recv_max); +} + +/* [MQTT-3.1.2.11.3]: quota exhausted must refuse before the wire. */ +TEST(publish_qos1_v5_receive_max_quota_exhausted_rejects_before_send) +{ + int rc; + MqttPublish publish; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max = 0; /* quota exhausted */ + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + g_frames_written = 0; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read; /* errors if ever reached */ + + rc = MqttClient_Publish(&test_client, &publish); + + ASSERT_EQ(MQTT_CODE_ERROR_SERVER_PROP, rc); + /* Rejected pre-send: no PUBLISH (or anything else) reached the wire. */ + ASSERT_EQ(0, g_frames_written); +} + +#ifdef WOLFMQTT_NONBLOCK +/* Quota decrements once per publish, not per non-blocking re-entry. */ +TEST(publish_qos1_v5_receive_max_quota_decrements_once_and_replenishes) +{ + int rc; + MqttPublish publish; + static byte payload[] = "hello"; + /* v5 PUBACK: type=0x40, remain=3, packet_id=1, reason=0x00 Success. */ + static const byte puback[] = { 0x40, 0x03, 0x00, 0x01, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max = 5; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_wouldblock; + + /* First entry: PUBLISH is sent (quota 5 -> 4), then the ack wait reports + * would-block. */ + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + ASSERT_EQ(4, test_client.server_recv_max); + + /* Re-entry for the same logical publish while still parked at + * MQTT_MSG_WAIT: must NOT decrement again. */ + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + ASSERT_EQ(4, test_client.server_recv_max); + + /* The broker's PUBACK now arrives: replenish exactly once. */ + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, puback, sizeof(puback)); + g_canned_len = (int)sizeof(puback); + g_canned_pos = 0; + + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(5, test_client.server_recv_max); +} +#endif /* WOLFMQTT_NONBLOCK */ + +/* These two exercise MqttClient_CancelMessage, which is only a public API when + * WOLFMQTT_MULTITHREAD or WOLFMQTT_NONBLOCK is defined (otherwise it is a + * file-local static in mqtt_client.c). */ +#if defined(WOLFMQTT_MULTITHREAD) || defined(WOLFMQTT_NONBLOCK) +/* [MQTT-4.9] MqttClient_CancelMessage must NOT credit the reserved Receive + * Maximum unit. An abandoned QoS>0 v5 publish may already be on the wire, where + * the server still counts it against Receive Maximum, so cancelling retains the + * unit while the connection stays open rather than risking an over-credit that + * lets the client exceed the negotiated quota. */ +TEST(cancel_message_retains_recv_quota_on_wire) +{ + int rc; + MqttPublish publish; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max_negotiated = 5; + test_client.server_recv_max = 4; /* one unit reserved and on the wire */ + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + publish.stat.recvQuotaHeld = 1; + + rc = MqttClient_CancelMessage(&test_client, (MqttObject*)&publish); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + /* Retained: the unit is not credited back and stays reserved. */ + ASSERT_EQ(4, test_client.server_recv_max); + ASSERT_EQ(1, (int)publish.stat.recvQuotaHeld); +} + +/* Cancelling the same abandoned publish twice is stable: the retained unit is + * never credited, so repeated cancels leave the quota unchanged. */ +TEST(cancel_message_retain_is_idempotent) +{ + int rc; + MqttPublish publish; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max_negotiated = 5; + test_client.server_recv_max = 4; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + publish.stat.recvQuotaHeld = 1; + + rc = MqttClient_CancelMessage(&test_client, (MqttObject*)&publish); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, test_client.server_recv_max); + + rc = MqttClient_CancelMessage(&test_client, (MqttObject*)&publish); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, test_client.server_recv_max); +} +#endif /* WOLFMQTT_MULTITHREAD || WOLFMQTT_NONBLOCK */ + +/* A QoS>0 v5 publish that fails on the wire (unsent) must give its reserved + * Receive Maximum unit back via RestoreRecvQuota on the write-failure path. + * The recvQuotaHeld flag keeps the credit to exactly one. */ +TEST(publish_qos1_v5_write_failure_restores_recv_quota) +{ + int rc; + MqttPublish publish; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max_negotiated = 5; + test_client.server_recv_max = 5; + + /* mock_net_write returns MQTT_CODE_ERROR_NETWORK, so the PUBLISH write + * fails after the quota unit has been reserved. */ + test_net.write = mock_net_write; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_TRUE(rc < 0); + /* Reserved then restored: back at the ceiling, credited exactly once. */ + ASSERT_EQ(5, test_client.server_recv_max); + ASSERT_EQ(0, (int)publish.stat.recvQuotaHeld); +} + +#endif /* WOLFMQTT_V5 */ + +/* [MQTT-2.2.2-2/4.13.1]: invalid fixed-header flags must disconnect. */ +TEST(wait_message_malformed_fixed_header_disconnects) +{ + int rc; + int i; + /* Malformed PUBACK: type nibble 4 (PUBACK), reserved flags nibble 1 + * (illegal; PUBACK requires 0). remain=2, packet_id=4. */ + static const byte malformed_puback[] = { 0x41, 0x02, 0x00, 0x04 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + + /* Simulate an already-connected client, as MqttClient_NetConnect would + * leave it, so the fix's IS_CONNECTED guard is actually exercised. */ + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + + g_disconnect_calls = 0; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + test_net.disconnect = mock_net_disconnect_counting; + XMEMCPY(g_canned_buf, malformed_puback, sizeof(malformed_puback)); + g_canned_len = (int)sizeof(malformed_puback); + g_canned_pos = 0; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + } + + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + /* IS_CONNECTED is cleared so the caller sees a dead connection; the + * transport teardown is deferred to MqttClient_NetDisconnect (avoids a + * double disconnect and a race with a concurrent writer). */ + ASSERT_EQ(0, g_disconnect_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* Feed a canned frame that decodes to a fatal protocol error, drive the + * receive/wait path to completion, and assert both the expected error and that + * IS_CONNECTED is cleared so the caller sees a dead connection. The teardown is + * gated on recvFatal, so this covers the MULTITHREAD build (active here) where a + * fatal received-data error must not leave the connected flag set. */ +static void drive_fatal_proto_teardown(const byte* frame, int frame_len, + int expected_rc) +{ + int rc; + int i; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; +#endif + + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + + g_disconnect_calls = 0; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + test_net.disconnect = mock_net_disconnect_counting; + XMEMCPY(g_canned_buf, frame, (size_t)frame_len); + g_canned_len = frame_len; + g_canned_pos = 0; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + } + + ASSERT_EQ(expected_rc, rc); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* An unexpected client-only packet type (CONNECT) arriving from the peer is a + * fatal PACKET_TYPE error and must clear IS_CONNECTED. */ +TEST(wait_message_fatal_packet_type_clears_is_connected) +{ + static const byte frame[] = { 0x10, 0x02, 0x00, 0x00 }; + drive_fatal_proto_teardown(frame, (int)sizeof(frame), + MQTT_CODE_ERROR_PACKET_TYPE); +} + +#ifdef WOLFMQTT_V5 +/* A received v5 AUTH carrying Authentication Data without an Authentication + * Method is a fatal PROPERTY error and must clear IS_CONNECTED. */ +TEST(wait_message_fatal_property_clears_is_connected) +{ + /* AUTH, remain=6, reason=0x18 (Continue Auth), prop_len=4, + * AUTH_DATA(0x16)="x" with no Auth Method. */ + static const byte frame[] = { + 0xF0, 0x06, 0x18, 0x04, 0x16, 0x00, 0x01, 'x' + }; + drive_fatal_proto_teardown(frame, (int)sizeof(frame), + MQTT_CODE_ERROR_PROPERTY); +} +#endif /* WOLFMQTT_V5 */ + +#ifdef WOLFMQTT_V5 +/* [MQTT-3.1.2.11.8]: CONNACK Topic Alias Maximum must latch into client. */ +TEST(connect_accepted_connack_latches_topic_alias_max) +{ + int rc; + int i; + MqttConnect connect; + /* CONNACK v5 accepted, prop_len=3, [0x22 00 05] = Topic Alias Maximum 5. */ + static const byte connack[] = { + 0x20, 0x06, 0x00, 0x00, 0x03, + 0x22, 0x00, 0x05 + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "test_client"; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(5, test_client.topic_alias_max); +} + +/* [MQTT-3.3.2.3.4]: Topic Alias over the max must be rejected pre-send. */ +TEST(publish_v5_topic_alias_exceeds_max_rejected) +{ + int rc; + MqttPublish publish; + MqttProp* prop; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.topic_alias_max = 5; /* server accepts alias values 1..5 */ + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + prop = MqttClient_PropsAdd(&publish.props); + ASSERT_NOT_NULL(prop); + prop->type = MQTT_PROP_TOPIC_ALIAS; + prop->data_short = 6; /* exceeds topic_alias_max of 5 */ + + g_frames_written = 0; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read; /* errors if ever reached */ + + rc = MqttClient_Publish(&test_client, &publish); + + ASSERT_EQ(MQTT_CODE_ERROR_SERVER_PROP, rc); + ASSERT_EQ(0, g_frames_written); + + MqttClient_PropsFree(publish.props); +} + +/* a Topic Alias of 0 is always illegal, even when + * the server did advertise a positive maximum. */ +TEST(publish_v5_topic_alias_zero_rejected) +{ + int rc; + MqttPublish publish; + MqttProp* prop; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.topic_alias_max = 5; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + prop = MqttClient_PropsAdd(&publish.props); + ASSERT_NOT_NULL(prop); + prop->type = MQTT_PROP_TOPIC_ALIAS; + prop->data_short = 0; + + g_frames_written = 0; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read; + + rc = MqttClient_Publish(&test_client, &publish); + + ASSERT_EQ(MQTT_CODE_ERROR_SERVER_PROP, rc); + ASSERT_EQ(0, g_frames_written); + + MqttClient_PropsFree(publish.props); +} + /* MQTT v5 [3.1.2.11.6]: only Max QoS 0 or 1 are legal. A non-conforming or * malicious broker that advertises a larger value (2 here) must be clamped to * MQTT_QOS_1 before being narrowed against this build's WOLFMQTT_MAX_QOS, so the @@ -2042,17 +2541,6 @@ static int mock_net_read_timeout(void *context, byte* buf, int buf_len, return MQTT_CODE_ERROR_TIMEOUT; } -#ifdef WOLFMQTT_NONBLOCK -/* Read side reports would-block, so a non-blocking wait defers rather than - * completing or timing out. */ -static int mock_net_read_wouldblock(void *context, byte* buf, int buf_len, - int timeout_ms) -{ - (void)context; (void)buf; (void)buf_len; (void)timeout_ms; - return MQTT_CODE_CONTINUE; -} -#endif - TEST(wait_message_auto_pings_on_keepalive_deadline) { int rc; @@ -2659,6 +3147,9 @@ void run_mqtt_client_tests(void) RUN_TEST(connect_v5_scrubs_connack_auth_data_from_rx_buf); RUN_TEST(connect_refused_connack_preserves_v5_defaults); RUN_TEST(connect_accepted_connack_clamps_illegal_max_qos); + RUN_TEST(connect_accepted_connack_latches_topic_alias_max); + RUN_TEST(connect_accepted_connack_rejects_zero_receive_max); + RUN_TEST(connect_accepted_connack_latches_receive_max); #endif /* MqttClient_Disconnect tests */ @@ -2699,6 +3190,17 @@ void run_mqtt_client_tests(void) RUN_TEST(publish_qos2_v5_success_returns_success); RUN_TEST(publish_qos2_v5_pubrec_rejection_returns_publish_rejected); RUN_TEST(publish_v311_ack_not_misread_as_rejected); + RUN_TEST(publish_v5_topic_alias_zero_rejected); + RUN_TEST(publish_v5_topic_alias_exceeds_max_rejected); +#ifdef WOLFMQTT_NONBLOCK + RUN_TEST(publish_qos1_v5_receive_max_quota_decrements_once_and_replenishes); +#endif + RUN_TEST(publish_qos1_v5_receive_max_quota_exhausted_rejects_before_send); +#if defined(WOLFMQTT_MULTITHREAD) || defined(WOLFMQTT_NONBLOCK) + RUN_TEST(cancel_message_retains_recv_quota_on_wire); + RUN_TEST(cancel_message_retain_is_idempotent); +#endif + RUN_TEST(publish_qos1_v5_write_failure_restores_recv_quota); #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_NONBLOCK) RUN_TEST(publish_qos2_v5_pubrec_rejection_multithread_reader); #endif @@ -2724,6 +3226,11 @@ void run_mqtt_client_tests(void) RUN_TEST(wait_message_pubrel_emits_pubcomp); RUN_TEST(wait_message_puback_emits_no_ack); RUN_TEST(wait_message_pubcomp_emits_no_ack); + RUN_TEST(wait_message_malformed_fixed_header_disconnects); + RUN_TEST(wait_message_fatal_packet_type_clears_is_connected); +#ifdef WOLFMQTT_V5 + RUN_TEST(wait_message_fatal_property_clears_is_connected); +#endif #ifndef WOLFMQTT_NO_TIME /* Automatic keep-alive (PINGREQ) scheduling tests */ diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 18d3e4d72..652fe175a 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -5266,6 +5266,245 @@ TEST(decode_auth_v5_reason_code_past_buf_rejected) } #endif /* WOLFMQTT_V5 */ +#if defined(WOLFMQTT_BROKER) && defined(WOLFMQTT_V5) +/* [MQTT-2.2.2.2]: duplicate Subscription ID in SUBSCRIBE is a Protocol Error. */ +TEST(decode_subscribe_v5_duplicate_subscription_id_rejected) +{ + byte rx_buf[] = { + 0x82, 0x0B, /* SUBSCRIBE, remain_len = 11 */ + 0x00, 0x01, /* packet_id */ + 0x04, /* props_len VBI = 4 */ + 0x0B, 0x01, /* Subscription Identifier = 1 */ + 0x0B, 0x01, /* Subscription Identifier = 1 (dup) */ + 0x00, 0x01, 'a', /* filter "a" */ + 0x00 /* options */ + }; + MqttSubscribe sub; + MqttTopic topic_arr[1]; + int rc; + + XMEMSET(&sub, 0, sizeof(sub)); + XMEMSET(topic_arr, 0, sizeof(topic_arr)); + sub.topics = topic_arr; + sub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Subscribe(rx_buf, (int)sizeof(rx_buf), &sub); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(sub.props); +} + +/* [MQTT-3.1.3.2]: a CONNECT-only property in Will Properties is rejected. */ +TEST(decode_connect_v5_will_props_session_expiry_rejected) +{ + byte buf[256]; + MqttConnect enc, dec; + MqttMessage enc_lwt, dec_lwt; + MqttProp will_prop; + int enc_len, rc; + + XMEMSET(&enc, 0, sizeof(enc)); + XMEMSET(&enc_lwt, 0, sizeof(enc_lwt)); + XMEMSET(&will_prop, 0, sizeof(will_prop)); + enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + enc.client_id = "cid"; + enc.enable_lwt = 1; + enc.lwt_msg = &enc_lwt; + enc_lwt.topic_name = "will/topic"; + enc_lwt.qos = MQTT_QOS_0; + will_prop.type = MQTT_PROP_SESSION_EXPIRY_INTERVAL; + will_prop.data_int = 30; + enc_lwt.props = &will_prop; + + enc_len = MqttEncode_Connect(buf, (int)sizeof(buf), &enc); + ASSERT_TRUE(enc_len > 0); + + XMEMSET(&dec, 0, sizeof(dec)); + XMEMSET(&dec_lwt, 0, sizeof(dec_lwt)); + dec.lwt_msg = &dec_lwt; + rc = MqttDecode_Connect(buf, enc_len, &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); +} +#endif /* WOLFMQTT_BROKER && WOLFMQTT_V5 */ + +#ifdef WOLFMQTT_V5 +/* [MQTT-3.4.2.1]: 0x92 is valid for PUBREL/PUBCOMP, not PUBACK/PUBREC. */ +TEST(encode_puback_v5_reason_code_out_of_table_rejected) +{ + byte buf[16]; + MqttPublishResp enc; + int enc_len; + + XMEMSET(&enc, 0, sizeof(enc)); + enc.packet_id = 1; + enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + enc.reason_code = MQTT_REASON_PACKET_ID_NOT_FOUND; + + enc_len = MqttEncode_PublishResp(buf, (int)sizeof(buf), + MQTT_PACKET_TYPE_PUBLISH_ACK, &enc); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, enc_len); +} + +/* Companion case: the same Reason Code IS valid for PUBREL, so the gate + * must be per-packet-type rather than a single fixed set. */ +TEST(encode_pubrel_v5_reason_code_packet_id_not_found_accepted) +{ + byte buf[16]; + MqttPublishResp enc; + int enc_len; + + XMEMSET(&enc, 0, sizeof(enc)); + enc.packet_id = 1; + enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + enc.reason_code = MQTT_REASON_PACKET_ID_NOT_FOUND; + + enc_len = MqttEncode_PublishResp(buf, (int)sizeof(buf), + MQTT_PACKET_TYPE_PUBLISH_REL, &enc); + ASSERT_TRUE(enc_len > 0); +} +#endif /* WOLFMQTT_V5 */ + +#if defined(WOLFMQTT_BROKER) && defined(WOLFMQTT_V5) +/* [MQTT-3.1.2.11.9/10]: Auth Data without Auth Method is a Protocol Error. */ +TEST(decode_connect_v5_auth_data_without_auth_method_rejected) +{ + byte buf[] = { + 0x10, 0x14, /* CONNECT, remain_len = 20 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, /* protocol level v5 */ + 0x02, /* flags: clean_session */ + 0x00, 0x3C, /* keep alive */ + 0x04, /* props_len VBI = 4 */ + 0x16, 0x00, 0x01, 'x', /* Authentication Data = "x" */ + 0x00, 0x03, 'c', 'i', 'd' /* client_id "cid" */ + }; + MqttConnect dec; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + dec.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(dec.props); +} + +/* [MQTT-3.1.2.11.4] Maximum Packet Size, if present, MUST NOT be 0. Wire: + * CONNECT, props_len=5, MAX_PACKET_SZ(39)=0x00000000, client_id "cid". */ +TEST(decode_connect_v5_max_packet_size_zero_rejected) +{ + byte buf[] = { + 0x10, 0x15, /* CONNECT, remain_len = 21 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, /* protocol level v5 */ + 0x02, /* flags: clean_session */ + 0x00, 0x3C, /* keep alive */ + 0x05, /* props_len VBI = 5 */ + 0x27, 0x00, 0x00, 0x00, 0x00, /* Maximum Packet Size = 0 */ + 0x00, 0x03, 'c', 'i', 'd' /* client_id "cid" */ + }; + MqttConnect dec; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + dec.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(dec.props); +} +#endif /* WOLFMQTT_BROKER && WOLFMQTT_V5 */ + +#ifdef WOLFMQTT_V5 +/* [MQTT-3.6.2.1] 0x10 (No Matching Subscribers) is valid for PUBACK/PUBREC but + * NOT for PUBREL; encoding it as PUBREL must be rejected. */ +TEST(encode_pubrel_v5_reason_code_out_of_table_rejected) +{ + byte buf[16]; + MqttPublishResp enc; + int enc_len; + + XMEMSET(&enc, 0, sizeof(enc)); + enc.packet_id = 1; + enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + enc.reason_code = MQTT_REASON_NO_MATCH_SUB; + + enc_len = MqttEncode_PublishResp(buf, (int)sizeof(buf), + MQTT_PACKET_TYPE_PUBLISH_REL, &enc); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, enc_len); +} + +/* [MQTT-3.7.2.1] Same out-of-table Reason Code rejected for PUBCOMP. */ +TEST(encode_pubcomp_v5_reason_code_out_of_table_rejected) +{ + byte buf[16]; + MqttPublishResp enc; + int enc_len; + + XMEMSET(&enc, 0, sizeof(enc)); + enc.packet_id = 1; + enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + enc.reason_code = MQTT_REASON_NO_MATCH_SUB; + + enc_len = MqttEncode_PublishResp(buf, (int)sizeof(buf), + MQTT_PACKET_TYPE_PUBLISH_COMP, &enc); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, enc_len); +} + +/* [MQTT-3.3.2.3.8] A Subscription Identifier MAY appear more than once in a + * PUBLISH; the duplicate-property gate must allow it here (accept direction, + * companion to decode_subscribe_v5_duplicate_subscription_id_rejected). Wire: + * PUBLISH QoS 0, topic "t", props_len=4, SUB_ID=1, SUB_ID=2, payload "x". */ +TEST(decode_publish_v5_duplicate_subscription_id_accepted) +{ + byte buf[] = { + 0x30, 0x09, 0x00, 0x01, 't', 0x04, + 0x0B, 0x01, 0x0B, 0x02, 'x' + }; + MqttPublish pub; + MqttProp* prop; + int rc, count = 0; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_TRUE(rc > 0); + for (prop = pub.props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_SUBSCRIPTION_ID) { + count++; + } + } + ASSERT_EQ(2, count); + MqttProps_Free(pub.props); +} + +/* [MQTT-3.1.2.11.4] Maximum Packet Size 0 is a Protocol Error. Today only the + * CONNECT direction is exercised; the check lives in MqttDecode_Props and must + * fire for the CONNACK direction too. Wire: MAX_PACKET_SZ(0x27)=0. */ +TEST(decode_props_connack_max_packet_size_zero_rejected) +{ + byte buf[] = { 0x27, 0x00, 0x00, 0x00, 0x00 }; + MqttProp* props = NULL; + int rc; + + rc = MqttDecode_Props(MQTT_PACKET_TYPE_CONNECT_ACK, &props, buf, + (word32)sizeof(buf), (word32)sizeof(buf)); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(props); +} + +/* [MQTT-3.1.2.11.9/10] Auth Data without Auth Method is a Protocol Error. Today + * only the CONNECT direction is exercised; the check lives in MqttDecode_Props + * and must fire for the AUTH direction too. Wire: AUTH_DATA(0x16)="x". */ +TEST(decode_props_auth_data_without_method_rejected) +{ + byte buf[] = { 0x16, 0x00, 0x01, 'x' }; + MqttProp* props = NULL; + int rc; + + rc = MqttDecode_Props(MQTT_PACKET_TYPE_AUTH, &props, buf, + (word32)sizeof(buf), (word32)sizeof(buf)); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(props); +} +#endif /* WOLFMQTT_V5 */ + /* ============================================================================ * Test Suite Runner * ============================================================================ */ @@ -5384,6 +5623,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_publish_v5_response_topic_wildcard_rejected); RUN_TEST(decode_publish_v5_property_count_capped); RUN_TEST(decode_publish_v5_duplicate_singleton_prop_rejected); + RUN_TEST(decode_publish_v5_duplicate_subscription_id_accepted); RUN_TEST(decode_publish_v5_props_freed_on_short_remain_len); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); @@ -5487,6 +5727,9 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_connect_v5_rejects_nul_in_client_id); RUN_TEST(decode_connect_v5_password_without_username_accepted); RUN_TEST(decode_connect_v5_props_freed_on_client_id_error); + RUN_TEST(decode_connect_v5_max_packet_size_zero_rejected); + RUN_TEST(decode_connect_v5_auth_data_without_auth_method_rejected); + RUN_TEST(decode_connect_v5_will_props_session_expiry_rejected); #endif /* MqttDecode_Subscribe */ @@ -5505,6 +5748,7 @@ void run_mqtt_packet_tests(void) #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_empty_payload_rejected); RUN_TEST(decode_subscribe_v5_props_freed_on_bad_filter); + RUN_TEST(decode_subscribe_v5_duplicate_subscription_id_rejected); #endif #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_options_byte_qos_extracted); @@ -5612,6 +5856,12 @@ void run_mqtt_packet_tests(void) RUN_TEST(publish_resp_v5_success_with_props_roundtrip); RUN_TEST(publish_resp_v5_error_no_props_roundtrip); RUN_TEST(publish_resp_v5_success_no_props_roundtrip); + RUN_TEST(encode_pubrel_v5_reason_code_packet_id_not_found_accepted); + RUN_TEST(encode_puback_v5_reason_code_out_of_table_rejected); + RUN_TEST(encode_pubrel_v5_reason_code_out_of_table_rejected); + RUN_TEST(encode_pubcomp_v5_reason_code_out_of_table_rejected); + RUN_TEST(decode_props_connack_max_packet_size_zero_rejected); + RUN_TEST(decode_props_auth_data_without_method_rejected); /* MqttEncode/Decode_Auth */ RUN_TEST(encode_props_string_invalid_utf8_rejected); diff --git a/tests/test_mqtt_sn.c b/tests/test_mqtt_sn.c index a0b0c7b86..4c80e4876 100644 --- a/tests/test_mqtt_sn.c +++ b/tests/test_mqtt_sn.c @@ -448,6 +448,21 @@ TEST(sn_gwinfo_wrong_type_rejected) ASSERT_EQ(MQTT_CODE_ERROR_PACKET_TYPE, rc); } +TEST(sn_gwinfo_null_gwaddr_with_addr_field_rejected) +{ + /* [len=5][type=GWINFO][gwId][addr0][addr1] - address field present, but + * gwAddr is left NULL (unlike sn_gwinfo_short_form_with_addr_valid). + * Prior to the NULL check this would XMEMCPY through a NULL pointer. + * Must be rejected instead of crashing/corrupting memory. */ + byte buf[5] = { 0x05, SN_MSG_TYPE_GWINFO, 0x09, 0xAA, 0xBB }; + SN_GwInfo info; + int rc; + XMEMSET(&info, 0, sizeof(info)); + ASSERT_TRUE(info.gwAddr == NULL); + rc = SN_Decode_GWInfo(buf, (int)sizeof(buf), &info); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + /* ============================================================================ * SN_Decode_Register * ============================================================================ */ @@ -1836,6 +1851,7 @@ int main(int argc, char** argv) RUN_TEST(sn_gwinfo_ind_form_no_addr_no_overread); RUN_TEST(sn_gwinfo_ind_form_with_addr_valid); RUN_TEST(sn_gwinfo_wrong_type_rejected); + RUN_TEST(sn_gwinfo_null_gwaddr_with_addr_field_rejected); /* SN_Decode_Register */ RUN_TEST(sn_register_short_form_valid); diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index b5483ffc3..4d594ace0 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -239,6 +239,15 @@ static const byte SUBACK_REJECT_FRAME[] = { 0x08, SN_MSG_TYPE_SUBACK, 0x00, /* Gateway PINGRESP: total_len=2, type. */ static const byte PINGRESP_FRAME[] = { 0x02, SN_MSG_TYPE_PING_RESP }; +#ifdef WOLFMQTT_NONBLOCK +/* Unsolicited GWINFO broadcast: total_len=5, type, gwId, addr(2). Not a + * match for any pending wait, so SN_Client_WaitType routes it through the + * shared client->msgSN object. */ +#define SN_TEST_GWINFO_GWID 0x09 +static const byte GWINFO_FRAME[] = { 0x05, SN_MSG_TYPE_GWINFO, + SN_TEST_GWINFO_GWID, 0xAA, 0xBB }; +#endif + /* Scripted publish-response frames for packet_id 1. * PUBACK: total_len=7, type, topicId(2), packet_id(2), return_code. * PUBREC: total_len=4, type, packet_id(2). @@ -1460,6 +1469,44 @@ TEST(sn_ping_nonblock_pendresp_lifecycle) ASSERT_NO_PENDRESP(); } +/* Positive path: an unsolicited GWINFO arrives while waiting on + * a PINGRESP, so SN_Client_WaitType routes it to the shared client->msgSN + * object (not a caller-supplied packet_obj). Confirms SN_Client_HandlePacket's + * `p_info->gwAddr = &p_info->gwAddrBuf;` wiring means the gateway address is + * actually captured, not just safely dropped by the NULL-check backstop. + * + * Only the GWINFO frame is armed here: with no further frame queued, the mock + * read returns CONTINUE once the mismatched GWINFO has been fully decoded, so + * this first call returns in-flight *before* a later packet's + * MqttSNClient_PacketReset() call zeroes the shared msgSN union again. The + * decoded fields are checked at that checkpoint. */ +TEST(sn_ping_unsolicited_gwinfo_captured) +{ + SN_PingReq ping; + int rc; + const byte expect_addr[2] = { 0xAA, 0xBB }; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + + mock_net_push(&g_mock, GWINFO_FRAME, (int)sizeof(GWINFO_FRAME)); + + XMEMSET(&ping, 0, sizeof(ping)); + + rc = SN_Client_Ping(&g_client, &ping); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + ASSERT_EQ(SN_TEST_GWINFO_GWID, g_client.msgSN.gwInfo.gwId); + ASSERT_NOT_NULL(g_client.msgSN.gwInfo.gwAddr); + ASSERT_MEM_EQ(expect_addr, &g_client.msgSN.gwInfo.gwAddrBuf, + sizeof(expect_addr)); + + /* Let the still-pending PINGREQ resolve so it does not leak a pendResp + * entry into later tests. */ + mock_net_push(&g_mock, PINGRESP_FRAME, (int)sizeof(PINGRESP_FRAME)); + rc = sn_ping_pump(&ping, NULL); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_NO_PENDRESP(); +} + #endif /* WOLFMQTT_NONBLOCK */ /* Regression: on the non-DTLS transport SN_Packet_Read peeks the 2-byte header @@ -1547,6 +1594,7 @@ int main(int argc, char** argv) #endif RUN_TEST(sn_ping_null_nonblock_no_dangling_pendresp); RUN_TEST(sn_ping_nonblock_pendresp_lifecycle); + RUN_TEST(sn_ping_unsolicited_gwinfo_captured); #endif TEST_SUITE_END(); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 6be8f2f27..e638ea0fd 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -455,6 +455,16 @@ typedef struct BrokerOutPub { WOLFMQTT_BROKER_TIME_T enq_time; word32 expiry_sec; /* v5 Message Expiry Interval, 0 = no expiry */ byte protocol_level; /* echoed back to subscriber on send */ +#ifdef WOLFMQTT_V5 + /* Deep copy (BrokerProps_Clone) of the originating PUBLISH's v5 + * Application Message properties (Payload Format Indicator, Content + * Type, Response Topic, Correlation Data, User Property, etc.), or + * NULL if none. Owned by this entry; freed once by BrokerOutPub_Free + * via BrokerProps_FreeClone - never via MqttProps_Free(). + * Not serialized by the persist layer: a queued message replayed after a + * broker restart is delivered without these properties. */ + MqttProp* props; +#endif struct BrokerOutPub* next; } BrokerOutPub; @@ -477,6 +487,15 @@ typedef struct BrokerOrphanSession { BrokerOutPub* out_q_tail; int out_q_count; int out_q_inflight; +#if WOLFMQTT_MAX_QOS >= 2 + /* Inbound QoS 2 dedup state (see BrokerClient.qos2_pending), moved + * here on disconnect so a retransmitted PUBLISH after reconnect is + * still recognized as a duplicate instead of being re-fanned-out. + * Ownership transfers by pointer reassignment, mirroring out_q_head + * above - see BrokerOrphan_Take / BrokerOrphan_Reclaim. */ + BrokerInboundQos2* qos2_pending; + int qos2_pending_count; +#endif struct BrokerOrphanSession* next; } BrokerOrphanSession; #endif @@ -732,6 +751,9 @@ typedef struct MqttBroker { * branches on that to look up the orphan by client_id. */ BrokerOrphanSession* orphan_sessions; int orphan_session_count; + /* Rate-limits BrokerOrphan_ExpireSweep so MqttBroker_Step does not + * walk the orphan list on every single call. */ + WOLFMQTT_BROKER_TIME_T orphan_last_expire_check; #endif } MqttBroker; diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 7342fdb38..1aaedf8db 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -284,6 +284,16 @@ typedef struct _MqttClient { * flag per the wolfSSL struct guidance for booleans. */ unsigned int keep_alive_from_server : 1; #endif + +#ifdef WOLFMQTT_V5 + /* Max unacked QoS>0 in flight; absent CONNACK means 65535 [3.1.2.11.3]. */ + word16 server_recv_max; + /* Ceiling server_recv_max replenishes to; a spurious/duplicate ack + * must not inflate the live quota above the negotiated value. */ + word16 server_recv_max_negotiated; + /* Max Topic Alias value; absent CONNACK means 0 [3.1.2.11.8]. */ + word16 topic_alias_max; +#endif } MqttClient; #ifdef WOLFMQTT_SN @@ -385,7 +395,11 @@ WOLFMQTT_API int MqttClient_Connect( * \return MQTT_CODE_SUCCESS, MQTT_CODE_CONTINUE (for non-blocking), MQTT_CODE_ERROR_PUBLISH_REJECTED if a v5 broker rejected a QoS>0 PUBLISH via a PUBACK (QoS 1) or PUBREC/PUBCOMP (QoS 2) - reason code >= 0x80 (see MqttPublish.resp.reason_code), or + reason code >= 0x80 (see MqttPublish.resp.reason_code), + MQTT_CODE_ERROR_SERVER_PROP if the request violates a + CONNACK-advertised v5 server property before sending (QoS above + Maximum QoS, Retain unavailable, Topic Alias above the server + maximum, or Receive Maximum quota exhausted), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) \sa MqttClient_Publish_WriteOnly \sa MqttClient_Publish_ex @@ -412,7 +426,11 @@ WOLFMQTT_API int MqttClient_Publish( * \return MQTT_CODE_SUCCESS, MQTT_CODE_CONTINUE (for non-blocking), MQTT_CODE_ERROR_PUBLISH_REJECTED if a v5 broker rejected a QoS>0 PUBLISH via a PUBACK (QoS 1) or PUBREC/PUBCOMP (QoS 2) - reason code >= 0x80 (see MqttPublish.resp.reason_code), or + reason code >= 0x80 (see MqttPublish.resp.reason_code), + MQTT_CODE_ERROR_SERVER_PROP if the request violates a + CONNACK-advertised v5 server property before sending (QoS above + Maximum QoS, Retain unavailable, Topic Alias above the server + maximum, or Receive Maximum quota exhausted), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_Publish_ex( @@ -568,6 +586,11 @@ WOLFMQTT_API int MqttClient_Disconnect_ex( /*! \brief Waits for packets to arrive. Incoming publish messages will arrive via callback provided in MqttClient_Init. * \note This is a blocking function that will wait for MqttNet.read + * \note A fatal protocol error in received data (malformed data, unexpected + packet type or id, or an invalid v5 property) marks the + connection disconnected (clears the connected flag); the + application should call MqttClient_NetDisconnect to tear down + the transport. * \param client Pointer to MqttClient structure * \param timeout_ms Milliseconds until read timeout * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -580,6 +603,11 @@ WOLFMQTT_API int MqttClient_WaitMessage( /*! \brief Waits for packets to arrive. Incoming publish messages will arrive via callback provided in MqttClient_Init. * \note This is a blocking function that will wait for MqttNet.read + * \note A fatal protocol error in received data (malformed data, unexpected + packet type or id, or an invalid v5 property) marks the + connection disconnected (clears the connected flag); the + application should call MqttClient_NetDisconnect to tear down + the transport. * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure * \param timeout_ms Milliseconds until read timeout diff --git a/wolfmqtt/mqtt_packet.h b/wolfmqtt/mqtt_packet.h index a8ae96b8b..3e7ccca20 100644 --- a/wolfmqtt/mqtt_packet.h +++ b/wolfmqtt/mqtt_packet.h @@ -49,15 +49,12 @@ #endif /* WOLFMQTT_NO_UTF8_VALIDATION - * Define to disable RFC 3629 UTF-8 well-formedness validation in - * MqttDecode_String. Spec requirement [MQTT-1.5.3-1] (v3.1.1 1.5.3 / - * v5 1.5.4) makes ill-formed UTF-8 a "MUST close the network - * connection" condition; disabling the check trades that compliance - * for ~300 bytes of .text on x86-64 (~200 bytes on ARM Thumb-2) and - * should only be considered for severely flash-constrained targets - * where the peer is known-trusted. The independent embedded-NUL - * check ([MQTT-1.5.3-2]) remains active either way because it also - * guards downstream C-string handling. */ + * Define to disable RFC 3629 UTF-8 well-formedness validation on the + * encode side (MqttEncode_Utf8Ok). Decode-side validation in + * MqttDecode_String is mandatory per [MQTT-1.5.3-1] and cannot be + * disabled. The independent embedded-NUL check ([MQTT-1.5.3-2]) also + * remains active either way because it also guards downstream + * C-string handling. */ #ifdef WOLFMQTT_V5 @@ -339,6 +336,7 @@ typedef struct _MqttMsgStat { byte isReadActive:1; byte isWriteActive:1; + byte recvQuotaHeld:1; /* v5 Receive Maximum unit reserved for this message */ } MqttMsgStat; #ifdef WOLFMQTT_MULTITHREAD diff --git a/wolfmqtt/mqtt_sn_packet.h b/wolfmqtt/mqtt_sn_packet.h index 9dd4d5111..b74246525 100644 --- a/wolfmqtt/mqtt_sn_packet.h +++ b/wolfmqtt/mqtt_sn_packet.h @@ -140,6 +140,11 @@ typedef struct _SN_GwInfo { byte gwId; /* ID of the gateway that sent this message */ SN_GwAddr* gwAddr; /* Address of the indicated gateway */ + + /* Backing storage for gwAddr. Callers may point gwAddr elsewhere; + * SN_Client_HandlePacket defaults it here so decode never writes + * through a NULL pointer. */ + SN_GwAddr gwAddrBuf; } SN_GwInfo; typedef struct _SN_SearchGw {