diff --git a/.github/workflows/broker-check.yml b/.github/workflows/broker-check.yml index 1c653ec54..8806de06f 100644 --- a/.github/workflows/broker-check.yml +++ b/.github/workflows/broker-check.yml @@ -115,6 +115,10 @@ jobs: cflags: "" wolfmqtt_opts: "--enable-broker --enable-v5 --enable-broker-persist --disable-broker-retained" check_units: "yes" + - name: "Broker with persist + nonblock" + cflags: "" + wolfmqtt_opts: "--enable-broker --enable-v5 --enable-broker-persist --enable-nonblock" + check_units: "yes" steps: - name: Install dependencies diff --git a/.gitignore b/.gitignore index 6c79ba464..2f1a989a1 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,6 @@ tests/test_mqtt_sn_client_mt_only tests/test_mqtt_sn_client_nodata tests/unit_tests tests/test_mqtt_props_init +tests/test_mqtt_sem_init +tests/test_broker_log tests/test_mqtt_curl_init diff --git a/BROKER.md b/BROKER.md index 97711b68b..65974a6e5 100644 --- a/BROKER.md +++ b/BROKER.md @@ -165,11 +165,39 @@ script or installer inherits the ambient umask - under a relaxed umask such as mkdir -p /var/lib/wolfmqtt && chmod 0700 /var/lib/wolfmqtt ``` +The POSIX backend names each record after its key in hex, so a key can be at +most `(NAME_MAX - 8) / 2` bytes: 123 on filesystems with the usual 255-byte +`NAME_MAX`. Sessions and subscriptions are keyed by client ID, retained +messages by topic, and offline-queue entries by client ID plus 3 bytes. With +this backend a client ID longer than 120 bytes loses its queued messages across +a restart, one longer than 123 bytes loses its whole session, and a retained +message whose topic is longer than 123 bytes is not kept. The broker still +serves them from memory while it runs. Use a custom backend through +`MqttBroker_SetPersistHooks()` if longer identifiers must persist. + +A custom backend must follow the same contract as the POSIX one. `kv_get` +treats `*inout_len` as the buffer capacity on input and sets the record length +on success, returns `MQTT_CODE_ERROR_NOT_FOUND` for a missing key, and returns +`MQTT_CODE_ERROR_OUT_OF_BUFFER` - never a truncated success - for a record +larger than the buffer. Every key `kv_iter` hands to its callback must be +accepted unchanged by `kv_get` and `kv_del`; the broker deletes records it +finds while iterating and repeats until none match. + | Macro | Default | Description | |---|---|---| -| `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions retained across restarts | +| `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions (connected and offline); a new persistent CONNECT beyond it is refused, existing sessions are never evicted | | `BROKER_MAX_OFFLINE_MSGS_PER_SUB` | 32 | Offline queue depth per session | -| `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 5 | On-disk record schema version | +| `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 6 | On-disk record schema version | + +Because existing persistent sessions are never evicted, any client allowed to +connect can use up `BROKER_MAX_PERSIST_SESSIONS` by opening persistent sessions +under fresh client IDs (a v3.1.1 CleanSession 0 session never expires); after +that, persistent CONNECTs with new client IDs are refused until sessions end, +and with persistence enabled the sessions survive a restart. Clients that +resume their own session are unaffected. Configure broker credentials +(`-u`/`-P`), size the cap for the deployment, and prefer bounded v5 Session +Expiry Intervals where this matters. The previous drop-oldest +policy let the same client destroy other clients' saved sessions instead. ### Encryption at rest diff --git a/README.md b/README.md index 4b6a71102..bb52b2353 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ These are the network connect / disconnect interfaces that wrap the MqttNet call * `int MqttClient_NetConnect(MqttClient *client, const char* host, word16 port, int timeout_ms, int use_tls, MqttTlsCb cb);` * `int MqttClient_NetDisconnect(MqttClient *client);` +* `int MqttClient_NetDisconnect_Wait(MqttClient *client);` - like `MqttClient_NetDisconnect`, but in `WOLFMQTT_MULTITHREAD` builds waits for other threads' network I/O on the client to finish first. Do not call it from a message, property or publish payload callback; where thread identities are available it returns `MQTT_CODE_ERROR_STAT` there instead of deadlocking. Helper functions: @@ -225,6 +226,8 @@ The multi-threading feature can also be used with the non-blocking socket (--ena If you are having issues with thread synchronization on Linux consider using not the conditional signal (`WOLFMQTT_NO_COND_SIGNAL`). +With `WOLFMQTT_NO_COND_SIGNAL`, or with `WOLFMQTT_USER_THREADING` unless `WOLFMQTT_SEM_SIGNAL` is defined, a write-only publish or `MqttClient_CancelMessage` whose response another thread is decoding waits for that thread to release the receive lock instead of being signalled. A thread that blocks in `MqttClient_WaitMessage` can then hold them up until its read returns. + ### Atomic publish and subscribe examples In the `examples/pub-sub` folder, there are two simple client examples: * mqtt-pub - publishes to a topic diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 0e0d6df05..c86adc03d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -133,7 +133,7 @@ typedef char wolfmqtt_broker_time_t_must_be_unsigned[ * arguments can appear in one log statement; the broker log path is single * threaded (as is PRINTF itself). Output is NUL-terminated and truncated to * fit. NULL src yields "(null)". */ -static const char* BrokerLog_Sanitize(const char* src) +WOLFMQTT_LOCAL const char* BrokerLog_Sanitize(const char* src) { static const char hex_digits[] = "0123456789abcdef"; static char pool[BROKER_LOG_SAN_POOL][BROKER_LOG_SAN_SZ]; @@ -1950,8 +1950,89 @@ static void BrokerProps_FreeClone(MqttProp* head) head = next; } } + +/* Encode e->props as PUBLISH Properties (no length prefix) into buf, or only + * size them when buf is NULL. Returns the length or a negative error. */ +WOLFMQTT_LOCAL int BrokerOutPub_EncodeProps(const BrokerOutPub* e, byte* buf) +{ + if (e == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } + if (e->props == NULL) { + return 0; + } + return MqttEncode_Props(MQTT_PACKET_TYPE_PUBLISH, e->props, buf); +} + +/* Decode PUBLISH Properties written by BrokerOutPub_EncodeProps into a + * broker-owned list on e. The whole buffer must decode, and a Message Expiry + * Interval must be present exactly when e->has_expiry is set, as + * BrokerOutPub_Alloc records it. */ +WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, + word32 len) +{ + MqttProp* decoded = NULL; + const MqttProp* prop; + int rc; + int expiry_found = 0; + + if (e == NULL || e->props != NULL || (buf == NULL && len > 0)) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } + if (len == 0) { + return e->has_expiry ? MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA) + : 0; + } + /* The broker's own property pool reference (MqttBroker_Init) covers + * restore, which runs before any client exists. */ + rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &decoded, (byte*)buf, + len, len); + if (rc >= 0 && (word32)rc != len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (rc >= 0) { + for (prop = decoded; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + expiry_found = 1; + break; + } + } + if (expiry_found != (e->has_expiry ? 1 : 0)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + } + if (rc >= 0) { + /* The decoded list points into buf, so keep a deep copy. */ + e->props = BrokerProps_Clone(decoded); + if (e->props == NULL) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + } + if (decoded != NULL) { + (void)MqttProps_Free(decoded); + } + return (rc < 0) ? rc : 0; +} #endif /* WOLFMQTT_V5 */ +/* MQTT 5.0 section 3.3.2.3.3: has the copy's Message Expiry Interval passed? + * Time is in whole seconds, so an explicit zero interval has passed once a + * second has: a copy whose delivery starts at once still goes out. */ +static int BrokerOutPub_Expired(const BrokerOutPub* e, + WOLFMQTT_BROKER_TIME_T now) +{ + WOLFMQTT_BROKER_TIME_T elapsed; + + if (!e->has_expiry || now < e->enq_time) { + return 0; + } + elapsed = now - e->enq_time; + if (e->expiry_sec == 0) { + return (elapsed > 0) ? 1 : 0; + } + return (elapsed >= e->expiry_sec) ? 1 : 0; +} + /* Free a single queue entry (topic, payload, props, the entry itself). */ static void BrokerOutPub_Free(BrokerOutPub* e) { @@ -2040,6 +2121,9 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, { BrokerOutPub* e; size_t topic_len; +#ifdef WOLFMQTT_V5 + const MqttProp* prop; +#endif if (topic == NULL) { return NULL; @@ -2078,8 +2162,18 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, BrokerOutPub_Free(e); return NULL; } + /* MQTT 5.0 section 3.3.2.3.3: the copy expires this long after it + * was queued. */ + for (prop = e->props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + e->has_expiry = 1; + e->expiry_sec = prop->data_int; + break; + } + } } #endif + e->enq_time = WOLFMQTT_BROKER_GET_TIME_S(); return e; } @@ -2123,6 +2217,36 @@ static void BrokerClient_FreeOutQueue(BrokerClient* bc) bc->out_q_pending_len = 0; } +/* Unlink cur (preceded by prev) from bc's out_q, drop any persisted copy and + * free it. Returns the entry that followed cur. */ +static BrokerOutPub* BrokerClient_OutQRemove(BrokerClient* bc, + BrokerOutPub* prev, BrokerOutPub* cur) +{ + BrokerOutPub* next = cur->next; + + if (prev == NULL) { + bc->out_q_head = next; + } + else { + prev->next = next; + } + if (bc->out_q_tail == cur) { + bc->out_q_tail = prev; + } + bc->out_q_count--; +#ifdef WOLFMQTT_BROKER_PERSIST + /* If this entry was previously shadow-written (orphan path), drop the + * disk record now so a future restart cannot replay it. Idempotent for + * entries that were never persisted (QoS 0, or not-yet-orphaned). */ + if (cur->qos > MQTT_QOS_0 && BROKER_STR_VALID(bc->client_id)) { + (void)BrokerPersist_DelOutPub(bc->broker, bc->client_id, + cur->packet_id); + } +#endif + BrokerOutPub_Free(cur); + return next; +} + /* Send as many QUEUED entries from out_q as the inflight cap allows. * * Ordering: walks from out_q_head, never reorders. Already-sent entries @@ -2156,6 +2280,10 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) while (cur != NULL) { MqttPublish out_pub; int enc_rc; + WOLFMQTT_BROKER_TIME_T now; + #ifdef WOLFMQTT_V5 + MqttProp* prop; + #endif if (cur->state != BROKER_OUTQ_QUEUED) { #if WOLFMQTT_MAX_QOS >= 2 @@ -2203,6 +2331,25 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) cur = cur->next; continue; } + /* Expiry applies until onward delivery starts; a deferred write or a + * DUP retransmission reuses the interval it was first sent with. */ + if (cur->has_expiry && bc->out_q_pending_len == 0 && + !cur->retransmit_dup && !cur->partial_send) { + now = WOLFMQTT_BROKER_GET_TIME_S(); + if (BrokerOutPub_Expired(cur, now)) { + /* [MQTT-3.3.2-5] Delete the expired copy */ + WBLOG_DBG(bc->broker, + "broker: drain dropped expired PUBLISH sock=%d topic=%s", + (int)bc->sock, BrokerLog_Sanitize(cur->topic)); + cur = BrokerClient_OutQRemove(bc, prev, cur); + continue; + } + if (now > cur->enq_time) { + /* Keep the same deadline, measured from now */ + cur->expiry_sec -= (word32)(now - cur->enq_time); + cur->enq_time = now; + } + } if (cur->qos > MQTT_QOS_0 && bc->out_q_inflight >= effective_cap) { /* Cap reached. Stop here - cannot send anything behind it * either, because [MQTT-4.6.0-3] requires ordered delivery. */ @@ -2224,6 +2371,13 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) #ifdef WOLFMQTT_V5 out_pub.protocol_level = cur->protocol_level; out_pub.props = cur->props; + /* [MQTT-3.3.2-6] Send the interval remaining after the wait */ + for (prop = cur->props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + prop->data_int = cur->expiry_sec; + break; + } + } #endif enc_rc = MqttEncode_Publish(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), @@ -2245,33 +2399,7 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) BROKER_FORCE_ZERO(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc)); /* Drop just this entry and continue. Encoding failure for * a single message is not fatal to the connection. */ - if (prev == NULL) { - bc->out_q_head = cur->next; - } - else { - prev->next = cur->next; - } - if (bc->out_q_tail == cur) { - bc->out_q_tail = prev; - } - bc->out_q_count--; - { - BrokerOutPub* free_me = cur; - cur = cur->next; - #ifdef WOLFMQTT_BROKER_PERSIST - /* If this entry was previously shadow-written (orphan - * path), drop the disk record now so a future restart - * cannot replay an undeliverable message. Idempotent - * for entries that were never persisted (QoS 0, or - * not-yet-orphaned). */ - if (free_me->qos > MQTT_QOS_0 && - BROKER_STR_VALID(bc->client_id)) { - (void)BrokerPersist_DelOutPub(bc->broker, - bc->client_id, free_me->packet_id); - } - #endif - BrokerOutPub_Free(free_me); - } + cur = BrokerClient_OutQRemove(bc, prev, cur); continue; } if (bc->out_q_pending_len != 0 && @@ -2656,6 +2784,12 @@ static void BrokerClient_Free(BrokerClient* bc) BROKER_FORCE_ZERO(bc->will_payload, bc->will_payload_len); WOLFMQTT_FREE(bc->will_payload); } +#ifdef WOLFMQTT_V5 + if (bc->will_props) { + BROKER_FORCE_ZERO(bc->will_props, bc->will_props_len); + WOLFMQTT_FREE(bc->will_props); + } +#endif #endif if (bc->tx_buf) { BROKER_FORCE_ZERO(bc->tx_buf, bc->tx_buf_len); @@ -2767,6 +2901,8 @@ static BrokerClient* BrokerClient_Add(MqttBroker* broker, #endif } else if (bc != NULL) { + /* The caller closes the rejected socket */ + bc->sock = BROKER_SOCKET_INVALID; BrokerClient_Free(bc); bc = NULL; } @@ -3599,6 +3735,32 @@ static BrokerOrphanSession* BrokerOrphan_Find(MqttBroker* broker, return NULL; } +/* Persistent Sessions held by ClientIds other than client_id: saved offline + * Sessions plus established connections whose Session outlives them. A + * Session client_id already holds is excluded, since resuming or taking it + * over does not add one. */ +static int BrokerSession_CountOthers(MqttBroker* broker, + const char* client_id) +{ + BrokerOrphanSession* o; + BrokerClient* c; + int count = 0; + + for (o = broker->orphan_sessions; o != NULL; o = o->next) { + if (o->client_id != NULL && XSTRCMP(o->client_id, client_id) != 0) { + count++; + } + } + for (c = broker->clients; c != NULL; c = c->next) { + if (c->session_established && c->session_expiry_sec != 0 && + BROKER_STR_VALID(c->client_id) && + XSTRCMP(c->client_id, client_id) != 0) { + count++; + } + } + return count; +} + /* Free everything an orphan owns (queue entries + client_id) but do * NOT unlink from broker->orphan_sessions; the caller does that. */ static void BrokerOrphan_FreeContents(BrokerOrphanSession* o) @@ -3831,7 +3993,8 @@ static BrokerOrphanSession* BrokerOrphan_Take(MqttBroker* broker, #endif } - /* Cap check; evict oldest if at limit. */ + /* Backstop only: BrokerHandle_Connect refuses a new persistent Session at + * BROKER_MAX_PERSIST_SESSIONS, so the cap is not normally reached here. */ while (broker->orphan_session_count >= BROKER_MAX_PERSIST_SESSIONS) { if (!BrokerOrphan_EvictOldest(broker)) { break; @@ -3943,9 +4106,6 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) o->out_q_tail = NULL; o->out_q_count = 0; o->out_q_inflight = 0; - 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; @@ -5304,6 +5464,211 @@ static int BrokerTopicMatch(const char* filter, const char* topic); /* LWT (Last Will and Testament) helpers */ /* -------------------------------------------------------------------------- */ #ifdef WOLFMQTT_BROKER_WILL +#ifdef WOLFMQTT_V5 +/* Will Properties carried onto the Will PUBLISH. The Will Delay Interval only + * schedules it (MQTT 5.0 section 3.1.3.2). */ +static int BrokerWill_IsPublishProp(const MqttProp* prop) +{ + return (prop->type == MQTT_PROP_PAYLOAD_FORMAT_IND || + prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL || + prop->type == MQTT_PROP_CONTENT_TYPE || + prop->type == MQTT_PROP_RESP_TOPIC || + prop->type == MQTT_PROP_CORRELATION_DATA || + prop->type == MQTT_PROP_USER_PROP); +} + +/* Encode the publishable Will Properties, keeping their order + * [MQTT-3.1.3-10], into buf, or only size them when buf is NULL. Returns the + * encoded length or a negative error. */ +static int BrokerWill_EncodeProps(const MqttProp* props, byte* buf) +{ + MqttProp one; + int len = 0; + int rc = 0; + + for (; props != NULL && rc >= 0; props = props->next) { + if (BrokerWill_IsPublishProp(props)) { + one = *props; + one.next = NULL; + rc = MqttEncode_Props(MQTT_PACKET_TYPE_PUBLISH, &one, + (buf != NULL) ? &buf[len] : NULL); + if (rc >= 0) { + len += rc; + } + } + } + if (rc >= 0 && len > 0xFFFF) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } + return (rc < 0) ? rc : len; +} + +#ifdef WOLFMQTT_STATIC_MEMORY +/* Number of publishable Will Properties in props. */ +static int BrokerWill_CountProps(const MqttProp* props) +{ + int count = 0; + + for (; props != NULL; props = props->next) { + if (BrokerWill_IsPublishProp(props)) { + count++; + } + } + return count; +} +#endif + +/* Take one 2-byte-length-prefixed field (MQTT 5.0 section 1.5.4/1.5.6) from + * buf at *pos. */ +static int BrokerWill_TakeField(const byte* buf, word16 len, word16* pos, + const byte** data, word16* data_len) +{ + if (len - *pos < 2) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + *data_len = (word16)(((word16)buf[*pos] << 8) | buf[*pos + 1]); + *pos = (word16)(*pos + 2); + if (len - *pos < *data_len) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + *data = &buf[*pos]; + *pos = (word16)(*pos + *data_len); + return 0; +} + +/* Rebuild Will Properties written by BrokerWill_EncodeProps as a list in + * nodes, without the shared property pool, so a Will can always be published + * with them. String and binary values point into buf, which must outlive the + * list. With nodes NULL the properties are only counted. Returns the count + * or a negative error. */ +static int BrokerWill_DecodeProps(const byte* buf, word16 len, + MqttProp* nodes, int max_nodes) +{ + MqttProp* prop = NULL; + const byte* data; + word16 data_len; + word16 pos = 0; + byte id; + int count = 0; + int rc = 0; + + while (rc == 0 && pos < len) { + id = buf[pos++]; + if (nodes != NULL) { + if (count >= max_nodes) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; + } + prop = &nodes[count]; + XMEMSET(prop, 0, sizeof(*prop)); + prop->type = (MqttPropertyType)id; + if (count > 0) { + nodes[count - 1].next = prop; + } + } + switch (id) { + case MQTT_PROP_PAYLOAD_FORMAT_IND: + if (len - pos < 1) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (prop != NULL) { + prop->data_byte = buf[pos]; + } + pos++; + break; + case MQTT_PROP_MSG_EXPIRY_INTERVAL: + if (len - pos < 4) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (prop != NULL) { + prop->data_int = ((word32)buf[pos] << 24) | + ((word32)buf[pos + 1] << 16) | + ((word32)buf[pos + 2] << 8) | (word32)buf[pos + 3]; + } + pos = (word16)(pos + 4); + break; + case MQTT_PROP_CONTENT_TYPE: + case MQTT_PROP_RESP_TOPIC: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + /* The encoder only reads through these pointers. */ + prop->data_str.str = (char*)data; + prop->data_str.len = data_len; + } + break; + case MQTT_PROP_CORRELATION_DATA: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + prop->data_bin.data = (byte*)data; + prop->data_bin.len = data_len; + } + break; + case MQTT_PROP_USER_PROP: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + prop->data_str.str = (char*)data; + prop->data_str.len = data_len; + } + if (rc == 0) { + rc = BrokerWill_TakeField(buf, len, &pos, &data, + &data_len); + } + if (rc == 0 && prop != NULL) { + prop->data_str2.str = (char*)data; + prop->data_str2.len = data_len; + } + break; + default: + /* Only publishable Will Properties are ever stored. */ + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (rc == 0) { + count++; + } + } + return (rc < 0) ? rc : count; +} + +/* Keep the publishable Will Properties until the Will is published. */ +static int BrokerClient_StoreWillProps(BrokerClient* bc, + const MqttProp* props) +{ + int len; + int rc = MQTT_CODE_SUCCESS; + + len = BrokerWill_EncodeProps(props, NULL); + if (len < 0) { + rc = len; + } +#ifdef WOLFMQTT_STATIC_MEMORY + else if (len > BROKER_MAX_WILL_PROPS_LEN) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } +#else + else if (len > 0) { + bc->will_props = (byte*)WOLFMQTT_MALLOC((size_t)len); + if (bc->will_props == NULL) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + } +#endif + if (rc == MQTT_CODE_SUCCESS && len > 0) { + rc = BrokerWill_EncodeProps(props, bc->will_props); + if (rc == len) { + bc->will_props_len = (word16)len; + rc = MQTT_CODE_SUCCESS; + } + else if (rc >= 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } + } + return rc; +} +#endif /* WOLFMQTT_V5 */ + static void BrokerClient_ClearWill(BrokerClient* bc) { if (bc == NULL) { @@ -5313,6 +5678,18 @@ static void BrokerClient_ClearWill(BrokerClient* bc) bc->will_qos = MQTT_QOS_0; bc->will_retain = 0; bc->will_delay_sec = 0; +#ifdef WOLFMQTT_V5 +#ifdef WOLFMQTT_STATIC_MEMORY + BROKER_FORCE_ZERO(bc->will_props, sizeof(bc->will_props)); +#else + if (bc->will_props != NULL) { + BROKER_FORCE_ZERO(bc->will_props, bc->will_props_len); + WOLFMQTT_FREE(bc->will_props); + bc->will_props = NULL; + } +#endif + bc->will_props_len = 0; +#endif #ifdef WOLFMQTT_STATIC_MEMORY BROKER_FORCE_ZERO(bc->will_topic, sizeof(bc->will_topic)); BROKER_FORCE_ZERO(bc->will_payload, sizeof(bc->will_payload)); @@ -5336,6 +5713,23 @@ static void BrokerClient_ClearWill(BrokerClient* bc) /* Pending will management (v5 Will Delay Interval) */ /* -------------------------------------------------------------------------- */ +#ifndef WOLFMQTT_STATIC_MEMORY +/* Scrub and free a pending Will's stored properties. */ +static void BrokerPendingWill_FreeProps(BrokerPendingWill* pw) +{ +#ifdef WOLFMQTT_V5 + if (pw->props != NULL) { + BROKER_FORCE_ZERO(pw->props, pw->props_len); + WOLFMQTT_FREE(pw->props); + pw->props = NULL; + } + pw->props_len = 0; +#else + (void)pw; +#endif +} +#endif + /* Add a pending will to be published after delay expires */ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) { @@ -5384,6 +5778,12 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) bc->will_payload_len); pw->payload_len = bc->will_payload_len; } + #ifdef WOLFMQTT_V5 + if (bc->will_props_len > 0) { + XMEMCPY(pw->props, bc->will_props, bc->will_props_len); + pw->props_len = bc->will_props_len; + } + #endif } } #else @@ -5432,6 +5832,18 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) rc = MQTT_CODE_ERROR_MEMORY; } } + #ifdef WOLFMQTT_V5 + if (rc == MQTT_CODE_SUCCESS && bc->will_props_len > 0) { + pw->props = (byte*)WOLFMQTT_MALLOC(bc->will_props_len); + if (pw->props != NULL) { + XMEMCPY(pw->props, bc->will_props, bc->will_props_len); + pw->props_len = bc->will_props_len; + } + else { + rc = MQTT_CODE_ERROR_MEMORY; + } + } + #endif } if (rc == MQTT_CODE_SUCCESS) { pw->next = broker->pending_wills; @@ -5449,6 +5861,7 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); } #endif @@ -5533,6 +5946,7 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); return; } @@ -5566,6 +5980,7 @@ static void BrokerPendingWill_FreeAll(MqttBroker* broker) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); pw = next; } @@ -5573,9 +5988,16 @@ static void BrokerPendingWill_FreeAll(MqttBroker* broker) #endif } +#ifdef WOLFMQTT_V5 +static MqttProp* BrokerProps_Find(MqttProp* head, MqttPropertyType type); +#endif static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, - MqttQoS qos, byte retain); + MqttQoS qos, byte retain +#ifdef WOLFMQTT_V5 + , const byte* props_buf, word16 props_len +#endif + ); /* Process pending wills - publish any that have expired their delay */ static int BrokerPendingWill_Process(MqttBroker* broker) @@ -5605,7 +6027,11 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, - pw->payload, pw->payload_len, pw->qos, pw->retain); + pw->payload, pw->payload_len, pw->qos, pw->retain + #ifdef WOLFMQTT_V5 + , pw->props, pw->props_len + #endif + ); BROKER_FORCE_ZERO(pw, sizeof(BrokerPendingWill)); activity = 1; } @@ -5620,7 +6046,11 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, - pw->payload, pw->payload_len, pw->qos, pw->retain); + pw->payload, pw->payload_len, pw->qos, pw->retain + #ifdef WOLFMQTT_V5 + , pw->props, pw->props_len + #endif + ); if (prev) { prev->next = next; } @@ -5649,6 +6079,7 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); activity = 1; } @@ -5812,6 +6243,12 @@ static int BrokerRetained_DeliverToClient(MqttBroker* broker, #endif enc_rc = MqttEncode_Publish(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length declaring + * bytes the frame does not carry [MQTT 3.1.1 section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(bc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " "len=%u qos=%d", (int)bc->sock, @@ -6038,14 +6475,75 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) BrokerClient_PublishWillImmediate(broker, bc->will_topic, bc->will_payload, bc->will_payload_len, bc->will_qos, - bc->will_retain); + bc->will_retain + #ifdef WOLFMQTT_V5 + , bc->will_props, bc->will_props_len + #endif + ); BrokerClient_ClearWill(bc); } +/* A persistent subscription whose client is offline. */ +static int BrokerSub_IsOffline(const BrokerSub* sub) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + return (sub->in_use && + (sub->client == NULL || !sub->client->connected) && + BROKER_STR_VALID(sub->client_id)); +#else + return (sub->client == NULL && sub->client_id != NULL); +#endif +} + +/* An offline Session gets one queued copy at the highest QoS of all of its + * subscriptions matching topic [MQTT-3.3.4-2]; a copy per subscription could + * let its bounded queue evict the highest-QoS one. Returns 1 when sub is the + * Session's first matching subscription, setting *eff_qos to that QoS capped + * at qos, or 0 when an earlier subscription already covers the Session. */ +static int BrokerSubs_OfflineFirstMatch(MqttBroker* broker, + const BrokerSub* sub, const char* topic, MqttQoS qos, MqttQoS* eff_qos) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + int i; +#endif + const BrokerSub* s; + MqttQoS max_qos = MQTT_QOS_0; + int first = -1; + +#ifdef WOLFMQTT_STATIC_MEMORY + for (i = 0; i < BROKER_MAX_SUBS; i++) { + s = &broker->subs[i]; +#else + for (s = broker->subs; s != NULL; s = s->next) { +#endif + if (!BrokerSub_IsOffline(s) || !BROKER_STR_VALID(s->filter) || + XSTRCMP(s->client_id, sub->client_id) != 0 || + !BrokerTopicMatch(s->filter, topic)) { + continue; + } + if (first < 0) { + first = (s == sub) ? 1 : 0; + if (first == 0) { + /* An earlier subscription queues this Session's copy. */ + break; + } + } + if (s->qos > max_qos) { + max_qos = s->qos; + } + } + *eff_qos = (qos < max_qos) ? qos : max_qos; + return (first == 1); +} + /* Publish a will message immediately (shared by direct and deferred paths) */ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, - MqttQoS qos, byte retain) + MqttQoS qos, byte retain +#ifdef WOLFMQTT_V5 + , const byte* props_buf, word16 props_len +#endif + ) { #ifdef WOLFMQTT_STATIC_MEMORY int i; @@ -6056,11 +6554,62 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, word32 clients_gen_snapshot; int t; #endif +#ifdef WOLFMQTT_V5 + MqttProp stack_props[BROKER_WILL_PROPS_STACK_ENTRIES]; + MqttProp* nodes = stack_props; + MqttProp* props = NULL; + MqttProp* expiry_prop; + word32 expiry = 0; + int node_count = BROKER_WILL_PROPS_STACK_ENTRIES; + int prop_rc; +#endif if (broker == NULL || topic == NULL) { return; } +#ifdef WOLFMQTT_V5 + /* The Will Properties travel with the Will on every delivery path + * (MQTT 5.0 section 3.1.3.2). */ + if (props_buf != NULL && props_len > 0) { + prop_rc = BrokerWill_DecodeProps(props_buf, props_len, NULL, 0); + #ifndef WOLFMQTT_STATIC_MEMORY + if (prop_rc > BROKER_WILL_PROPS_STACK_ENTRIES) { + nodes = (MqttProp*)WOLFMQTT_MALLOC(sizeof(MqttProp) * + (size_t)prop_rc); + if (nodes == NULL) { + nodes = stack_props; + prop_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + else { + node_count = prop_rc; + } + } + #endif + if (prop_rc > 0) { + prop_rc = BrokerWill_DecodeProps(props_buf, props_len, nodes, + node_count); + } + if (prop_rc > 0) { + props = nodes; + } + else if (prop_rc < 0) { + /* Not reachable for properties this broker encoded, except on + * heap exhaustion in a dynamic build. */ + WBLOG_ERR(broker, + "broker: will properties rebuild failed rc=%d, publishing " + "without them topic=%s", prop_rc, BrokerLog_Sanitize(topic)); + } + expiry_prop = BrokerProps_Find(props, MQTT_PROP_MSG_EXPIRY_INTERVAL); + if (expiry_prop != NULL) { + expiry = expiry_prop->data_int; + } + } + #ifndef WOLFMQTT_BROKER_RETAINED + (void)expiry; /* only the retained store uses it */ + #endif +#endif + /* Handle retain flag on will message */ if (retain) { if (payload_len == 0) { @@ -6068,7 +6617,13 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } else { int ret_rc = BrokerRetained_Store(broker, topic, payload, - payload_len, qos, 0); + payload_len, qos, + #ifdef WOLFMQTT_V5 + expiry + #else + 0 + #endif + ); if (ret_rc != MQTT_CODE_SUCCESS) { WBLOG_ERR(broker, "Retained store failed: %s", MqttClient_ReturnCodeToString(ret_rc)); @@ -6112,7 +6667,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, enqueue_rc = BrokerStaticOrphan_Enqueue(broker, orphan, topic, payload, payload_len, eff_qos, 0, 0 #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (enqueue_rc != MQTT_CODE_SUCCESS) { @@ -6149,9 +6704,19 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } #ifdef WOLFMQTT_V5 out_pub.protocol_level = wc->protocol_level; + if (wc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + out_pub.props = props; + } #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length + * declaring bytes the frame does not carry [MQTT 3.1.1 + * section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(wc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { wr_rc = BrokerClient_WriteDirect(wc, enc_rc); /* Scrub tx_buf unless still in-progress (CONTINUE). */ @@ -6178,15 +6743,16 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BROKER_STR_VALID(sub->client_id) && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { - MqttQoS eff_qos = (qos < sub->qos) ? qos : sub->qos; - if (eff_qos >= MQTT_QOS_1) { + MqttQoS eff_qos; + if (BrokerSubs_OfflineFirstMatch(broker, sub, topic, qos, + &eff_qos) && eff_qos >= MQTT_QOS_1) { BrokerStaticOrphanSession* orphan = BrokerStaticOrphan_Find(broker, sub->client_id); if (orphan != NULL) { int enqueue_rc = BrokerStaticOrphan_Enqueue(broker, orphan, topic, payload, payload_len, eff_qos, 0, 0 #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (enqueue_rc != MQTT_CODE_SUCCESS) { @@ -6216,6 +6782,28 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BrokerFanout_Add(&targets, sub->client, (qos < sub->qos) ? qos : sub->qos); } + /* Offline persistent Session: queue QoS 1/2 for delivery on + * reconnect, as the PUBLISH fan-out does. */ + else if (sub->client == NULL && sub->client_id != NULL && + BROKER_STR_VALID(sub->filter) && + BrokerTopicMatch(sub->filter, topic)) { + MqttQoS eff_qos; + if (BrokerSubs_OfflineFirstMatch(broker, sub, topic, qos, + &eff_qos) && eff_qos > MQTT_QOS_0) { + BrokerOrphanSession* o = BrokerOrphan_Find(broker, + sub->client_id); + if (o != NULL && BrokerOrphan_Enqueue(broker, o, topic, + payload, payload_len, eff_qos, 0 + #ifdef WOLFMQTT_V5 + , props + #endif + ) < 0) { + WBLOG_ERR(broker, + "broker: offline will queue failed client_id=%s", + BrokerLog_Sanitize(sub->client_id)); + } + } + } } if (targets.truncated) { WBLOG_ERR(broker, @@ -6236,6 +6824,12 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, !BrokerClients_StillLinked(broker, wc)) { continue; } + /* A write started on a connection being taken over could leave its + * Session Taken Over DISCONNECT unsent. QoS 0 to it is dropped and + * QoS 1/2 is only queued, for the Session if it is resumed. */ + if (wc->takeover_closing && eff_qos == MQTT_QOS_0) { + continue; + } if (eff_qos >= MQTT_QOS_1 || wc->connack_pending_len != 0 || wc->out_q_pending_len != 0 || @@ -6268,7 +6862,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BrokerOutPub* e = BrokerOutPub_Alloc(topic, (payload_len > 0) ? payload : NULL, payload_len #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (e == NULL) { @@ -6295,7 +6889,9 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, e->protocol_level = wc->protocol_level; #endif BrokerClient_EnqueueOutPub(wc, e); - BrokerClient_DrainOutQueue(wc); + if (!wc->takeover_closing) { + BrokerClient_DrainOutQueue(wc); + } } } } @@ -6313,9 +6909,18 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, out_pub.total_len = payload_len; #ifdef WOLFMQTT_V5 out_pub.protocol_level = wc->protocol_level; + if (wc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + out_pub.props = props; + } #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length declaring + * bytes the frame does not carry [MQTT 3.1.1 section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(wc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { wr_rc = BrokerClient_WriteDirect(wc, enc_rc); /* Scrub tx_buf unless still in-progress (CONTINUE). */ @@ -6327,6 +6932,12 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } BrokerFanout_Free(&targets); #endif +#ifdef WOLFMQTT_V5 + if (nodes != stack_props) { + BROKER_FORCE_ZERO(nodes, sizeof(MqttProp) * (size_t)node_count); + WOLFMQTT_FREE(nodes); + } +#endif } #endif /* WOLFMQTT_BROKER_WILL */ @@ -6519,6 +7130,28 @@ static int BrokerSend_Disconnect(BrokerClient* bc, byte reason_code) /* Packet handlers */ /* -------------------------------------------------------------------------- */ +/* Wipe the plaintext CONNECT in bc->rx_buf (the decoded username, password + * and v5 authentication data point into it) and the bc->username / + * bc->password copies. Nothing reads them once CONNECT is handled. */ +static void BrokerClient_ScrubConnect(BrokerClient* bc, int rx_len) +{ + BROKER_FORCE_ZERO(bc->rx_buf, (word32)rx_len); +#ifdef WOLFMQTT_BROKER_AUTH +#ifdef WOLFMQTT_STATIC_MEMORY + BROKER_FORCE_ZERO(bc->username, BROKER_MAX_USERNAME_LEN); + BROKER_FORCE_ZERO(bc->password, BROKER_MAX_PASSWORD_LEN); +#else + if (bc->username != NULL) { + BROKER_FORCE_ZERO(bc->username, XSTRLEN(bc->username) + 1); + } + if (bc->password != NULL) { + BROKER_FORCE_ZERO(bc->password, (size_t)bc->password_len + 1); + } +#endif + bc->password_len = 0; +#endif +} + /* Returns: > 0 success, 0 auth rejected (CONNACK sent with refused), * < 0 error */ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, @@ -6547,6 +7180,10 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #ifdef WOLFMQTT_V5 int auto_assigned = 0; #endif +#if defined(WOLFMQTT_STATIC_MEMORY) && \ + (defined(WOLFMQTT_V5) || WOLFMQTT_MAX_QOS >= 1) + int will_props_len = 0; +#endif XMEMSET(&mc, 0, sizeof(mc)); XMEMSET(&ack, 0, sizeof(ack)); @@ -6559,7 +7196,18 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: CONNECT recv sock=%d len=%d", (int)bc->sock, rx_len); rc = MqttDecode_Connect(bc->rx_buf, rx_len, &mc); - if (rc < 0) { + /* The decoder records the Protocol Level once the Protocol Name checks + * out, before the rest of the packet, whose layout depends on that + * level. A CONNECT for a level this build does not support may then fail + * to decode, yet [MQTT-3.1.2-2] still calls for CONNACK 0x01: let it + * reach the unsupported-level refusal below. Level 0 means the decoder + * stopped before reading it. */ + if (rc < 0 && (mc.protocol_level == 0 || + mc.protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_4 + #ifdef WOLFMQTT_V5 + || mc.protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_5 + #endif + )) { WBLOG_ERR(broker, "broker: CONNECT decode failed rc=%d", rc); #ifdef WOLFMQTT_V5 if (mc.props) { (void)MqttProps_Free(mc.props); } @@ -7005,6 +7653,42 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } goto send_connack; } +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_STATIC_MEMORY) + /* Will Properties are v5 only. Refuse rather than drop any. */ + will_props_len = BrokerWill_EncodeProps(mc.lwt_msg->props, NULL); + if (will_props_len > BROKER_MAX_WILL_PROPS_LEN || + BrokerWill_CountProps(mc.lwt_msg->props) > + BROKER_WILL_PROPS_STACK_ENTRIES) { + WBLOG_ERR(broker, + "broker: LWT properties too large (max %d) sock=%d", + BROKER_MAX_WILL_PROPS_LEN, (int)bc->sock); + ack.return_code = MQTT_REASON_PACKET_TOO_LARGE; + goto send_connack; + } +#endif +#if defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 + /* A Will at QoS 1/2 after the WOLFMQTT_MAX_QOS limit may be queued + * for an offline Session, so payload and properties must fit one + * offline slot together. */ + if (mc.lwt_msg->qos > MQTT_QOS_0 && + (word32)will_props_len + mc.lwt_msg->total_len > + BROKER_MAX_STATIC_OFFLINE_DATA_LEN) { + WBLOG_ERR(broker, + "broker: LWT payload and properties too large (max %d) " + "sock=%d", BROKER_MAX_STATIC_OFFLINE_DATA_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_PACKET_TOO_LARGE; + } + else + #endif + { + ack.return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL; + } + goto send_connack; + } +#endif #ifdef WOLFMQTT_V5 /* [MQTT-3.2.2-11] A v5 Server that caps Maximum QoS below the requested * Will QoS must refuse the connection, not silently downgrade the Will @@ -7070,6 +7754,12 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #endif bc->will_payload_len = wp_len; } +#ifdef WOLFMQTT_V5 + if (will_stored && BrokerClient_StoreWillProps(bc, + mc.lwt_msg->props) != MQTT_CODE_SUCCESS) { + will_stored = 0; + } +#endif if (!will_stored) { /* A Will missing its topic or payload would be dropped or * published altered, so refuse the connection instead. */ @@ -7275,6 +7965,28 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, orphan = BrokerStaticOrphan_Find(broker, bc->client_id); had_static_session = (orphan != NULL); } +#else + /* Bound persistent Sessions the same way: at BROKER_MAX_PERSIST_SESSIONS + * a new one is refused here, rather than admitted and later made room for + * by evicting another client's saved Session when this one goes offline. + * Resuming or taking over this ClientId's own Session is not counted. */ + if (bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id) && + BrokerSession_CountOthers(broker, bc->client_id) >= + BROKER_MAX_PERSIST_SESSIONS) { + WBLOG_ERR(broker, + "broker: persistent-session quota exceeded sock=%d", + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_QUOTA_EXCEEDED; + } + else + #endif + { + ack.return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL; + } + goto send_connack; + } #endif bc->session_established = 1; @@ -7290,6 +8002,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " "old sock=%d", BrokerLog_Sanitize(bc->client_id), (int)old->sock); +#ifndef WOLFMQTT_STATIC_MEMORY + old->takeover_closing = 1; +#endif #ifdef ENABLE_MQTT_WEBSOCKET /* Guard old across its takeover fan-out: a re-entrant * LWS_CALLBACK_CLOSED for old's dropped socket must take the @@ -7305,9 +8020,23 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, BrokerClient_PublishWill(broker, old); } else { +#ifdef WOLFMQTT_BROKER_WILL + /* MQTT 5.0 section 3.1.4: a resumed Session suppresses a + * delayed Will [MQTT-3.1.3-9]; an ending Session or a zero + * Will Delay publishes it now. Done before the DISCONNECT so + * a copy for the old connection itself (subscribed to its + * own Will topic) is not written after it [MQTT-3.14.4-1]. */ + if (old->will_delay_sec > 0 && !mc.clean_session && + old->session_expiry_sec != 0) { + BrokerClient_ClearWill(old); + } + else { + old->will_delay_sec = 0; + BrokerClient_PublishWill(broker, old); + } +#endif BrokerSend_Disconnect(old, MQTT_REASON_SESSION_TAKEN_OVER); - BrokerClient_ClearWill(old); } #else BrokerClient_PublishWill(broker, old); @@ -7437,9 +8166,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #else else { /* Persistent session reconnect: pick up any queued messages - * left by the prior incarnation. Inherits session_expiry - * from the orphan when this CONNECT did not specify one. - * Drain is invoked further down, after CONNACK is sent. */ + * left by the prior incarnation. The Session Expiry Interval + * comes from this CONNECT [MQTT-3.1.2.11.2]. Drain is invoked + * further down, after CONNACK is sent. */ if (BrokerOrphan_Reclaim(broker, bc)) { session_present = 1; } @@ -7508,9 +8237,11 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, * Closing now would deliver only the bytes already written, so the * client could never read why it was refused. Keep the socket open * until BrokerClient_ResumeConnAck finishes the write; it then - * performs the close. */ + * performs the close. The credentials are not needed for that, + * so wipe them now instead of when the client is freed. */ bc->connack_pending_len = ack_len; bc->connack_refused = 1; + BrokerClient_ScrubConnect(bc, rx_len); } #endif return 0; @@ -7582,23 +8313,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, * re-auth path. Scrub them all now so credentials reside in memory only * for the duration of CONNECT processing rather than the whole session. * Refused/failed CONNECTs return earlier and are wiped when the caller - * frees the client. Mirrors the client-side CLIENT_FORCE_ZERO hardening - * in mqtt_client.c. */ - BROKER_FORCE_ZERO(bc->rx_buf, (word32)rx_len); -#ifdef WOLFMQTT_BROKER_AUTH -#ifdef WOLFMQTT_STATIC_MEMORY - BROKER_FORCE_ZERO(bc->username, BROKER_MAX_USERNAME_LEN); - BROKER_FORCE_ZERO(bc->password, BROKER_MAX_PASSWORD_LEN); -#else - if (bc->username != NULL) { - BROKER_FORCE_ZERO(bc->username, XSTRLEN(bc->username) + 1); - } - if (bc->password != NULL) { - BROKER_FORCE_ZERO(bc->password, (size_t)bc->password_len + 1); - } -#endif - bc->password_len = 0; -#endif + * frees the client, or above if their CONNACK is deferred. Mirrors the + * client-side CLIENT_FORCE_ZERO hardening in mqtt_client.c. */ + BrokerClient_ScrubConnect(bc, rx_len); return rc; } @@ -7608,6 +8325,9 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, { int rc; int i; +#ifdef WOLFMQTT_BROKER_PERSIST + int subs_changed = 0; +#endif MqttSubscribe sub; MqttTopic topic_buf[MAX_MQTT_TOPICS]; byte return_codes[MAX_MQTT_TOPICS]; @@ -7617,6 +8337,7 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, sub.protocol_level = bc->protocol_level; #endif XMEMSET(topic_buf, 0, sizeof(topic_buf)); + XMEMSET(return_codes, 0, sizeof(return_codes)); sub.topics = topic_buf; WBLOG_INFO(broker, "broker: SUBSCRIBE recv sock=%d len=%d", (int)bc->sock, rx_len); @@ -7679,6 +8400,11 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, #endif { sub_rc = BrokerSubs_Add(broker, bc, f, flen, topic_qos); + #ifdef WOLFMQTT_BROKER_PERSIST + if (sub_rc >= 0) { + subs_changed = 1; + } + #endif } if (sub_rc < 0) { granted_qos = (MqttQoS)fail_code; @@ -7762,19 +8488,21 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, * avoid reading past the end of the return_codes array */ rc = BrokerSend_SubAck(bc, sub.packet_id, return_codes, i); +#ifdef WOLFMQTT_BROKER_RETAINED +subscribe_cleanup: +#endif #ifdef WOLFMQTT_BROKER_PERSIST /* Shadow-write the full subscription list for this client. Only * meaningful for clean_session=0 sessions; the persist layer no-ops - * when no hooks are installed. */ - if (rc > 0 && bc->session_expiry_sec != 0 && + * when no hooks are installed. Keyed on the in-memory list having + * changed, not on the SUBACK write: a deferred or failed SUBACK, or a + * retained delivery that failed after some filters were added, leaves + * those subscriptions in the Session all the same. */ + if (subs_changed && bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } #endif - -#ifdef WOLFMQTT_BROKER_RETAINED -subscribe_cleanup: -#endif #ifdef WOLFMQTT_V5 if (sub.props) { (void)MqttProps_Free(sub.props); @@ -7788,6 +8516,9 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, { int rc; int i; +#ifdef WOLFMQTT_BROKER_PERSIST + int subs_changed = 0; +#endif MqttUnsubscribe unsub; MqttUnsubscribeAck ack; MqttTopic topic_buf[MAX_MQTT_TOPICS]; @@ -7818,16 +8549,19 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, #endif if (f && MqttDecode_Num((byte*)f - MQTT_DATA_LEN_SIZE, &flen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { -#ifdef WOLFMQTT_V5 if (BrokerSubs_Remove(broker, bc, f, flen)) { + #ifdef WOLFMQTT_V5 reasons[i] = MQTT_REASON_SUCCESS; + #endif + #ifdef WOLFMQTT_BROKER_PERSIST + subs_changed = 1; + #endif } + #ifdef WOLFMQTT_V5 else { reasons[i] = MQTT_REASON_NO_SUB_EXIST; } -#else - BrokerSubs_Remove(broker, bc, f, flen); -#endif + #endif } } @@ -7854,8 +8588,9 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, } #ifdef WOLFMQTT_BROKER_PERSIST - /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs). */ - if (rc > 0 && bc->session_expiry_sec != 0 && + /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs) whenever a + * subscription was removed, whatever the UNSUBACK write returned. */ + if (subs_changed && bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } @@ -8039,6 +8774,13 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, * delivered. */ WBLOG_ERR(broker, "broker: PUBLISH topic alloc failed sock=%d", (int)bc->sock); + #if WOLFMQTT_MAX_QOS >= 2 + /* Nothing was delivered, so the retransmission must not be + * treated as a duplicate [MQTT-4.3.3-2]. */ + if (pub.qos == MQTT_QOS_2 && !qos2_duplicate) { + BrokerInboundQos2_Remove(bc, pub.packet_id); + } + #endif rc = MQTT_CODE_ERROR_MEMORY; goto publish_cleanup; } @@ -8180,6 +8922,13 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #endif sub_rc = MqttEncode_Publish(sub->client->tx_buf, BROKER_CLIENT_TX_SZ(sub->client), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length + * declaring bytes the frame does not carry [MQTT 3.1.1 + * section 2.2.3]. */ + if (sub_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(sub->client->tx_buf, sub_rc); + sub_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (sub_rc > 0) { WBLOG_DBG(broker, "broker: PUBLISH fwd sock=%d -> sock=%d " @@ -8758,6 +9507,37 @@ static int BrokerClient_ResumeDirect(MqttBroker* broker, BrokerClient* bc) /* -------------------------------------------------------------------------- */ /* Per-client processing (called from Step) */ /* -------------------------------------------------------------------------- */ +/* MQTT 5.0 section 4.13.1: after a Malformed Packet (malformed != 0) or a + * Protocol Error in a packet other than CONNECT, a v5 Server SHOULD send + * DISCONNECT with the Reason Code before closing the Network Connection. + * BrokerSend_Disconnect sends nothing below v5, so this then only closes. */ +static void BrokerClient_ErrorClose(MqttBroker* broker, BrokerClient* bc, + int malformed) +{ +#ifdef WOLFMQTT_V5 + byte reason = (byte)(malformed ? MQTT_REASON_MALFORMED_PACKET : + MQTT_REASON_PROTOCOL_ERR); + + #ifndef WOLFMQTT_STATIC_MEMORY + if (BrokerSend_Disconnect(bc, reason) == MQTT_CODE_CONTINUE) { + /* Finish writing the DISCONNECT on a later step before closing. */ + bc->close_after_direct = 1; + return; + } + #elif defined(WOLFMQTT_NONBLOCK) + /* Static clients cannot resume a deferred write, and closing part way + * through one would leave a truncated DISCONNECT on the wire, so close + * without it. The DISCONNECT is a SHOULD, not a MUST. */ + (void)reason; + #else + (void)BrokerSend_Disconnect(bc, reason); + #endif +#else + (void)malformed; +#endif + BrokerClient_AbnormalClose(broker, bc); +} + static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) { int rc; @@ -8906,7 +9686,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) "sock=%d [MQTT-2.2.2-2]", type, bc->rx_buf[0], (int)bc->sock); if (bc->connected) { - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 1); } else { BrokerSubs_RemoveClient(broker, bc); @@ -8928,7 +9708,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) WBLOG_ERR(broker, "broker: second CONNECT on sock=%d [MQTT-3.1.0-2]", (int)bc->sock); - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 0); return 0; } switch (type) { @@ -9081,7 +9861,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) * only - Remaining Length MUST be 0. Reject malformed * PINGREQ before sending PINGRESP. */ if (bc->client.packet.remain_len != 0) { - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 1); return 0; } /* A failed PINGRESP write is an I/O error like any other @@ -9154,13 +9934,14 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) bc->session_expiry_sec = se_prop->data_int; } #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); + /* [MQTT-3.14.4-3] Only Reason 0x00 discards the + * Will; any other code publishes it (MQTT 5.0 + * section 3.1.2.5). */ + if (disc.reason_code == MQTT_REASON_SUCCESS) { + BrokerClient_ClearWill(bc); } else { - BrokerClient_ClearWill(bc); + BrokerClient_PublishWill(broker, bc); } #endif } @@ -9174,12 +9955,11 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) "broker: DISCONNECT properties dropped, out of " "memory sock=%d", (int)bc->sock); #ifdef WOLFMQTT_BROKER_WILL - if (disc.reason_code == - MQTT_REASON_DISCONNECT_W_WILL_MSG) { - BrokerClient_PublishWill(broker, bc); + if (disc.reason_code == MQTT_REASON_SUCCESS) { + BrokerClient_ClearWill(bc); } else { - BrokerClient_ClearWill(bc); + BrokerClient_PublishWill(broker, bc); } #endif } @@ -9221,7 +10001,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) WBLOG_ERR(broker, "broker: unhandled packet type %u sock=%d", type, (int)bc->sock); - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 0); return 0; } #ifdef ENABLE_MQTT_WEBSOCKET @@ -9342,10 +10122,23 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) /* -------------------------------------------------------------------------- */ int MqttBroker_Init(MqttBroker* broker, MqttBrokerNet* net) { +#ifdef WOLFMQTT_V5 + int rc; +#endif + if (broker == NULL || net == NULL) { return MQTT_CODE_ERROR_BAD_ARG; } XMEMSET(broker, 0, sizeof(*broker)); +#ifdef WOLFMQTT_V5 + /* Restored queue properties are decoded with the shared property pool, + * also before the first and after the last client holds a reference. */ + rc = MqttProps_Init(); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + broker->props_ref = 1; +#endif XMEMCPY(&broker->net, net, sizeof(MqttBrokerNet)); broker->listen_sock = BROKER_SOCKET_INVALID; broker->port = MQTT_DEFAULT_PORT; @@ -9970,6 +10763,14 @@ int MqttBroker_Free(MqttBroker* broker) sizeof(broker->persist_key_cache)); broker->persist_key_loaded = 0; #endif +#ifdef WOLFMQTT_V5 + if (broker->props_ref) { + broker->props_ref = 0; + if (MqttProps_ShutDown() != MQTT_CODE_SUCCESS) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } + } +#endif return MQTT_CODE_SUCCESS; } @@ -10189,6 +10990,7 @@ int wolfmqtt_broker(int argc, char** argv) rc = wolfmqtt_broker_set_auth_pass(&broker, argv[++i], auth_pass_buf, (word32)sizeof(auth_pass_buf)); if (rc != MQTT_CODE_SUCCESS) { + (void)MqttBroker_Free(&broker); return rc; } } @@ -10239,11 +11041,13 @@ int wolfmqtt_broker(int argc, char** argv) #endif else if (XSTRCMP(argv[i], "-h") == 0) { BrokerUsage(argv[0]); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return 0; } else { BrokerUsage(argv[0]); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } @@ -10266,12 +11070,14 @@ int wolfmqtt_broker(int argc, char** argv) PRINTF("broker: ERROR persist+encrypt build needs -E " "(only \"dev\" is recognized; production deployments " "must install MqttBrokerPersistHooks.derive_key)"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } if (XSTRCMP(encrypt_key_source, "dev") != 0) { PRINTF("broker: ERROR unknown -E source \"%s\" " "(only \"dev\" is recognized)", encrypt_key_source); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } @@ -10282,6 +11088,7 @@ int wolfmqtt_broker(int argc, char** argv) PRINTF("broker: ERROR persist+encrypt build has no built-in key " "source (rebuild with --enable-broker-persist-encrypt-dev-key " "for testing, or install MqttBrokerPersistHooks.derive_key)"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; #endif @@ -10296,6 +11103,7 @@ int wolfmqtt_broker(int argc, char** argv) "subdirectories must be owned by this user, must not be " "group- or other-writable (mode 0700), and no path " "component may be a symlink"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return rc; } diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 54f674b1a..f11f80621 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -211,8 +211,23 @@ static INLINE int wmqb_read_header(const byte* buf, word32 buf_len, return 0; } +#if defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) || \ + !defined(WOLFMQTT_STATIC_MEMORY) +/* Volatile wipe the compiler cannot elide, for plaintext about to be freed. */ +static void wmqb_force_zero(void* mem, word32 len) +{ + volatile byte* p = (volatile byte*)mem; + if (p == NULL) { + return; + } + while (len > 0) { + *p++ = 0; + len--; + } +} +#endif + #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT -static void wmqb_force_zero(void* mem, word32 len); /* Lazy-init key cache. Single-threaded broker - no lock needed. The * application-provided derive_key hook fills 32 bytes on first request. @@ -255,19 +270,6 @@ static int wmqb_get_key(MqttBroker* broker) * rekeying only when persist_key_cache changes would amortize the AES * key schedule. Requires deciding the threading model first - today * the broker is single-threaded so a single shared context is safe. */ -/* Volatile wipe the compiler cannot elide, for plaintext about to be freed. */ -static void wmqb_force_zero(void* mem, word32 len) -{ - volatile byte* p = (volatile byte*)mem; - if (p == NULL) { - return; - } - while (len > 0) { - *p++ = 0; - len--; - } -} - /* AES-GCM additional data: the record header followed by where the record * is stored, encoded unambiguously as namespace(1) || key_len(2, big * endian) || key. A ciphertext moved to another namespace or storage key @@ -347,6 +349,7 @@ static int wmqb_encrypt_blob(MqttBroker* broker, byte ns, if (wc_AesGcmSetKey(&aes, broker->persist_key_cache, (word32)sizeof(broker->persist_key_cache)) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_SYSTEM; @@ -359,11 +362,15 @@ static int wmqb_encrypt_blob(MqttBroker* broker, byte ns, WMQB_GCM_TAG_LEN, aad, aad_len) != 0) { /* aad = header + location */ wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_SYSTEM; } + /* The Aes holds the expanded persistence key. wc_AesFree only clears + * it in newer wolfSSL releases, so wipe it here on every path. */ wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); *ct_out = out; return 0; @@ -416,6 +423,7 @@ static int wmqb_decrypt_blob(MqttBroker* broker, byte ns, if (wc_AesGcmSetKey(&aes, broker->persist_key_cache, (word32)sizeof(broker->persist_key_cache)) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); wmqb_force_zero(out, *plain_out_len); WOLFMQTT_FREE(out); @@ -429,12 +437,14 @@ static int wmqb_decrypt_blob(MqttBroker* broker, byte ns, WMQB_GCM_TAG_LEN, aad, aad_len) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); wmqb_force_zero(out, *plain_out_len); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_MALFORMED_DATA; } wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); *plain_out = out; return 0; @@ -809,9 +819,10 @@ int BrokerPersist_PutSubs(MqttBroker* broker, const char* client_id) { size_t raw = XSTRLEN(client_id); /* word16 downcast - reject lengths that wouldn't fit instead - * of silently truncating. MQTT v3.1.1 caps client_id at 23 - * bytes; v5 caps at 65535 (i.e., fits in word16). Anything - * longer is malformed input from a caller. */ + * of silently truncating. MQTT strings are at most 65535 bytes + * (MQTT 3.1.1 section 1.5.3); the 23-byte Client ID limit in + * [MQTT-3.1.3-5] is a minimum a server must accept, not a cap. + * Anything longer is malformed input from a caller. */ if (raw == 0 || raw > 0xFFFFu) { return MQTT_CODE_ERROR_OUT_OF_BUFFER; } @@ -1021,7 +1032,7 @@ static int wmqb_outq_build_key(const char* client_id, word16 packet_id, * 2 1 retain * 3 1 protocol_level * 4 1 retransmit_dup - * 5 1 _reserved + * 5 1 flags (bit 0: Message Expiry Interval present) * 6 2 packet_id (redundant with key, simplifies decode) * 8 8 enq_time * 16 8 enqueue_seq @@ -1029,7 +1040,9 @@ static int wmqb_outq_build_key(const char* client_id, word16 packet_id, * 28 2 topic_len * 30 N topic * 30+N 4 payload_len - * 34+N M payload */ + * 34+N M payload + * 34+N+M 4 props_len + * 38+N+M P props (v5 PUBLISH Properties, no length prefix) */ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, const struct BrokerOutPub* e) { @@ -1051,6 +1064,7 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, byte key[256 + 3]; word16 key_len; int rc; + int props_len = 0; if (broker == NULL || broker->persist == NULL || client_id == NULL || p_e == NULL) { @@ -1075,8 +1089,14 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, if (rc != 0) { return rc; } +#ifdef WOLFMQTT_V5 + props_len = BrokerOutPub_EncodeProps(p_e, NULL); + if (props_len < 0) { + return props_len; + } +#endif body_len = 1 + 1 + 1 + 1 + 2 + 2 + 8 + 8 + 4 + 2 + topic_len + 4 - + payload_len; + + payload_len + 4 + (word32)props_len; total_len = WMQB_HDR_LEN + body_len; buf = (byte*)WOLFMQTT_MALLOC(total_len); if (buf == NULL) { @@ -1089,7 +1109,7 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, *bp++ = p_e->retain; *bp++ = p_e->protocol_level; *bp++ = p_e->retransmit_dup; - *bp++ = 0; /* _reserved */ + *bp++ = p_e->has_expiry ? 1 : 0; wmqb_w_u16(bp, p_e->packet_id); bp += 2; wmqb_w_u64(bp, (word64)p_e->enq_time); bp += 8; wmqb_w_u64(bp, p_e->enqueue_seq); bp += 8; @@ -1100,9 +1120,18 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, if (payload_len > 0 && p_e->payload != NULL) { XMEMCPY(bp, p_e->payload, payload_len); } + bp += payload_len; + wmqb_w_u32(bp, (word32)props_len); bp += 4; +#ifdef WOLFMQTT_V5 + if (props_len > 0 && BrokerOutPub_EncodeProps(p_e, bp) != props_len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } +#endif - rc = wmqb_kv_put_commit(broker, BROKER_PERSIST_NS_OUTQ, - key, key_len, buf, total_len); + if (rc == 0) { + rc = wmqb_kv_put_commit(broker, BROKER_PERSIST_NS_OUTQ, + key, key_len, buf, total_len); + } #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT wmqb_force_zero(buf, total_len); #endif @@ -1766,6 +1795,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, payload_len = wmqb_r_u32(p); p += 4; if ((word32)(end - p) < payload_len) { + wmqb_force_zero(m->topic, (word32)topic_len + 1); WOLFMQTT_FREE(m->topic); WOLFMQTT_FREE(m); return MQTT_CODE_ERROR_MALFORMED_DATA; @@ -1773,6 +1803,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, if (payload_len > 0) { m->payload = (byte*)WOLFMQTT_MALLOC(payload_len); if (m->payload == NULL) { + wmqb_force_zero(m->topic, (word32)topic_len + 1); WOLFMQTT_FREE(m->topic); WOLFMQTT_FREE(m); return MQTT_CODE_ERROR_MEMORY; @@ -1859,7 +1890,9 @@ static int wmqb_purge_expired_retained_cb(const byte* key, word16 key_len, * every live copy and nothing at runtime can reach them again. Stages a * bounded batch, leaves the iterator, then deletes: a backend is not required * to tolerate mutation under an active iterator. Repeats until a scan finds - * nothing, so one restore drains a whole backlog at fixed memory cost. */ + * nothing or WMQB_RETAINED_PURGE_PASSES scans have run, so a large backlog + * can take more than one restore to drain. A key too long to stage is left + * in place. */ static void wmqb_restore_purge_expired_retained(MqttBroker* broker) { const MqttBrokerPersistHooks* h = broker->persist; @@ -2407,7 +2440,7 @@ static void wmqb_restore_drop_ended(MqttBroker* broker, if (rc != 0) { WMQB_LOG_ERR(broker, "broker: persist drop ended session failed " - "client_id=%s rc=%d", cid, rc); + "client_id=%s rc=%d", BrokerLog_Sanitize(cid), rc); } WOLFMQTT_FREE(cid); } @@ -2443,10 +2476,12 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, byte retain; byte protocol_level; byte retransmit_dup; + byte flags; word16 packet_id; word64 enq_time; word64 enqueue_seq; word32 expiry_sec; + word32 props_len; BrokerOutPub** prev_link; BrokerOutPub* iter; @@ -2472,11 +2507,16 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, retain = *p++; protocol_level = *p++; retransmit_dup = *p++; - p++; /* _reserved */ - if (retransmit_dup > 1) { + flags = *p++; + if (retransmit_dup > 1 || (flags & ~0x01) != 0) { return MQTT_CODE_ERROR_MALFORMED_DATA; } packet_id = wmqb_r_u16(p); p += 2; + /* The key repeats the Packet Identifier; both copies must agree, and a + * queued QoS > 0 message never has identifier 0 [MQTT-2.2.1-3]. */ + if (packet_id == 0 || packet_id != wmqb_r_u16(&key[cid_len + 1])) { + return MQTT_CODE_ERROR_MALFORMED_DATA; + } enq_time = wmqb_r_u64(p); p += 8; enqueue_seq = wmqb_r_u64(p); p += 8; if (enqueue_seq == 0) { @@ -2511,7 +2551,16 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, p += topic_len; payload_len = wmqb_r_u32(p); p += 4; - if ((word32)(end - p) < payload_len) { + if ((word32)(end - p) < payload_len || + (word32)(end - p) - payload_len < 4) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); + WOLFMQTT_FREE(e->topic); + WOLFMQTT_FREE(e); + return MQTT_CODE_ERROR_MALFORMED_DATA; + } + props_len = wmqb_r_u32(p + payload_len); + if ((word32)(end - p) - payload_len - 4 != props_len) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); WOLFMQTT_FREE(e->topic); WOLFMQTT_FREE(e); return MQTT_CODE_ERROR_MALFORMED_DATA; @@ -2519,6 +2568,7 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, if (payload_len > 0) { e->payload = (byte*)WOLFMQTT_MALLOC(payload_len); if (e->payload == NULL) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); WOLFMQTT_FREE(e->topic); WOLFMQTT_FREE(e); return MQTT_CODE_ERROR_MEMORY; @@ -2526,6 +2576,22 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, XMEMCPY(e->payload, p, payload_len); } e->payload_len = payload_len; + e->has_expiry = (byte)(flags & 0x01); + p += payload_len + 4; +#ifdef WOLFMQTT_V5 + rc = BrokerOutPub_DecodeProps(e, p, props_len); + if (rc != 0) { + if (e->payload != NULL) { + wmqb_force_zero(e->payload, payload_len); + WOLFMQTT_FREE(e->payload); + } + wmqb_force_zero(e->topic, (word32)topic_len + 1); + WOLFMQTT_FREE(e->topic); + WOLFMQTT_FREE(e); + return (rc == MQTT_CODE_ERROR_MEMORY) ? rc : + MQTT_CODE_ERROR_MALFORMED_DATA; + } +#endif e->qos = (MqttQoS)qos; e->packet_id = packet_id; e->retain = retain; @@ -2594,7 +2660,7 @@ static void wmqb_restore_expiry_sweep(MqttBroker* broker) WMQB_LOG_INFO(broker, "broker: persist expired session client_id=%s " "(expiry=%us)", - cur->client_id == NULL ? "(null)" : cur->client_id, + BrokerLog_Sanitize(cur->client_id), (unsigned)cur->session_expiry_sec); /* Shared teardown lives in mqtt_broker.c so the eviction * path and this expiry-sweep path can't drift. Drops @@ -2773,11 +2839,12 @@ int BrokerPersist_Restore(MqttBroker* broker) if (rc == MQTT_CODE_ERROR_MALFORMED_DATA || rc == MQTT_CODE_ERROR_BAD_ARG) { /* Schema or magic mismatch. Wipe-and-restart per the chosen - * policy: iterate every namespace, delete every record, - * restamp META. New records get written fresh as activity - * resumes. (BAD_ARG = header magic / schema version mismatch - * from wmqb_read_header; MALFORMED_DATA = decrypt or size - * failure from wmqb_kv_get_decrypt_meta.) */ + * policy: iterate every namespace, delete every record + * (dynamic memory builds only; static builds skip the wipe + * in wmqb_wipe_all), restamp META. New records get written + * fresh as activity resumes. (BAD_ARG = header magic / schema + * version mismatch from wmqb_read_header; MALFORMED_DATA = + * decrypt or size failure from wmqb_kv_get_decrypt_meta.) */ WMQB_LOG_ERR(broker, "broker: persist schema mismatch - wiping all records"); rc = wmqb_wipe_all(broker); diff --git a/src/mqtt_broker_persist_posix.c b/src/mqtt_broker_persist_posix.c index 39c2a3b04..2bff8c663 100644 --- a/src/mqtt_broker_persist_posix.c +++ b/src/mqtt_broker_persist_posix.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -120,7 +121,10 @@ static void wmqb_hex_encode(char* out, const byte* in, word16 in_len) } /* hex decode a NUL-terminated hex string into out. Returns the byte - * length on success, -1 on malformed input. */ + * length on success, -1 on malformed input. Only lowercase is accepted, the + * form wmqb_hex_encode writes: an uppercase name decodes to a key whose + * get/del path is the lowercase name, so iterating it would yield a record + * that can never be deleted. */ static int wmqb_hex_decode(const char* in, byte* out, word16 out_cap) { word16 n; @@ -145,12 +149,10 @@ static int wmqb_hex_decode(const char* in, byte* out, word16 out_cap) char c = in[2 * i]; if (c >= '0' && c <= '9') hi = (byte)(c - '0'); else if (c >= 'a' && c <= 'f') hi = (byte)(10 + c - 'a'); - else if (c >= 'A' && c <= 'F') hi = (byte)(10 + c - 'A'); else return -1; c = in[2 * i + 1]; if (c >= '0' && c <= '9') lo = (byte)(c - '0'); else if (c >= 'a' && c <= 'f') lo = (byte)(10 + c - 'a'); - else if (c >= 'A' && c <= 'F') lo = (byte)(10 + c - 'A'); else return -1; out[i] = (byte)((hi << 4) | lo); } @@ -309,6 +311,16 @@ static int wmqb_open_ns(const WmqbPosixCtx* c, byte ns, int create, return 0; } +/* Longest key this backend can store. A record is named ".bin" and + * written through ".bin.tmp", so 2 * key_len + 8 must fit in one + * file name component (NAME_MAX, 255 on common filesystems: 123 bytes). */ +#ifdef NAME_MAX + #define WMQB_NAME_MAX NAME_MAX +#else + #define WMQB_NAME_MAX 255 +#endif +#define WMQB_MAX_KEY_LEN ((WMQB_NAME_MAX - 8) / 2) + static int wmqb_rec_name(const byte* key, word16 key_len, char* out, size_t out_cap) { @@ -318,7 +330,7 @@ static int wmqb_rec_name(const byte* key, word16 key_len, char* out, if (key == NULL || out == NULL) { return MQTT_CODE_ERROR_BAD_ARG; } - if (key_len > 256) { + if (key_len > 256 || key_len > WMQB_MAX_KEY_LEN) { return MQTT_CODE_ERROR_OUT_OF_BUFFER; } wmqb_hex_encode(hex, key, key_len); @@ -455,6 +467,8 @@ static int wmqb_posix_get(void* ctx, byte ns, const byte* key, ssize_t r; word32 cap; word32 read_total = 0; + byte probe; + int probe_eintr = 0; if (c == NULL || key == NULL || inout_len == NULL) { return MQTT_CODE_ERROR_BAD_ARG; @@ -506,6 +520,18 @@ static int wmqb_posix_get(void* ctx, byte ns, const byte* key, read_total += (word32)r; } } + /* A full buffer need not be the whole record. Probe for one more byte + * so a larger record is reported, not returned truncated. */ + if (read_total == cap) { + do { + r = read(fd, &probe, 1); + } while (r < 0 && errno == EINTR && probe_eintr++ < 16); + if (r != 0) { + (void)close(fd); + return (r > 0) ? MQTT_CODE_ERROR_OUT_OF_BUFFER : + MQTT_CODE_ERROR_SYSTEM; + } + } (void)close(fd); *inout_len = read_total; return 0; diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 60983c190..711884ad3 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -47,7 +47,7 @@ WOLFMQTT_LOCAL void MqttClient_ForceZero(void* mem, word32 len) * flagged, so the other thread knows it completed. * * WOLFMQTT_NONBLOCK: Enabled transport support for returning WANT READ/WRITE, - * which becomes WOLFMQTT_CODE_CONTINUE. This prevents blocking if the + * which becomes MQTT_CODE_CONTINUE. This prevents blocking if the * transport (socket) has no data. * * WOLFMQTT_V5: Enables MQTT v5.0 support @@ -72,11 +72,13 @@ WOLFMQTT_LOCAL void MqttClient_ForceZero(void* mem, word32 len) #define MQTT_CLIENT_INIT_LOCK_RECV 0x02U #define MQTT_CLIENT_INIT_LOCK_CLIENT 0x04U #define MQTT_CLIENT_INIT_LOCK_CURL 0x08U + #define MQTT_CLIENT_INIT_DECODE_DONE 0x10U #endif /* forward declarations */ static int MqttClient_Publish_ReadPayload(MqttClient* client, MqttPublish* publish, int timeout_ms); +static int MqttClient_SendIds_Find(const MqttClient* client, word16 packet_id); #ifdef WOLFMQTT_V5 static int MqttClient_AuthEx(MqttClient *client, MqttAuth* auth, MqttMsgStat* stat, @@ -137,11 +139,16 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); #elif defined(WOLFMQTT_POSIX_SEMAPHORES) /* Posix style semaphore */ int wm_SemInit(wm_Sem *s) { + if (pthread_mutex_init(&s->mutex, NULL) != 0) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } #ifndef WOLFMQTT_NO_COND_SIGNAL s->lockCount = 0; - pthread_cond_init(&s->cond, NULL); + if (pthread_cond_init(&s->cond, NULL) != 0) { + (void)pthread_mutex_destroy(&s->mutex); + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } #endif - pthread_mutex_init(&s->mutex, NULL); return 0; } int wm_SemFree(wm_Sem *s) { @@ -176,7 +183,14 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); /* FreeRTOS binary semaphore */ int wm_SemInit(wm_Sem *s) { *s = xSemaphoreCreateBinary(); - xSemaphoreGive(*s); + if (*s == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + if (xSemaphoreGive(*s) != pdTRUE) { + vSemaphoreDelete(*s); + *s = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } return 0; } int wm_SemFree(wm_Sem *s) { @@ -196,6 +210,9 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); /* Windows semaphore object */ int wm_SemInit(wm_Sem *s) { *s = CreateSemaphoreW( NULL, 1, 1, NULL); + if (*s == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } return 0; } int wm_SemFree(wm_Sem *s) { @@ -397,6 +414,10 @@ WOLFMQTT_LOCAL int MqttReadStart(MqttClient* client, MqttMsgStat* stat) { /* mark read active */ client->read.isActive = 1; + #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + client->read_owner = WOLFMQTT_THREAD_SELF(); + client->read_owner_valid = 1; + #endif /* reset the packet state used by MqttPacket_Read */ client->packet.stat = MQTT_PK_BEGIN; @@ -421,11 +442,16 @@ WOLFMQTT_LOCAL void MqttReadStop(MqttClient* client, MqttMsgStat* stat) #endif #ifdef WOLFMQTT_MULTITHREAD - if (wm_SemLock(&client->lockClient) == 0) + /* Only the lockRecv holder may reset the read state. A repeat stop after + * the release would clobber another reader that has since taken it. */ + if (stat->isReadActive && wm_SemLock(&client->lockClient) == 0) #endif { /* reset read */ XMEMSET(&client->read, 0, sizeof(client->read)); + #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + client->read_owner_valid = 0; + #endif #ifdef WOLFMQTT_MULTITHREAD wm_SemUnlock(&client->lockClient); #endif @@ -448,14 +474,17 @@ WOLFMQTT_LOCAL void MqttReadStop(MqttClient* client, MqttMsgStat* stat) * 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. */ + * (already held, or newly reserved), 0 if the quota is exhausted, or a + * negative error code if lockClient could not be taken, in which case the + * quota was not checked and nothing is reserved. */ 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 */ + int rc = wm_SemLock(&client->lockClient); + if (rc != 0) { + return rc; } #endif if (publish->stat.recvQuotaHeld) { @@ -506,7 +535,7 @@ static void MqttClient_RecvQuotaRelease(MqttClient* client, MqttMsgStat* stat) * digest of it would let two different ClientIds that happen to share one * inherit each other's replay entries and QoS 2 pending ids. Returns 0 when no * Session is recorded, so a first connect never matches. */ -static int MqttClient_SessionIdMatches(const MqttClient* client, +WOLFMQTT_LOCAL int MqttClient_SessionIdMatches(const MqttClient* client, const char* client_id) { size_t len; @@ -525,7 +554,7 @@ static int MqttClient_SessionIdMatches(const MqttClient* client, * too long for the buffer is not recorded, so the next Session Present is * treated as a different Session and the state is dropped rather than * inherited on a partial match. */ -static void MqttClient_SessionIdRecord(MqttClient* client, +WOLFMQTT_LOCAL void MqttClient_SessionIdRecord(MqttClient* client, const char* client_id) { size_t len; @@ -548,8 +577,9 @@ static void MqttClient_SessionIdRecord(MqttClient* client, * QoS 2 PUBLISH to the application and answered with PUBREC records its packet * id until the matching PUBREL arrives, so a retransmitted PUBLISH is * acknowledged again without a second delivery [MQTT-4.3.3-10]. These run on - * the receive path (under lockRecv), so no extra locking is required. */ -static int MqttClient_RecvQos2_Contains(const MqttClient* client, + * the receive path (under lockRecv), so no extra locking is required. The + * MQTT-SN client shares them for its QoS 2 flow (MQTT-SN 1.2 section 6.7). */ +WOLFMQTT_LOCAL int MqttClient_RecvQos2_Contains(const MqttClient* client, word16 packet_id) { int i; @@ -565,7 +595,7 @@ static int MqttClient_RecvQos2_Contains(const MqttClient* client, return 0; } -static int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) +WOLFMQTT_LOCAL int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) { int i; @@ -580,7 +610,8 @@ static int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) return 0; } -static void MqttClient_RecvQos2_Add(MqttClient* client, word16 packet_id) +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Add(MqttClient* client, + word16 packet_id) { int i; @@ -598,7 +629,8 @@ static void MqttClient_RecvQos2_Add(MqttClient* client, word16 packet_id) } } -static void MqttClient_RecvQos2_Remove(MqttClient* client, word16 packet_id) +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Remove(MqttClient* client, + word16 packet_id) { int i; @@ -737,16 +769,27 @@ static void MqttClient_Replay_Add(MqttClient* client, MqttPublish* publish) /* QoS 2 advanced past PUBREC, so [MQTT-4.4.0-1] replays the PUBREL rather * than the PUBLISH. Recorded even when no copy was retained: a PUBREL needs - * only the Packet Identifier. */ + * only the Packet Identifier. Only this client's own QoS 2 PUBLISH counts: a + * PUBREC for an identifier it never published, or for a QoS 1 PUBLISH, is not + * Session state (MQTT 5.0 section 3.6.2.1, Packet Identifier not found) and + * must neither take a slot nor turn a QoS 1 replay into a PUBREL. A QoS 2 + * PUBLISH sent without a slot (e.g. no topic to retain) is recognised by its + * in-flight reservation awaiting PUBCOMP. */ static void MqttClient_Replay_PubRelSent(MqttClient* client, word16 packet_id) { MqttReplayMsg* slot; + int i; if (packet_id == 0) { return; } slot = MqttClient_Replay_Find(client, packet_id); if (slot == NULL) { + i = MqttClient_SendIds_Find(client, packet_id); + if (i < 0 || client->send_inflight[i].ack_type != + (byte)MQTT_PACKET_TYPE_PUBLISH_COMP) { + return; + } slot = MqttClient_Replay_Find(client, 0); if (slot == NULL) { return; @@ -755,6 +798,9 @@ static void MqttClient_Replay_PubRelSent(MqttClient* client, word16 packet_id) slot->packet_id = packet_id; slot->qos = MQTT_QOS_2; } + else if (slot->qos != MQTT_QOS_2) { + return; + } slot->pubrelSent = 1; } @@ -885,8 +931,9 @@ static void MqttClient_Replay_PubRelSentSafe(MqttClient* client, * dropped as soon as the waiting call returns - including on a timeout, where * no acknowledgement was processed at all. This table is the connection-level * record the rule actually asks for, so it is kept in every build. It is - * cleared when a Network Connection starts or ends, because the client holds - * no outbound session state across one. + * cleared when a Network Connection starts or ends. Unacknowledged messages + * kept for session replay have their identifiers reserved again when a + * resumed Session re-sends them. * * These are called from both the send and receive paths, so they take * client->lockClient themselves; no caller may already hold it. Every caller @@ -1067,8 +1114,8 @@ static void MqttClient_SendIdDisown(MqttClient* client, const void* owner) } /* Drop every reservation. The identifiers were only in use on the Network - * Connection that is starting or ending, and the client keeps no outbound - * session state across one. */ + * Connection that is starting or ending. Replayed messages reserve theirs + * again on a Session Present reconnect. */ static void MqttClient_SendIdsReset(MqttClient* client) { #ifdef WOLFMQTT_MULTITHREAD @@ -1200,6 +1247,135 @@ void MqttClient_RespList_Remove(MqttClient *client, MqttPendResp *rmResp) #endif } +/* Returns 1 if the calling thread holds lockRecv, or lockSend when check_write + * is set. Waiting on either would then self-deadlock. Without thread + * identities this cannot be told and returns 0. Caller must hold lockClient. */ +static int MqttClient_OwnsIoLock(MqttClient *client, int check_write) +{ + int owns = 0; + +#ifdef WOLFMQTT_THREAD_ID_T + if (client->read.isActive && client->read_owner_valid && + WOLFMQTT_THREAD_EQUAL(client->read_owner, + WOLFMQTT_THREAD_SELF())) { + owns = 1; + } + if (check_write && client->write.isActive && client->write_owner_valid && + WOLFMQTT_THREAD_EQUAL(client->write_owner, + WOLFMQTT_THREAD_SELF())) { + owns = 1; + } +#else + (void)client; + (void)check_write; +#endif + + return owns; +} + +/* Mark a reader done with resp and wake any thread waiting to unlink it. + * Caller must hold lockClient. */ +void MqttClient_RespList_EndProcessing(MqttClient *client, MqttPendResp *resp) +{ + resp->packetProcessing = 0; +#ifdef WOLFMQTT_SEM_SIGNAL + if (client->decodeWaiters > 0) { + wm_SemUnlock(&client->decodeDone); + } +#else + (void)client; +#endif +} + +/* Unlink an owner's pending response once no reader is decoding into it, so + * the owner may free the object on return. A reader marks the entry under + * lockRecv and clears the mark with MqttClient_RespList_EndProcessing, which + * posts decodeDone. Without WOLFMQTT_SEM_SIGNAL, taking lockRecv waits the + * reader out instead. Returns MQTT_CODE_ERROR_STAT, leaving the entry linked, + * when the caller holds lockRecv or lockSend, since the reader may need either + * to finish. Caller must not hold lockClient. */ +WOLFMQTT_LOCAL int MqttClient_RespList_RemoveIdle(MqttClient *client, + MqttPendResp *rmResp) +{ + int rc; + int busy = 0; +#ifdef WOLFMQTT_SEM_SIGNAL + int waited = 0; +#endif + + do { + rc = wm_SemLock(&client->lockClient); + if (rc == 0) { + busy = rmResp->packetProcessing; + if (busy && MqttClient_OwnsIoLock(client, 1)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + else if (!busy) { + MqttClient_RespList_Remove(client, rmResp); + } + #ifdef WOLFMQTT_SEM_SIGNAL + if (waited) { + client->decodeWaiters--; + waited = 0; + /* decodeDone is binary, so one post wakes one waiter. Pass it + * on when leaving so the others recheck too. */ + if ((!busy || rc != 0) && client->decodeWaiters > 0) { + wm_SemUnlock(&client->decodeDone); + } + } + if (busy && rc == 0) { + client->decodeWaiters++; + waited = 1; + } + #endif + wm_SemUnlock(&client->lockClient); + } + if (rc == 0 && busy) { + #ifdef WOLFMQTT_SEM_SIGNAL + rc = wm_SemLock(&client->decodeDone); + if (rc != 0 && wm_SemLock(&client->lockClient) == 0) { + client->decodeWaiters--; + wm_SemUnlock(&client->lockClient); + } + #else + rc = wm_SemLock(&client->lockRecv); + if (rc == 0) { + wm_SemUnlock(&client->lockRecv); + } + #endif + } + } while (rc == 0 && busy); + + return rc; +} + +/* Find the pending response belonging to msg, by its packet object or its + * embedded pendResp. Caller must hold lockClient. */ +static MqttPendResp* MqttClient_RespList_FindObj(MqttClient *client, + MqttObject* msg) +{ + MqttPendResp *tmpResp; + + for (tmpResp = client->firstPendResp; + tmpResp != NULL; + tmpResp = tmpResp->next) + { + #ifdef WOLFMQTT_DEBUG_CLIENT + PRINTF("\tMsg: %p (obj %p), Type %s (%d), ID %d, InProc %d, Done %d", + tmpResp, tmpResp->packet_obj, + MqttPacket_TypeDesc(tmpResp->packet_type), + tmpResp->packet_type, tmpResp->packet_id, + tmpResp->packetProcessing, tmpResp->packetDone); + #endif + if ((size_t)tmpResp->packet_obj == (size_t)msg || + (size_t)tmpResp - OFFSETOF(MqttMessage, pendResp) == (size_t)msg) { + break; + } + } + + return tmpResp; +} + /* return codes: 0=not found, 1=found */ int MqttClient_RespList_Find(MqttClient *client, MqttPacketType packet_type, word16 packet_id, MqttPendResp **retResp) @@ -1713,6 +1889,27 @@ static int MqttClient_DecodePacket(MqttClient* client, byte* rx_buf, return rc; } +#ifdef WOLFMQTT_V5 +/* Clear mask then set flags in client->flags, reporting a lockClient failure + * instead of skipping the update the way MqttClient_Flags does. */ +static int MqttClient_UpdateFlags(MqttClient *client, word32 mask, + word32 flags) +{ +#ifdef WOLFMQTT_MULTITHREAD + int rc = wm_SemLock(&client->lockClient); + if (rc != 0) { + return rc; + } +#endif + client->flags &= ~mask; + client->flags |= flags; +#ifdef WOLFMQTT_MULTITHREAD + wm_SemUnlock(&client->lockClient); +#endif + return MQTT_CODE_SUCCESS; +} +#endif + static int MqttClient_HandlePacket(MqttClient* client, MqttPacketType packet_type, void *packet_obj, MqttPublishResp* resp, int timeout_ms) @@ -1720,6 +1917,9 @@ static int MqttClient_HandlePacket(MqttClient* client, int rc = MQTT_CODE_SUCCESS; MqttQoS packet_qos = MQTT_QOS_0; word16 packet_id = 0; +#ifdef WOLFMQTT_V5 + int flag_rc; +#endif if (client == NULL || packet_obj == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); @@ -1735,6 +1935,15 @@ static int MqttClient_HandlePacket(MqttClient* client, rc = MqttClient_DecodePacket(client, client->rx_buf, client->packet.buf_len, packet_obj, &packet_type, &packet_qos, &packet_id, 1); + #ifdef WOLFMQTT_V5 + /* The CONNACK ends the [MQTT-3.1.2-30] authentication window. A + * failed clear fails the CONNACK; the next CONNECT resets it. */ + flag_rc = MqttClient_UpdateFlags(client, + MQTT_CLIENT_FLAG_AUTH_PENDING, 0); + if (flag_rc != MQTT_CODE_SUCCESS && rc >= 0) { + rc = flag_rc; + } + #endif break; } case MQTT_PACKET_TYPE_PUBLISH: @@ -2032,7 +2241,10 @@ WOLFMQTT_LOCAL int MqttClient_CheckPendResp(MqttClient *client, byte wait_type, if (MqttClient_RespList_Find(client, (MqttPacketType)wait_type, wait_packet_id, &pendResp)) { - if ((pendResp != NULL) && (pendResp->packetDone)) { + /* A duplicate ack can be decoding into a completed entry, so + * it is only done once no reader is processing it. */ + if ((pendResp != NULL) && (pendResp->packetDone) && + !pendResp->packetProcessing) { /* pending response is already done, so return */ rc = pendResp->packet_ret; #ifdef WOLFMQTT_DEBUG_CLIENT @@ -2233,6 +2445,20 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, /* advance state, since we received some data */ mms_stat->read = MQTT_MSG_HEADER; } + #else + /* A blocking read cannot resume a packet: the partial state + * is discarded below, but its bytes have already left the + * transport, so the next read would parse the rest of this + * packet as a new one. A timeout after part of a packet + * arrived therefore ends the connection rather than reading + * as an idle wait. */ + if (rc == MQTT_CODE_ERROR_TIMEOUT && + (client->packet.stat > MQTT_PK_BEGIN || + client->read.pos > 0)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_NETWORK); + (void)MqttClient_Flags(client, + MQTT_CLIENT_FLAG_IS_CONNECTED, 0); + } #endif break; } @@ -2427,6 +2653,14 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, rc = MQTT_CODE_SUCCESS; } else { + #ifdef WOLFMQTT_MULTITHREAD + /* Release the entry so its owner does not wait on it */ + if (pendResp != NULL && + wm_SemLock(&client->lockClient) == 0) { + MqttClient_RespList_EndProcessing(client, pendResp); + wm_SemUnlock(&client->lockClient); + } + #endif /* error, break */ break; } @@ -2437,6 +2671,7 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, if (wm_SemLock(&client->lockClient) == 0) { pendResp->packetDone = 1; pendResp->packet_ret = rc; + MqttClient_RespList_EndProcessing(client, pendResp); #ifdef WOLFMQTT_V5 /* A write-only QoS>0 publish reserves a Receive Maximum unit * but does not wait for its own ack; release it here as this @@ -2714,8 +2949,7 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, * 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) { + if (recvFatal && MqttClient_IsFatalProtoError(rc)) { (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_IS_CONNECTED, 0); } return rc; @@ -2741,6 +2975,14 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, } #endif + /* The matched packet is finished. A packet that needed no reply leaves + * the wait at its payload state; rewind it so the next wait on this + * object (MqttClient_WaitMessage always uses client->msg) reads a new + * packet instead of handling this one again. */ + if (mms_stat->read == MQTT_MSG_PAYLOAD || + mms_stat->read == MQTT_MSG_PAYLOAD2) { + mms_stat->read = MQTT_MSG_BEGIN; + } return rc; } @@ -2774,8 +3016,8 @@ int MqttClient_Init(MqttClient *client, MqttNet* net, client->rx_buf_len = rx_buf_len; client->cmd_timeout_ms = cmd_timeout_ms; #ifdef WOLFMQTT_V5 - /* Initialize to this build's Maximum QoS. Handle_Props will narrow - * this if the server advertises a lower MQTT_PROP_MAX_QOS. */ + /* Initialize to this build's Maximum QoS. Handle_ConnectAck_Props will + * narrow this if the server advertises a lower MQTT_PROP_MAX_QOS. */ client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS; client->retain_avail = 1; client->protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL; @@ -2817,6 +3059,16 @@ int MqttClient_Init(MqttClient *client, MqttNet* net, } } #endif + #ifdef WOLFMQTT_SEM_SIGNAL + if (rc == 0) { + rc = wm_SemInit(&client->decodeDone); + if (rc == 0) { + client->init_flags |= MQTT_CLIENT_INIT_DECODE_DONE; + /* Start taken, so a waiter blocks until a reader posts. */ + rc = wm_SemLock(&client->decodeDone); + } + } + #endif #endif if (rc == 0) { @@ -2840,6 +3092,13 @@ void MqttClient_DeInit(MqttClient *client) MqttClient_Replay_Reset(client); #endif #ifdef WOLFMQTT_MULTITHREAD + #ifdef WOLFMQTT_SEM_SIGNAL + if ((client->init_flags & MQTT_CLIENT_INIT_DECODE_DONE) != 0U) { + if (wm_SemFree(&client->decodeDone) == MQTT_CODE_SUCCESS) { + client->init_flags &= (byte)~MQTT_CLIENT_INIT_DECODE_DONE; + } + } + #endif #ifdef ENABLE_MQTT_CURL if ((client->init_flags & MQTT_CLIENT_INIT_LOCK_CURL) != 0U) { if (wm_SemFree(&client->lockCURL) == MQTT_CODE_SUCCESS) { @@ -2870,6 +3129,9 @@ void MqttClient_DeInit(MqttClient *client) } } #endif + /* tx_buf and rx_buf belong to the caller, who may already have freed + * them (the bundled examples do), so they are not touched here. + * MqttClient_NetDisconnect scrubs them while they are still in use. */ } } @@ -3147,6 +3409,28 @@ static int MqttClient_CheckConnectSent(MqttClient *client) return MQTT_CODE_SUCCESS; } +/* Gate for PUBLISH, SUBSCRIBE, UNSUBSCRIBE and PINGREQ: CONNECT must have been + * sent, and [MQTT-3.1.2-30] forbids them while a CONNECT with an + * Authentication Method still awaits its CONNACK. DISCONNECT and AUTH stay + * allowed in that window, so they use MqttClient_CheckConnectSent alone. */ +static int MqttClient_CheckSendAllowed(MqttClient *client) +{ + word32 flags = 0; + int rc; + + /* One snapshot of the flags for every condition. */ + rc = MqttClient_ReadFlags(client, &flags); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + if ((flags & MQTT_CLIENT_FLAG_CONNECT_SENT) == 0 || + (flags & MQTT_CLIENT_FLAG_DISCONNECT_SENT) != 0 || + (flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + return MQTT_CODE_SUCCESS; +} + #ifndef WOLFMQTT_NO_TIME /* Apply the auto keep-alive decision once a connect attempt has reached a * final result. Shared by the normal handshake tail and by the @@ -3268,9 +3552,9 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) } /* Past the guard, so this really is a new handshake: start a fresh - * outbound Packet Identifier space. The client keeps no - * unacknowledged outbound PUBLISH/PUBREL across a Network Connection, - * so nothing from a previous one is still in flight [MQTT-2.3.1-3]. + * outbound Packet Identifier space. Identifiers from a previous + * Network Connection are not in flight on this one [MQTT-2.3.1-3]; + * replayed messages reserve theirs again if the Session resumes. * Must run after the guard above - a refused duplicate CONNECT would * otherwise wipe identifiers still in flight on the live connection - * and outside the WOLFMQTT_V5 block below, since the table exists in @@ -3421,6 +3705,24 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) } #endif + #ifdef WOLFMQTT_V5 + /* [MQTT-3.1.2-30] With an Authentication Method, only AUTH or + * DISCONNECT may follow until the CONNACK. Raised before any CONNECT + * byte can reach the peer, so neither another sender nor a reader + * that processes the CONNACK can see the window open late. */ + rc = MqttClient_UpdateFlags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, + client->auth_method_set ? MQTT_CLIENT_FLAG_AUTH_PENDING : 0); + if (rc != MQTT_CODE_SUCCESS) { + /* Without the window raised nothing would hold back other + * packets, so do not send the CONNECT. */ + int xfer = client->write.len; + CLIENT_FORCE_ZERO(client->tx_buf, xfer); + MqttWriteStop(client, &mc_connect->stat); + (void)MqttClient_CancelMessage(client, (MqttObject*)mc_connect); + return rc; + } + #endif + mc_connect->stat.write = MQTT_MSG_HEADER; } if (mc_connect->stat.write == MQTT_MSG_HEADER) { @@ -3478,6 +3780,15 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) /* The handshake state set above stands or not on whether bytes * reached the peer, so a retry is refused only when some of this * CONNECT is already out there. */ + #ifdef WOLFMQTT_V5 + if (wrote == 0) { + /* No CONNECT reached the peer, so no CONNACK is coming. The + * write error is returned either way, and a flag left set by + * a lock failure is reset by the next CONNECT. */ + (void)MqttClient_UpdateFlags(client, + MQTT_CLIENT_FLAG_AUTH_PENDING, 0); + } + #endif MqttClient_CancelMessage(client, (MqttObject*)mc_connect); return rc; } @@ -3731,6 +4042,18 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) return rc; } +/* The message callback refused a PUBLISH. With payload still unread the + * stream can no longer be parsed, so mark the connection unusable as the + * MqttMsgCb contract promises; the application tears the transport down via + * MqttClient_NetDisconnect. */ +static void MqttClient_Publish_AbandonPayload(MqttClient* client, + byte msg_done) +{ + if (!msg_done) { + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_IS_CONNECTED, 0); + } +} + static int MqttClient_Publish_ReadPayload(MqttClient* client, MqttPublish* publish, int timeout_ms) { @@ -3772,6 +4095,7 @@ static int MqttClient_Publish_ReadPayload(MqttClient* client, rc = client->msg_cb(client, publish, publish->buffer_new, msg_done); if (rc != MQTT_CODE_SUCCESS) { + MqttClient_Publish_AbandonPayload(client, msg_done); return rc; }; } @@ -3829,6 +4153,7 @@ static int MqttClient_Publish_ReadPayload(MqttClient* client, rc = client->msg_cb(client, publish, publish->buffer_new, msg_done); if (rc != MQTT_CODE_SUCCESS) { + MqttClient_Publish_AbandonPayload(client, msg_done); return rc; }; } @@ -3938,7 +4263,9 @@ static int MqttClient_Publish_WritePayload(MqttClient *client, } while (publish->buffer_pos < publish->total_len); } else if (publish->buffer_pos < publish->total_len) { - if (publish->buffer_pos > 0) { + /* buffer_pos is 0 when the header filled tx_buf; build the first + * payload chunk then too. */ + if (publish->buffer != NULL) { client->write.len = (publish->total_len - publish->buffer_pos); if (client->write.len > client->tx_buf_len) { client->write.len = client->tx_buf_len; @@ -4049,6 +4376,9 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, { int rc = MQTT_CODE_SUCCESS; MqttPacketType resp_type; +#ifdef WOLFMQTT_MULTITHREAD + int rmRc; +#endif /* Validate required arguments */ if (client == NULL || publish == NULL) { @@ -4056,7 +4386,7 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4117,14 +4447,20 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, * non-blocking one keeps its pending response for the reader, so * both reserve. */ if (client->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && - publish->qos > MQTT_QOS_0 && + publish->qos > MQTT_QOS_0 #if defined(WOLFMQTT_MULTITHREAD) && !defined(WOLFMQTT_NONBLOCK) - !writeOnly && + && !writeOnly #endif - !MqttClient_RecvQuotaReserve(client, publish)) + ) { - MqttWriteStop(client, &publish->stat); - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + rc = MqttClient_RecvQuotaReserve(client, publish); + if (rc <= 0) { + MqttWriteStop(client, &publish->stat); + if (rc == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + } + return rc; + } } #endif @@ -4427,9 +4763,10 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, * WOLFMQTT_NONBLOCK returned CONTINUE above and broke out before * here, leaving its entry for the reading thread to complete and * remove. */ - if (wm_SemLock(&client->lockClient) == 0) { - MqttClient_RespList_Remove(client, &publish->pendResp); - wm_SemUnlock(&client->lockClient); + rmRc = MqttClient_RespList_RemoveIdle(client, + &publish->pendResp); + if (rmRc != MQTT_CODE_SUCCESS && rc >= 0) { + rc = rmRc; } #endif } @@ -4454,7 +4791,10 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, #endif ) { + /* Reset the response wait state too (see MqttClient_Subscribe) */ publish->stat.write = MQTT_MSG_BEGIN; + publish->resp.stat.read = MQTT_MSG_BEGIN; + publish->resp.stat.ack = MQTT_MSG_BEGIN; } if (rc > 0) { rc = MQTT_CODE_SUCCESS; @@ -4494,7 +4834,7 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4614,6 +4954,8 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) * requested topic; too few would be read as granted QoS 0 (fail-open). */ if (subscribe->ack.return_code_count != subscribe->topic_count) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + (void)MqttClient_Flags(client, + MQTT_CLIENT_FLAG_IS_CONNECTED, 0); } else { for (i = 0; i < subscribe->topic_count && i < MAX_MQTT_TOPICS; i++) { @@ -4629,8 +4971,12 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) } } - /* reset state */ + /* reset state. The ack wait state is reset too, as in + * MqttClient_Connect: left at MQTT_MSG_PAYLOAD, a reuse of this object + * would skip the read and dispatch stale rx_buf contents. */ subscribe->stat.write = MQTT_MSG_BEGIN; + subscribe->ack.stat.read = MQTT_MSG_BEGIN; + subscribe->ack.stat.ack = MQTT_MSG_BEGIN; return rc; } @@ -4648,7 +4994,7 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4791,17 +5137,36 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) } #endif - /* reset state */ + /* reset state, including the ack wait state (see MqttClient_Subscribe) */ unsubscribe->stat.write = MQTT_MSG_BEGIN; + unsubscribe->ack.stat.read = MQTT_MSG_BEGIN; + unsubscribe->ack.stat.ack = MQTT_MSG_BEGIN; return rc; } -static int MqttClient_PingTimeoutDisconnect(MqttClient *client) +/* Also used when a keep-alive ping times out, see MqttClient_Ping_ex. */ +int MqttClient_NetDisconnect_Wait(MqttClient *client) { #ifdef WOLFMQTT_MULTITHREAD int rc; + if (client == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } + /* From a callback this thread may already hold an I/O lock, and taking it + * again would self-deadlock. */ + rc = wm_SemLock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + if (MqttClient_OwnsIoLock(client, 1)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + wm_SemUnlock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } /* A message callback can hold lockRecv while starting a publish, so take * the I/O locks in that same direction. Once both are held, no read or * write callback can still reference socket, TLS, or curl state while the @@ -4835,7 +5200,7 @@ int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4924,7 +5289,7 @@ int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping) * direct Ping callers while the automatic keep-alive path maps it to a * network error. */ if (rc == MQTT_CODE_ERROR_TIMEOUT) { - (void)MqttClient_PingTimeoutDisconnect(client); + (void)MqttClient_NetDisconnect_Wait(client); } return rc; @@ -5285,14 +5650,25 @@ static int MqttClient_KeepAlive(MqttClient *client, MqttObject* msg) int rc = MQTT_CODE_SUCCESS; int mid_transfer; word32 now, elapsed, threshold; + word32 flags = 0; if (client == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } /* Disabled at runtime by the application (e.g. it schedules its own ping), - * independent of the keep-alive negotiated with the broker. */ - if ((client->flags & MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE) != 0) { + * independent of the keep-alive negotiated with the broker. Read under + * lockClient, since another thread may change the flags. */ + rc = MqttClient_ReadFlags(client, &flags); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + if ((flags & MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE) != 0) { + return MQTT_CODE_SUCCESS; + } + /* No PINGREQ while an authentication exchange awaits its CONNACK + * [MQTT-3.1.2-30]. */ + if ((flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { return MQTT_CODE_SUCCESS; } @@ -5439,6 +5815,27 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) PRINTF("Cancel Msg: %p", msg); #endif +#ifdef WOLFMQTT_MULTITHREAD + /* Refuse, before changing any state, when a reader is decoding into the + * message and this thread holds a lock that reader may need, e.g. + * cancelling it from its own ack's property callback. This message's own + * write is released below before waiting, so it does not count. */ + rc = wm_SemLock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + tmpResp = MqttClient_RespList_FindObj(client, msg); + if (tmpResp != NULL && tmpResp->packetProcessing && + (MqttClient_OwnsIoLock(client, 0) || + (!mms_stat->isWriteActive && MqttClient_OwnsIoLock(client, 1)))) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + wm_SemUnlock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } +#endif + /* Whether this message's packet finished going out. MQTT_MSG_WAIT is only * reached once the whole Control Packet has been written. */ onWire = (mms_stat->write == MQTT_MSG_WAIT) ? 1 : 0; @@ -5481,6 +5878,19 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) mms_stat->recvQuotaHeld = 0; #endif + /* Release this message's write before waiting for a reader below, which + * may need lockSend to answer the packet it is decoding. */ + if (mms_stat->isWriteActive) { + #ifdef WOLFMQTT_DEBUG_CLIENT + PRINTF("Cancel Write Lock"); + #endif + /* An abandoned write (e.g. a partial nonblocking CONNECT) leaves the + * encoded packet - possibly plaintext credentials - in tx_buf. Scrub + * it before MqttWriteStop releases lockSend. */ + CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); + MqttWriteStop(client, mms_stat); + } + #ifdef WOLFMQTT_MULTITHREAD /* Remove any pending responses expected */ rc = wm_SemLock(&client->lockClient); @@ -5488,37 +5898,38 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) return rc; } - for (tmpResp = client->firstPendResp; - tmpResp != NULL; - tmpResp = tmpResp->next) - { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("\tMsg: %p (obj %p), Type %s (%d), ID %d, InProc %d, Done %d", + /* Do not credit any reserved Receive Maximum unit here: the PUBLISH may + * already be on the wire, where the server keeps counting it [MQTT-4.9], + * so crediting it on a local cancel could exceed the negotiated quota. The + * unit is released on the acknowledgement, or recovered when the + * connection resets server_recv_max. */ + tmpResp = MqttClient_RespList_FindObj(client, msg); +#ifdef WOLFMQTT_DEBUG_CLIENT + if (tmpResp != NULL) { + PRINTF("Found Cancel Msg: %p (obj %p), Type %s (%d), ID %d, " + "InProc %d, Done %d", tmpResp, tmpResp->packet_obj, MqttPacket_TypeDesc(tmpResp->packet_type), tmpResp->packet_type, tmpResp->packet_id, tmpResp->packetProcessing, tmpResp->packetDone); - #endif - if ((size_t)tmpResp->packet_obj == (size_t)msg || - (size_t)tmpResp - OFFSETOF(MqttMessage, pendResp) == (size_t)msg) { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("Found Cancel Msg: %p (obj %p), Type %s (%d), ID %d, " - "InProc %d, Done %d", - tmpResp, tmpResp->packet_obj, - MqttPacket_TypeDesc(tmpResp->packet_type), - tmpResp->packet_type, tmpResp->packet_id, - tmpResp->packetProcessing, tmpResp->packetDone); - #endif - /* Do not credit any reserved Receive Maximum unit here: the PUBLISH - * may already be on the wire, where the server keeps counting it - * [MQTT-4.9], so crediting it on a local cancel could exceed the - * negotiated quota. The unit is released on the acknowledgement, or - * recovered when the connection resets server_recv_max. */ - MqttClient_RespList_Remove(client, tmpResp); - break; - } } +#endif wm_SemUnlock(&client->lockClient); + + if (tmpResp != NULL) { + if (mms_stat->isReadActive) { + /* This thread holds lockRecv, so no other reader can be using + * the entry and waiting for one would self-deadlock. */ + rc = wm_SemLock(&client->lockClient); + if (rc == MQTT_CODE_SUCCESS) { + MqttClient_RespList_Remove(client, tmpResp); + wm_SemUnlock(&client->lockClient); + } + } + else { + rc = MqttClient_RespList_RemoveIdle(client, tmpResp); + } + } #endif /* WOLFMQTT_MULTITHREAD */ /* cancel any active flags / locks */ @@ -5532,16 +5943,6 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); MqttReadStop(client, mms_stat); } - if (mms_stat->isWriteActive) { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("Cancel Write Lock"); - #endif - /* An abandoned write (e.g. a partial nonblocking CONNECT) leaves the - * encoded packet - possibly plaintext credentials - in tx_buf. Scrub - * it before MqttWriteStop releases lockSend. */ - CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); - MqttWriteStop(client, mms_stat); - } return rc; } @@ -5644,8 +6045,9 @@ int MqttClient_NetDisconnect(MqttClient *client) } #endif - /* The Network Connection is going away and the client keeps no outbound - * session state across one, so nothing stays in flight [MQTT-2.3.1-3]. */ + /* The Network Connection is going away, so none of its identifiers stay + * in flight [MQTT-2.3.1-3]. Replayed messages reserve theirs again if a + * later Session resumes. */ MqttClient_SendIdsReset(client); rc = MqttSocket_Disconnect(client); diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 63dcd052b..d79e49598 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -33,22 +33,29 @@ struct MqttPropMatrix { MqttDataType data; word16 packet_type_mask; /* allowed packets */ }; +/* Will Properties have their own allow-list [MQTT-3.1.3.2]. Packet type 0 is + * reserved and never sent, so its bit marks the Will scope. */ +#define MQTT_PROP_SCOPE_WILL MQTT_PACKET_TYPE_RESERVED static const struct MqttPropMatrix gPropMatrix[] = { { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_PAYLOAD_FORMAT_IND, MQTT_DATA_TYPE_BYTE, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_MSG_EXPIRY_INTERVAL, MQTT_DATA_TYPE_INT, - (1 << MQTT_PACKET_TYPE_CONNECT) | + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_CONTENT_TYPE, MQTT_DATA_TYPE_STRING, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_RESP_TOPIC, MQTT_DATA_TYPE_STRING, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_CORRELATION_DATA, MQTT_DATA_TYPE_BINARY, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_SUBSCRIPTION_ID, MQTT_DATA_TYPE_VAR_INT, @@ -79,7 +86,7 @@ static const struct MqttPropMatrix gPropMatrix[] = { { MQTT_PROP_REQ_PROB_INFO, MQTT_DATA_TYPE_BYTE, (1 << MQTT_PACKET_TYPE_CONNECT) }, { MQTT_PROP_WILL_DELAY_INTERVAL, MQTT_DATA_TYPE_INT, - (1 << MQTT_PACKET_TYPE_CONNECT) }, + (1 << MQTT_PROP_SCOPE_WILL) }, { MQTT_PROP_REQ_RESP_INFO, MQTT_DATA_TYPE_BYTE, (1 << MQTT_PACKET_TYPE_CONNECT) }, { MQTT_PROP_RESP_INFO, MQTT_DATA_TYPE_STRING, @@ -926,8 +933,11 @@ int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf) } case MQTT_DATA_TYPE_SHORT: { - /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. */ - if (cur_prop->type == MQTT_PROP_TOPIC_ALIAS && + /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. + * MQTT 5.0 sections 3.1.2.11.3 and 3.2.2.3.3: so is a + * Receive Maximum of 0. */ + if ((cur_prop->type == MQTT_PROP_TOPIC_ALIAS || + cur_prop->type == MQTT_PROP_RECEIVE_MAX) && cur_prop->data_short == 0) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); } @@ -1087,7 +1097,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); break; } - /* Allocate a structure and add to head. */ + /* Allocate a structure and append it, keeping wire order. */ cur_prop = MqttProps_Add(props); if (cur_prop == NULL) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); @@ -1108,6 +1118,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, } cur_prop->type = (MqttPropertyType)prop_type; tmp = rc; + /* The Identifier must fit in the remaining Property Length */ + if ((word32)tmp > prop_len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } buf += tmp; total += tmp; prop_len -= tmp; @@ -1563,7 +1578,8 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect) } /* Determine length of properties */ - lwt_props_len = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, + lwt_props_len = MqttEncode_Props( + (MqttPacketType)MQTT_PROP_SCOPE_WILL, mc_connect->lwt_msg->props, NULL); if (lwt_props_len >= 0) { int tmp_len = MqttEncode_Vbi(NULL, lwt_props_len); @@ -1682,7 +1698,7 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect) /* Encode the lwt property length */ tx_payload += tmp_len; - tmp_len = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, + tmp_len = MqttEncode_Props((MqttPacketType)MQTT_PROP_SCOPE_WILL, mc_connect->lwt_msg->props, tx_payload); if (tmp_len < 0) { return tmp_len; @@ -1843,11 +1859,9 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) * Corner case: a peer claiming level 6 but sending a v5-shape wire * (extra properties-length VBI present) will misparse on the v3.1.1 * path and the strict tail-consumption check below returns - * MALFORMED_DATA, which the broker translates to a silent socket - * close - CONNACK 0x01 is emitted only when the v3.1.1-shape decode - * succeeds. This is a best-effort spec compliance trade-off; clients - * that misrepresent their protocol level should not expect the broker - * to reverse-engineer the wire shape. */ + * MALFORMED_DATA. protocol_level is already recorded at that point, so + * the broker still answers any unsupported level with CONNACK 0x01 + * [MQTT-3.1.2-2], whether or not the body decodes. */ if (mc_connect->protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_5) { /* Decode Length of Properties */ if (rx_buf_len < (rx_payload - rx_buf)) { @@ -1919,10 +1933,15 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) rx_payload += lwt_tmp; if (lwt_props_len > 0) { /* Decode, then enforce the tighter Will allow-list below. */ - lwt_tmp = MqttDecode_Props(MQTT_PACKET_TYPE_CONNECT, + lwt_tmp = MqttDecode_Props( + (MqttPacketType)MQTT_PROP_SCOPE_WILL, &mc_connect->lwt_msg->props, rx_payload, (word32)(rx_buf_len - (rx_payload - rx_buf)), lwt_props_len); + /* Report a non-Will property as the encoder does */ + if (lwt_tmp == MQTT_CODE_ERROR_PROPERTY_MISMATCH) { + lwt_tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } if (lwt_tmp < 0) { rc = lwt_tmp; goto cleanup; @@ -2066,6 +2085,49 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) } #endif /* WOLFMQTT_BROKER */ +/* Validate against MQTT v3.1.1 section 3.2.2.3 or the MQTT 5.0 CONNACK + * Reason Code table in section 3.2.2.2. Used by the decoder and, in broker + * builds, the encoder. */ +static int MqttPacket_ConnectAckReturnCodeValid(byte code, + byte protocol_level) +{ +#ifdef WOLFMQTT_V5 + if (protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + switch (code) { + case MQTT_REASON_SUCCESS: + case MQTT_REASON_UNSPECIFIED_ERR: + case MQTT_REASON_MALFORMED_PACKET: + case MQTT_REASON_PROTOCOL_ERR: + case MQTT_REASON_IMPL_SPECIFIC_ERR: + case MQTT_REASON_UNSUP_PROTO_VER: + case MQTT_REASON_CLIENT_ID_NOT_VALID: + case MQTT_REASON_BAD_USER_OR_PASS: + case MQTT_REASON_NOT_AUTHORIZED: + case MQTT_REASON_SERVER_UNAVAILABLE: + case MQTT_REASON_SERVER_BUSY: + case MQTT_REASON_BANNED: + case MQTT_REASON_BAD_AUTH_METHOD: + case MQTT_REASON_TOPIC_NAME_INVALID: + case MQTT_REASON_PACKET_TOO_LARGE: + case MQTT_REASON_QUOTA_EXCEEDED: + case MQTT_REASON_PAYLOAD_FORMAT_INVALID: + case MQTT_REASON_RETAIN_NOT_SUPPORTED: + case MQTT_REASON_QOS_NOT_SUPPORTED: + case MQTT_REASON_USE_ANOTHER_SERVER: + case MQTT_REASON_SERVER_MOVED: + case MQTT_REASON_CON_RATE_EXCEED: + return 1; + default: + return 0; + } + } +#else + (void)protocol_level; +#endif + + return code <= MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH; +} + int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, MqttConnectAck *connect_ack) { @@ -2117,6 +2179,18 @@ int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, connect_ack->flags = *rx_payload++; connect_ack->return_code = *rx_payload++; + /* A code outside the table for this protocol version is a protocol + * violation, not an ordinary refusal. */ + #ifdef WOLFMQTT_V5 + if (!MqttPacket_ConnectAckReturnCodeValid(connect_ack->return_code, + connect_ack->protocol_level)) { + #else + if (!MqttPacket_ConnectAckReturnCodeValid(connect_ack->return_code, + MQTT_CONNECT_PROTOCOL_LEVEL_4)) { + #endif + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + /* [MQTT-3.2.2-1] Bits 7-1 of the Connect Acknowledge Flags byte are * reserved and MUST be 0. Any other value is a protocol violation; * [MQTT-4.8.0-1] requires the receiver to close the connection. */ @@ -2180,48 +2254,6 @@ int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, } #ifdef WOLFMQTT_BROKER -/* Validate against MQTT v3.1.1 section 3.2.2.3 or the MQTT 5.0 CONNACK - * Reason Code table in section 3.2.2.2. */ -static int MqttPacket_ConnectAckReturnCodeValid(byte code, - byte protocol_level) -{ -#ifdef WOLFMQTT_V5 - if (protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - switch (code) { - case MQTT_REASON_SUCCESS: - case MQTT_REASON_UNSPECIFIED_ERR: - case MQTT_REASON_MALFORMED_PACKET: - case MQTT_REASON_PROTOCOL_ERR: - case MQTT_REASON_IMPL_SPECIFIC_ERR: - case MQTT_REASON_UNSUP_PROTO_VER: - case MQTT_REASON_CLIENT_ID_NOT_VALID: - case MQTT_REASON_BAD_USER_OR_PASS: - case MQTT_REASON_NOT_AUTHORIZED: - case MQTT_REASON_SERVER_UNAVAILABLE: - case MQTT_REASON_SERVER_BUSY: - case MQTT_REASON_BANNED: - case MQTT_REASON_BAD_AUTH_METHOD: - case MQTT_REASON_TOPIC_NAME_INVALID: - case MQTT_REASON_PACKET_TOO_LARGE: - case MQTT_REASON_QUOTA_EXCEEDED: - case MQTT_REASON_PAYLOAD_FORMAT_INVALID: - case MQTT_REASON_RETAIN_NOT_SUPPORTED: - case MQTT_REASON_QOS_NOT_SUPPORTED: - case MQTT_REASON_USE_ANOTHER_SERVER: - case MQTT_REASON_SERVER_MOVED: - case MQTT_REASON_CON_RATE_EXCEED: - return 1; - default: - return 0; - } - } -#else - (void)protocol_level; -#endif - - return code <= MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH; -} - int MqttEncode_ConnectAck(byte *tx_buf, int tx_buf_len, MqttConnectAck *connect_ack) { @@ -3208,8 +3240,9 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) /* Preserve v5 No Local / Retain As Published / Retain Handling * (bits 2-5) so the full options byte round-trips. Reserved and * QoS bits were validated above; for a valid v3.1.1 packet these - * bits are 0. The broker stores these but does not yet enforce - * them on delivery. */ + * bits are 0. The broker applies Retain Handling when it sends + * retained messages for a new subscription; it does not store + * No Local or Retain As Published. */ topic->sub_options = (byte)(options & MQTT_SUBSCRIBE_OPTIONS_MASK); #endif subscribe->topic_count++; @@ -3262,6 +3295,14 @@ int MqttDecode_SubscribeAck(byte* rx_buf, int rx_buf_len, if (remain_len < MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } +#ifdef WOLFMQTT_V5 + /* MQTT 5.0 section 3.9.2: a v5 SUBACK always carries a Property Length + * after the Packet Identifier, even when it is zero. */ + if (subscribe_ack->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + remain_len <= MQTT_DATA_LEN_SIZE) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } +#endif rx_payload = &rx_buf[header_len]; @@ -3650,10 +3691,12 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, /* MQTT 3.1.1 section 3.11.1: the variable header is exactly the two-byte * Packet Identifier and there is no payload, so Remaining Length is fixed - * at 2. v5 section 3.11 appends Properties and Reason Codes. */ + * at 2. v5 section 3.11.2 appends a Property Length, which is present + * even when zero, so a v5 packet holding only the Packet Identifier is + * malformed. */ #ifdef WOLFMQTT_V5 if (unsubscribe_ack->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - if (remain_len < MQTT_DATA_LEN_SIZE) { + if (remain_len <= MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } } @@ -4118,9 +4161,13 @@ int MqttEncode_Auth(byte *tx_buf, int tx_buf_len, MqttAuth *auth) } /* [MQTT-4.12] AUTH (REAUTH/CONT_AUTH) must carry an Authentication Method - * property; reject if the list is empty or lacks it. */ + * property; reject if the list is empty or lacks it. A Success AUTH may + * omit Properties entirely (MQTT 5.0 section 3.15.2.1), but once it has + * any, the Authentication Method is required too (section 3.15.2.2.2). */ if ((auth->reason_code == MQTT_REASON_CONT_AUTH) || - (auth->reason_code == MQTT_REASON_REAUTH)) { + (auth->reason_code == MQTT_REASON_REAUTH) || + ((auth->reason_code == MQTT_REASON_SUCCESS) && + (auth->props != NULL))) { auth_prop = auth->props; while (auth_prop != NULL && auth_prop->type != MQTT_PROP_AUTH_METHOD) { auth_prop = auth_prop->next; @@ -4212,6 +4259,7 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) int header_len, remain_len, tmp; byte *rx_payload; word32 props_len = 0; + MqttProp* auth_prop; /* Validate required arguments */ @@ -4276,6 +4324,18 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) Remaining Length of 0 and never reaches this branch. */ return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } + /* MQTT 5.0 section 3.15.2.2.2: omitting the Authentication + * Method is a Protocol Error. */ + auth_prop = auth->props; + while (auth_prop != NULL && + auth_prop->type != MQTT_PROP_AUTH_METHOD) { + auth_prop = auth_prop->next; + } + if (auth_prop == NULL) { + (void)MqttProps_Free(auth->props); + auth->props = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } } else { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); @@ -4297,8 +4357,8 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) return header_len + remain_len; } -/* Must be called once from a single thread before any concurrent access - * to MQTTv5 property functions. Not thread-safe if called concurrently. */ +/* Reference counted: each call must be paired with one MqttProps_ShutDown + * call. Calls must not run concurrently with each other. */ int MqttProps_Init(void) { int ret = MQTT_CODE_SUCCESS; @@ -4313,9 +4373,9 @@ int MqttProps_Init(void) return ret; } -/* Must be called once from a single thread after all concurrent access - * to MQTTv5 property functions has ceased. Not thread-safe if called - * concurrently. */ +/* Releases one MqttProps_Init reference and frees the shared property lock + * when the last one is released. Calls must not run concurrently with each + * other. */ int MqttProps_ShutDown(void) { int ret = MQTT_CODE_SUCCESS; @@ -4428,6 +4488,19 @@ int MqttProps_Free(MqttProp *head) return ret; } +/* Find the first property of a specific type. Returns NULL if none */ +MqttProp* MqttProps_FindType(MqttProp *head, MqttPropertyType type) +{ + while (head != NULL) { + if (head->type == type) { + break; + } + head = head->next; + } + + return head; +} + #endif /* WOLFMQTT_V5 */ int MqttPacket_HandleNetError(MqttClient *client, int rc) @@ -4467,9 +4540,10 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) rc = MqttPacket_HandleNetError(client, rc); #ifndef WOLFMQTT_NO_TIME - /* Record when a full control packet leaves the wire so the keep-alive - * scheduler can measure how long the outbound link has been idle. Only - * needed while auto-ping is armed; skip the clock read otherwise. */ + /* Record each fully written buffer (a whole packet, or one segment of a + * streamed PUBLISH payload) so the keep-alive scheduler can measure how + * long the outbound link has been idle. Only needed while auto-ping is + * armed; skip the clock read otherwise. */ if (rc == tx_buf_len && client->keep_alive_sec != 0) { client->last_tx_time = WOLFMQTT_GET_TIME_S(); } @@ -4481,7 +4555,8 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, int timeout_ms) { - int rc, len, remain_read = 0; + int rc, len, remain_read = 0, header_len = 0; + word32 read_len; MqttPacket* header; if (client == NULL || rx_buf == NULL) { @@ -4583,21 +4658,27 @@ int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, case MQTT_PK_READ: { - /* read remainder of packet */ + /* read remainder of packet; use locals the socket read cannot + * modify */ remain_read = client->packet.remain_len; + header_len = client->packet.header_len; client->packet.stat = MQTT_PK_READ; /* Make sure it does not overflow rx_buf */ - if (rx_buf_len < client->packet.header_len) { + if (header_len < 0 || header_len > rx_buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } - if (remain_read > rx_buf_len - client->packet.header_len) { - remain_read = rx_buf_len - client->packet.header_len; + if (remain_read < 0) { + return MqttPacket_HandleNetError(client, + MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA)); + } + if (remain_read > rx_buf_len - header_len) { + remain_read = rx_buf_len - header_len; } /* Read remaining */ if (client->packet.remain_len > 0) { - rc = MqttSocket_Read(client, &rx_buf[client->packet.header_len], + rc = MqttSocket_Read(client, &rx_buf[header_len], remain_read, timeout_ms); if (rc <= 0) { return MqttPacket_HandleNetError(client, rc); @@ -4616,14 +4697,13 @@ int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, /* reset state */ client->packet.stat = MQTT_PK_BEGIN; - /* Return read length. header_len and remain_read are each bounded by - * rx_buf_len above, so their sum cannot exceed it; clamp to keep the - * returned length provably in range for static analysis. */ - rc = client->packet.header_len + remain_read; - if (rc > rx_buf_len) { - rc = rx_buf_len; + /* Return read length. Both terms are non-negative ints, so the unsigned + * sum cannot wrap; the clamp is defensive. */ + read_len = (word32)header_len + (word32)remain_read; + if (read_len > (word32)rx_buf_len) { + read_len = (word32)rx_buf_len; } - return rc; + return (int)read_len; } #ifndef WOLFMQTT_NO_ERROR_STRINGS diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 4158e4a64..5cb8465e2 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -262,6 +262,24 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, * the callback may safely call a client send API. MQTT_MSG_WAIT * records successful delivery across a busy-writer retry. */ if (wait_stat->ack == MQTT_MSG_BEGIN) { + #if WOLFMQTT_MAX_QOS >= 2 + /* MQTT-SN 1.2 section 6.7: a QoS 2 PUBLISH is delivered once. + * A retransmit of one still awaiting its PUBREL is answered + * with PUBREC again but not delivered a second time. */ + if (p_pub->qos == MQTT_QOS_2 && + MqttClient_RecvQos2_Contains(client, + p_pub->packet_id)) { + wait_stat->ack = MQTT_MSG_WAIT; + } + else if (p_pub->qos == MQTT_QOS_2 && + !MqttClient_RecvQos2_HasFreeSlot(client)) { + /* A new id that cannot be tracked must not be delivered, + * or its retransmit would be delivered again. MQTT-SN + * cannot refuse it in-band, so fail without PUBREC. */ + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PACKET_ID); + } + else + #endif if (client->msg_cb) { /* if using the temp publish message buffer, then populate message context with client context */ @@ -271,6 +289,12 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, if (rc != MQTT_CODE_SUCCESS) { return rc; } + #if WOLFMQTT_MAX_QOS >= 2 + if (p_pub->qos == MQTT_QOS_2) { + /* Delivered; remember the id until its PUBREL. */ + MqttClient_RecvQos2_Add(client, p_pub->packet_id); + } + #endif wait_stat->ack = MQTT_MSG_WAIT; } else { @@ -354,6 +378,14 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, } packet_id = p_publish_resp->packet_id; + #if WOLFMQTT_MAX_QOS >= 2 + /* PUBREL completes the inbound QoS 2 flow: drop the id so a later + * PUBLISH may reuse it. */ + if (packet_type == SN_MSG_TYPE_PUBREL) { + MqttClient_RecvQos2_Remove(client, packet_id); + } + #endif + /* If Qos then send response */ if (packet_type == SN_MSG_TYPE_PUBREC || packet_type == SN_MSG_TYPE_PUBREL) { @@ -814,6 +846,15 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, mms_stat, &response_pending, timeout_ms); if (rc == MQTT_CODE_CONTINUE) { + #ifdef WOLFMQTT_MULTITHREAD + /* A resumed wait finds the entry again, so release it now + * and let its owner unlink it meanwhile. */ + if (pendResp != NULL && + wm_SemLock(&client->lockClient) == 0) { + MqttClient_RespList_EndProcessing(client, pendResp); + wm_SemUnlock(&client->lockClient); + } + #endif break; } @@ -828,6 +869,7 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, if (wm_SemLock(&client->lockClient) == 0) { pendResp->packetDone = 1; pendResp->packet_ret = rc; + MqttClient_RespList_EndProcessing(client, pendResp); #ifdef WOLFMQTT_DEBUG_CLIENT PRINTF("PendResp Marked Done %p", pendResp); #endif @@ -882,12 +924,11 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, client->sn_wait_packet_type = SN_MSG_TYPE_RESERVED; client->sn_wait_packet_id = 0; MqttReadStop(client, mms_stat); - if (rc == MQTT_CODE_CONTINUE) { - /* CONTINUE from an application callback is a terminal callback result, - * not an internal automatic-response retry. Consume the packet so a - * later wait cannot replay the callback. */ - mms_stat->read = MQTT_MSG_BEGIN; - } + /* The packet is finished: handled, failed, or (for CONTINUE from an + * application callback, a terminal callback result rather than an + * automatic-response retry) consumed. Rewind so the next wait on this + * object reads a new packet instead of handling this one again. */ + mms_stat->read = MQTT_MSG_BEGIN; #ifdef WOLFMQTT_NONBLOCK #ifdef WOLFMQTT_DEBUG_CLIENT @@ -1320,7 +1361,9 @@ static int SN_WillMessage(MqttClient *client, SN_Will *will) will->stat.write = MQTT_MSG_PAYLOAD; } else { - CLIENT_FORCE_ZERO(client->tx_buf, client->write.len); + /* write.len was not set for this packet; wipe the whole + * buffer as the other encode-failure paths do. */ + CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); MqttWriteStop(client, &will->stat); will->stat.write = MQTT_MSG_BEGIN; break; @@ -1395,9 +1438,34 @@ enum { SN_WILL_DONE_TOPIC = 2 /* will topic sent, message still pending */ }; +/* The Will exchange after CONNECT failed. Release the CONNACK pending + * response and rewind the state so a retry with the same SN_Connect sends a + * new CONNECT; the gateway only requests the Will in reply to one. A reader + * may already be decoding an early CONNACK into mc_connect, so the entry is + * unlinked only once it is idle, and the state is left alone if that fails. */ +static int SN_Client_ConnectReset(MqttClient *client, SN_Connect *mc_connect) +{ + int rc = MQTT_CODE_SUCCESS; + +#ifdef WOLFMQTT_MULTITHREAD + rc = MqttClient_RespList_RemoveIdle(client, &mc_connect->pendResp); +#else + (void)client; +#endif + if (rc == MQTT_CODE_SUCCESS) { + mc_connect->stat.write = MQTT_MSG_BEGIN; + mc_connect->will_done = SN_WILL_DONE_NONE; + } + return rc; +} + int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) { int rc = 0; + int reset_rc; +#if WOLFMQTT_MAX_QOS >= 2 + int session_id_matched; +#endif /* Validate required arguments */ if ((client == NULL) || (mc_connect == NULL)) { @@ -1469,6 +1537,12 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) if (mc_connect->will_done == SN_WILL_DONE_NONE) { rc = SN_WillTopic(client, &mc_connect->will); if (rc != 0) { + if (rc != MQTT_CODE_CONTINUE) { + reset_rc = SN_Client_ConnectReset(client, mc_connect); + if (reset_rc != MQTT_CODE_SUCCESS) { + rc = reset_rc; + } + } return rc; } mc_connect->will_done = SN_WILL_DONE_TOPIC; @@ -1476,6 +1550,12 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) rc = SN_WillMessage(client, &mc_connect->will); if (rc != 0) { + if (rc != MQTT_CODE_CONTINUE) { + reset_rc = SN_Client_ConnectReset(client, mc_connect); + if (reset_rc != MQTT_CODE_SUCCESS) { + rc = reset_rc; + } + } return rc; } mc_connect->will_done = SN_WILL_DONE_ALL; @@ -1506,6 +1586,19 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) mc_connect->ack.return_code != SN_RC_ACCEPTED) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_CONNECT_REFUSED); } +#if WOLFMQTT_MAX_QOS >= 2 + /* A clean session, or a session for a different ClientId, starts with + * no inbound QoS 2 flows in progress. */ + if (rc == MQTT_CODE_SUCCESS) { + session_id_matched = + MqttClient_SessionIdMatches(client, mc_connect->client_id); + MqttClient_SessionIdRecord(client, mc_connect->client_id); + if (mc_connect->clean_session || !session_id_matched) { + XMEMSET(client->recv_qos2_pending, 0, + sizeof(client->recv_qos2_pending)); + } + } +#endif return rc; } @@ -1527,9 +1620,10 @@ int SN_Client_WillTopicUpdate(MqttClient *client, SN_Will *will) return rc; } - /* Encode Will Topic Update */ + /* Encode Will Topic Update. With no topic, send the empty form that + * deletes the stored Will (MQTT-SN 1.2 section 5.4.19). */ rc = SN_Encode_WillTopicUpdate(client->tx_buf, - client->tx_buf_len, will); + client->tx_buf_len, (will->willTopic != NULL) ? will : NULL); #ifdef WOLFMQTT_DEBUG_CLIENT PRINTF("MqttClient_EncodePacket: Len %d, Type %s (%d)", rc, SN_Packet_TypeDesc(SN_MSG_TYPE_WILLTOPICUPD), diff --git a/src/mqtt_sn_packet.c b/src/mqtt_sn_packet.c index baba295d7..3bc205eb5 100644 --- a/src/mqtt_sn_packet.c +++ b/src/mqtt_sn_packet.c @@ -1082,6 +1082,10 @@ int SN_Encode_Subscribe(byte *tx_buf, int tx_buf_len, SN_Subscribe *subscribe) subscribe->topicNameId == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (subscribe->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } /* Determine packet length */ if (subscribe->topic_type == SN_TOPIC_ID_TYPE_NORMAL) { @@ -1206,6 +1210,10 @@ int SN_Encode_Publish(byte *tx_buf, int tx_buf_len, SN_Publish *publish) if (tx_buf == NULL || publish == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (publish->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } if (tx_buf_len < 0) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } @@ -1264,7 +1272,8 @@ int SN_Encode_Publish(byte *tx_buf, int tx_buf_len, SN_Publish *publish) } if ((publish->topic_type == SN_TOPIC_ID_TYPE_SHORT) || (publish->topic_type == SN_TOPIC_ID_TYPE_PREDEF)) { - /* Short and predefined topic names are 2 chars */ + /* A short topic name is 2 chars; a predefined topic ID is a 2-byte + * identifier, not a name */ XMEMCPY(tx_payload, publish->topic_name, 2); tx_payload += 2; } @@ -1496,6 +1505,10 @@ int SN_Encode_Unsubscribe(byte *tx_buf, int tx_buf_len, unsubscribe->topicNameId == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (unsubscribe->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } /* Determine packet length */ if (unsubscribe->topic_type == SN_TOPIC_ID_TYPE_NORMAL) { diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index e98f80ba1..cb6fd699a 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -192,6 +192,22 @@ int MqttSocket_Init(MqttClient *client, MqttNet *net) return rc; } +/* Saturate rather than overflow: the broker never resets its read total. */ +static int MqttSocket_AddTotal(int total, int len) +{ + word32 sum = 0; + + if (total > 0) { + sum = (word32)total; + } + /* both terms are non-negative ints, so this cannot wrap */ + sum += (word32)len; + if (sum > (word32)0x7FFFFFFF) { + sum = (word32)0x7FFFFFFF; + } + return (int)sum; +} + static int MqttSocket_WriteDo(MqttClient *client, const byte* buf, int buf_len, int timeout_ms) { @@ -253,6 +269,7 @@ int MqttSocket_Write(MqttClient *client, const byte* buf, int buf_len, int timeout_ms) { int rc; + int remain; /* Validate arguments */ if (client == NULL || client->net == NULL || client->net->write == NULL || @@ -261,47 +278,55 @@ int MqttSocket_Write(MqttClient *client, const byte* buf, int buf_len, } /* check for buffer position overflow */ - if (client->write.pos >= buf_len) { + if (client->write.pos < 0 || client->write.pos >= buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + /* Track bytes left in a local the net callback cannot reach */ + remain = buf_len - client->write.pos; #ifdef WOLFMQTT_NONBLOCK - rc = MqttSocket_WriteDo(client, &buf[client->write.pos], - buf_len - client->write.pos, timeout_ms); + rc = MqttSocket_WriteDo(client, &buf[buf_len - remain], remain, timeout_ms); if (rc >= 0) { /* Clamp return value: write callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->write.pos) { - rc = buf_len - client->write.pos; + if (rc > remain) { + rc = remain; } - client->write.pos += rc; - client->write.total += rc; - if (client->write.pos < buf_len) { + remain -= rc; + client->write.pos = buf_len - remain; + client->write.total = MqttSocket_AddTotal(client->write.total, rc); + if (remain > 0) { rc = MQTT_CODE_CONTINUE; } } #else do { - rc = MqttSocket_WriteDo(client, &buf[client->write.pos], - buf_len - client->write.pos, timeout_ms); + rc = MqttSocket_WriteDo(client, &buf[buf_len - remain], remain, + timeout_ms); if (rc <= 0) { break; } /* Clamp return value: write callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->write.pos) { - rc = buf_len - client->write.pos; - } - client->write.pos += rc; - client->write.total += rc; - } while (client->write.pos < buf_len); + if (rc > remain) { + rc = remain; + } + remain -= rc; + client->write.pos = buf_len - remain; + client->write.total = MqttSocket_AddTotal(client->write.total, rc); + } while (remain > 0); + /* A blocking write that makes no progress cannot finish the packet; + * returning 0 would read as success to callers. */ + if (rc == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_NETWORK); + } #endif /* WOLFMQTT_NONBLOCK */ /* handle return code */ if (rc > 0) { /* return length write and reset position */ - rc = client->write.pos; + rc = buf_len - remain; client->write.pos = 0; } @@ -371,6 +396,7 @@ static int MqttSocket_ReadDo(MqttClient *client, byte* buf, int buf_len, int MqttSocket_Read(MqttClient *client, byte* buf, int buf_len, int timeout_ms) { int rc; + int remain; /* Validate arguments */ if (client == NULL || client->net == NULL || client->net->read == NULL || @@ -379,53 +405,51 @@ int MqttSocket_Read(MqttClient *client, byte* buf, int buf_len, int timeout_ms) } /* check for buffer position overflow */ - if (client->read.pos >= buf_len) { + if (client->read.pos < 0 || client->read.pos >= buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + /* Track bytes left in a local the net callback cannot reach */ + remain = buf_len - client->read.pos; #ifdef WOLFMQTT_NONBLOCK - rc = MqttSocket_ReadDo(client, &buf[client->read.pos], - buf_len - client->read.pos, timeout_ms); + rc = MqttSocket_ReadDo(client, &buf[buf_len - remain], remain, timeout_ms); if (rc >= 0) { /* Clamp return value: read callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->read.pos) { - rc = buf_len - client->read.pos; + if (rc > remain) { + rc = remain; } - client->read.pos += rc; - client->read.total += rc; - if (client->read.pos < buf_len) { + remain -= rc; + client->read.pos = buf_len - remain; + client->read.total = MqttSocket_AddTotal(client->read.total, rc); + if (remain > 0) { rc = MQTT_CODE_CONTINUE; } } #else do { - rc = MqttSocket_ReadDo(client, &buf[client->read.pos], - buf_len - client->read.pos, timeout_ms); + rc = MqttSocket_ReadDo(client, &buf[buf_len - remain], remain, + timeout_ms); if (rc <= 0) { break; } /* Clamp return value: read callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->read.pos) { - rc = buf_len - client->read.pos; + if (rc > remain) { + rc = remain; } - client->read.pos += rc; - client->read.total += rc; - } while (client->read.pos < buf_len); + remain -= rc; + client->read.pos = buf_len - remain; + client->read.total = MqttSocket_AddTotal(client->read.total, rc); + } while (remain > 0); #endif /* WOLFMQTT_NONBLOCK */ /* handle return code */ if (rc > 0) { /* return length read and reset position */ - rc = client->read.pos; + rc = buf_len - remain; client->read.pos = 0; - /* Never report more than the caller requested; guards against a - * read callback that returns more bytes than buf_len */ - if (rc > buf_len) { - rc = buf_len; - } } return rc; @@ -497,7 +521,8 @@ int MqttSocket_Connect(MqttClient *client, const char* host, word16 port, * MqttClient_NetDisconnect. */ MqttClient_Flags(client, (MQTT_CLIENT_FLAG_CONNECT_SENT | - MQTT_CLIENT_FLAG_DISCONNECT_SENT), + MQTT_CLIENT_FLAG_DISCONNECT_SENT | + MQTT_CLIENT_FLAG_AUTH_PENDING), MQTT_CLIENT_FLAG_IS_CONNECTED); } @@ -712,7 +737,8 @@ int MqttSocket_Disconnect(MqttClient *client) * the next one must start with its own CONNECT [MQTT-3.1.0-1]. */ MqttClient_Flags(client, (MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT | - MQTT_CLIENT_FLAG_DISCONNECT_SENT), 0); + MQTT_CLIENT_FLAG_DISCONNECT_SENT | + MQTT_CLIENT_FLAG_AUTH_PENDING), 0); #ifdef ENABLE_MQTT_CURL if (client->curl_initialized) { diff --git a/tests/include.am b/tests/include.am index 9d79bef16..f2cf26a93 100644 --- a/tests/include.am +++ b/tests/include.am @@ -41,6 +41,20 @@ tests_test_mqtt_props_init_SOURCES = \ tests_test_mqtt_props_init_CPPFLAGS = -I$(top_srcdir) \ -include $(top_srcdir)/tests/test_mqtt_props_config.h $(AM_CPPFLAGS) +# Native semaphore initialization tests. Build mqtt_client.c directly with +# the POSIX mutex and condition-variable primitives renamed so their +# initialization failures are deterministic. +check_PROGRAMS += tests/test_mqtt_sem_init +tests_test_mqtt_sem_init_SOURCES = \ + tests/test_mqtt_sem_init.c \ + src/mqtt_client.c \ + src/mqtt_packet.c \ + src/mqtt_socket.c \ + src/mqtt_sn_client.c \ + src/mqtt_sn_packet.c +tests_test_mqtt_sem_init_CPPFLAGS = -I$(top_srcdir) \ + -include $(top_srcdir)/tests/test_mqtt_sem_config.h $(AM_CPPFLAGS) + # libcurl global lifecycle tests. Compile mqtt_socket.c directly so the test # can inject deterministic curl_global_init/cleanup results. if HAVE_LIBCURL @@ -54,7 +68,8 @@ endif # Test framework headers noinst_HEADERS += tests/unit_test.h tests/test_broker_alloc.h \ - tests/test_mqtt_props_config.h tests/test_mqtt_curl_config.h + tests/test_mqtt_props_config.h tests/test_mqtt_curl_config.h \ + tests/test_mqtt_sem_config.h tests/test_broker_log_config.h # MQTT-SN packet decoder tests. The SN_Decode_* routines are WOLFMQTT_LOCAL # (hidden) so they cannot be linked from libwolfmqtt; src/mqtt_sn_packet.c is @@ -155,6 +170,23 @@ tests_test_broker_connect_CPPFLAGS = -I$(top_srcdir) \ tests_test_broker_connect_LDADD = src/libwolfmqtt.la $(LIB_STATIC_ADD) tests_test_broker_connect_DEPENDENCIES = src/libwolfmqtt.la +# Broker log sanitization tests: logging enabled, with PRINTF redirected into +# a capture buffer in the test. +check_PROGRAMS += tests/test_broker_log +tests_test_broker_log_SOURCES = \ + tests/test_broker_log.c \ + src/mqtt_broker.c \ + src/mqtt_broker_persist.c \ + src/mqtt_broker_persist_posix.c +tests_test_broker_log_CFLAGS = -DWOLFMQTT_BROKER -DWOLFMQTT_BROKER_CUSTOM_NET \ + -DNO_MAIN_DRIVER '-DPRINTF(...)=wolfmqtt_test_log(__VA_ARGS__)' \ + '-DWOLFMQTT_BROKER_GET_TIME_S()=wolfmqtt_test_broker_time_s()' \ + '-DBROKER_SLEEP_MS(ms)=do{}while(0)' $(AM_CFLAGS) +tests_test_broker_log_CPPFLAGS = -I$(top_srcdir) \ + -include $(top_srcdir)/tests/test_broker_log_config.h $(AM_CPPFLAGS) +tests_test_broker_log_LDADD = src/libwolfmqtt.la $(LIB_STATIC_ADD) +tests_test_broker_log_DEPENDENCIES = src/libwolfmqtt.la + check_PROGRAMS += tests/test_broker_connect_static tests_test_broker_connect_static_SOURCES = \ tests/test_broker_connect.c \ @@ -164,6 +196,7 @@ tests_test_broker_connect_static_SOURCES = \ tests_test_broker_connect_static_CFLAGS = -DWOLFMQTT_BROKER \ -DWOLFMQTT_BROKER_CUSTOM_NET -DWOLFMQTT_BROKER_NO_LOG \ -DWOLFMQTT_STATIC_MEMORY -DNO_MAIN_DRIVER \ + -DBROKER_MAX_WILL_PAYLOAD_LEN=320 \ '-DWOLFMQTT_BROKER_GET_TIME_S()=wolfmqtt_test_broker_time_s()' \ '-DBROKER_SLEEP_MS(ms)=do{}while(0)' $(AM_CFLAGS) tests_test_broker_connect_static_CPPFLAGS = -I$(top_srcdir) \ diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 5844231b9..384d99bce 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -45,7 +45,9 @@ #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT #include #endif + #include #include + #include #include #include #include @@ -69,6 +71,7 @@ static void* g_capture_alloc_ptr; static byte g_capture_freed[64]; static size_t g_capture_freed_len; static int g_capture_free_seen; +static size_t g_alloc_fail_size; /* fail the next malloc of exactly this size */ void* wolfmqtt_test_broker_malloc(size_t size) { @@ -79,6 +82,11 @@ void* wolfmqtt_test_broker_malloc(size_t size) g_alloc_failure_count++; return NULL; } + if (g_alloc_fail_size != 0 && size == g_alloc_fail_size) { + g_alloc_fail_size = 0; + g_alloc_failure_count++; + return NULL; + } if (g_alloc_fail_after > 0) { g_alloc_fail_after--; } @@ -144,7 +152,8 @@ static void broker_test_disable_alloc_failure(void) } #endif -#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +#if defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) || \ + (defined(WOLFMQTT_BROKER_PERSIST) && !defined(WOLFMQTT_STATIC_MEMORY)) static void broker_test_capture_free(size_t allocation_size) { g_capture_alloc_size = allocation_size; @@ -165,17 +174,21 @@ static void broker_test_disable_free_capture(void) #define MOCK_LISTEN_SOCK 100 #define MOCK_CLIENT_SOCK_BASE 101 /* sock = base + index */ #define MOCK_BUF_SZ 1024 +/* Room for a PUBLISH that fills the broker receive buffer */ +#define MOCK_IN_BUF_SZ (BROKER_RX_BUF_SZ + 64) #define MOCK_MAX_CLIENTS 4 /* Per-mock-client state. Multi-client tests (e.g. QoS 2 dedup) need a * subscriber and a publisher running through the same broker instance. */ typedef struct MockClient { - byte in_buf[MOCK_BUF_SZ]; + byte in_buf[MOCK_IN_BUF_SZ]; size_t in_len; size_t in_pos; byte out_buf[MOCK_BUF_SZ]; size_t out_len; int closed; + int close_count; /* net->close calls on this socket */ + int publish_writes; /* PUBLISH frames the broker tried to write */ int read_err; /* when set, mock_read returns a network error (peer RST) */ int write_err; /* when set, mock_write returns a network error */ int write_continue; /* when set, mock_write reports would-block */ @@ -189,6 +202,8 @@ typedef struct MockClient { /* When set, the next PUBLISH frame (and only that) fails. Lets a test aim * at a delivery without also failing the SUBACK that precedes it. */ int write_err_on_publish; + /* positive: short-write the next PUBLISH frame to this many bytes */ + int write_limit_on_publish; /* Next mock_write call reports a transient timeout: the socket is alive, * select() just did not report it writable in time. */ int write_timeout_pending; @@ -296,6 +311,10 @@ static int mock_write(void* ctx, BROKER_SOCKET_T sock, return MQTT_CODE_ERROR_NETWORK; } mc = &g_clients[idx]; + if (buf != NULL && buf_len > 0 && + ((buf[0] & 0xF0) >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + mc->publish_writes++; + } if (mc->write_err_pending) { mc->write_err_pending = 0; return MQTT_CODE_ERROR_NETWORK; @@ -312,6 +331,13 @@ static int mock_write(void* ctx, BROKER_SOCKET_T sock, if (mc->write_continue) { return MQTT_CODE_CONTINUE; } + if (mc->write_limit_on_publish > 0 && buf != NULL && buf_len > 0 && + ((buf[0] & 0xF0) >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (write_len > mc->write_limit_on_publish) { + write_len = mc->write_limit_on_publish; + } + mc->write_limit_on_publish = 0; + } if (mc->write_limit_then_err > 0) { if (write_len > mc->write_limit_then_err) { write_len = mc->write_limit_then_err; @@ -353,6 +379,7 @@ static int mock_close(void* ctx, BROKER_SOCKET_T sock) (void)ctx; if (idx >= 0) { g_clients[idx].closed = 1; + g_clients[idx].close_count++; } else if (sock == MOCK_LISTEN_SOCK) { g_listen_close_count++; @@ -968,6 +995,248 @@ TEST(connect_v5_oversize_will_payload_emits_connack) } #ifdef WOLFMQTT_STATIC_MEMORY +/* A static Session cannot store Will Properties larger than + * BROKER_MAX_WILL_PROPS_LEN. Dropping some would publish the Will altered, so + * the CONNECT is refused instead. Wire: v5 CONNECT with Will Flag and Clean + * Start, ClientId "P", Will Properties holding one User Property (0x26) + * "k" = BROKER_MAX_WILL_PROPS_LEN bytes of 'v', Will Topic "lwt", Will + * Payload "bye". */ +TEST(connect_v5_oversize_will_props_emits_connack) +{ + MqttBroker broker; + MqttBrokerNet net; + byte connect[MOCK_BUF_SZ]; + size_t pos = 0; + word32 value_len = BROKER_MAX_WILL_PROPS_LEN; + word32 props_len = 1 + 3 + 2 + value_len; + word32 remain; + word32 v; + + remain = 11 + 3 + ((props_len > 127) ? 2 : 1) + props_len + 5 + 5; + ASSERT_TRUE(remain + 3 <= sizeof(connect)); + connect[pos++] = 0x10; + for (v = remain; ; ) { + connect[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + connect[pos++] |= 0x80; + } + XMEMCPY(&connect[pos], "\x00\x04MQTT\x05\x06\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&connect[pos], "\x00\x01P", 3); + pos += 3; + for (v = props_len; ; ) { + connect[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + connect[pos++] |= 0x80; + } + connect[pos++] = MQTT_PROP_USER_PROP; + XMEMCPY(&connect[pos], "\x00\x01k", 3); + pos += 3; + connect[pos++] = (byte)(value_len >> 8); + connect[pos++] = (byte)value_len; + XMEMSET(&connect[pos], 'v', value_len); + pos += value_len; + XMEMCPY(&connect[pos], "\x00\x03lwt\x00\x03" "bye", 10); + pos += 10; + + 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, pos); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 5); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#if BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5 < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 +/* Builds a v5 CONNECT, MQTT 5.0 section 3.1: Clean Start, Will Flag, Will QoS + * 1, Keep Alive 60, ClientId "P", Will Properties holding a Message Expiry + * Interval (0x02) of 60 when with_expiry is set (otherwise an empty Will + * Properties length of 0), Will Topic "lwt" and payload_len bytes of 'x'. */ +static size_t build_v5_connect_will_expiry(byte* buf, size_t buf_sz, + word32 payload_len, int with_expiry) +{ + size_t pos = 0; + word32 remain = 11 + 3 + 1 + (with_expiry ? 5 : 0) + 5 + 2 + payload_len; + word32 v; + + if (remain + 4 > buf_sz) { + return 0; + } + buf[pos++] = 0x10; + for (v = remain; ; ) { + buf[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + buf[pos++] |= 0x80; + } + XMEMCPY(&buf[pos], "\x00\x04MQTT\x05\x0E\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&buf[pos], "\x00\x01P", 3); + pos += 3; + if (with_expiry) { + XMEMCPY(&buf[pos], "\x05\x02\x00\x00\x00\x3C", 6); + pos += 6; + } + else { + buf[pos++] = 0x00; + } + XMEMCPY(&buf[pos], "\x00\x03lwt", 5); + pos += 5; + buf[pos++] = (byte)(payload_len >> 8); + buf[pos++] = (byte)payload_len; + XMEMSET(&buf[pos], 'x', payload_len); + pos += payload_len; + return pos; +} + +/* Builds a v3.1.1 CONNECT, MQTT 3.1.1 section 3.1: Clean Session, Will Flag, + * Will QoS will_qos, Keep Alive 60, ClientId "P", Will Topic "lwt" and + * payload_len bytes of 'x'. */ +static size_t build_v311_connect_will_qos(byte* buf, size_t buf_sz, + byte will_qos, word32 payload_len) +{ + size_t pos = 0; + word32 remain = 10 + 3 + 5 + 2 + payload_len; + word32 v; + + if (remain + 4 > buf_sz) { + return 0; + } + buf[pos++] = 0x10; + for (v = remain; ; ) { + buf[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + buf[pos++] |= 0x80; + } + XMEMCPY(&buf[pos], "\x00\x04MQTT\x04", 7); + pos += 7; + buf[pos++] = (byte)(0x06 | (will_qos << 3)); + buf[pos++] = 0x00; + buf[pos++] = 0x3C; + XMEMCPY(&buf[pos], "\x00\x01P", 3); + pos += 3; + XMEMCPY(&buf[pos], "\x00\x03lwt", 5); + pos += 5; + buf[pos++] = (byte)(payload_len >> 8); + buf[pos++] = (byte)payload_len; + XMEMSET(&buf[pos], 'x', payload_len); + pos += payload_len; + return pos; +} + +/* Runs one CONNECT and returns the CONNACK return or Reason Code, or -1. */ +static int connect_connack_code(const byte* connect, size_t connect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + int reason = -1; + + if (connect_len == 0) { + return -1; + } + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS) { + return -1; + } + if (MqttBroker_Start(&broker) == MQTT_CODE_SUCCESS) { + reset_mock_state(connect, connect_len); + run_broker_one_connect(&broker); + if (g_out_len >= 4 && g_out_buf[0] == 0x20) { + reason = g_out_buf[3]; + } + MqttBroker_Stop(&broker); + } + MqttBroker_Free(&broker); + return reason; +} + +/* Runs one v5 Will CONNECT and returns the CONNACK Reason Code, or -1. */ +static int will_expiry_connack(word32 payload_len, int with_expiry) +{ + byte connect[MOCK_BUF_SZ]; + + return connect_connack_code(connect, + build_v5_connect_will_expiry(connect, sizeof(connect), payload_len, + with_expiry)); +} + +/* Runs one v3.1.1 Will CONNECT and returns the CONNACK return code, or -1. */ +static int v311_will_connack(byte will_qos, word32 payload_len) +{ + byte connect[MOCK_BUF_SZ]; + + return connect_connack_code(connect, + build_v311_connect_will_qos(connect, sizeof(connect), will_qos, + payload_len)); +} + +/* A static QoS 1/2 Will may have to be queued for an offline Session, where + * its payload and properties share one BROKER_MAX_STATIC_OFFLINE_DATA_LEN + * slot. One that cannot fit is refused at CONNECT rather than accepted and + * then dropped when it is published. */ +TEST(connect_v5_static_will_exceeding_offline_slot_refused) +{ + /* Payload plus the 5-byte Message Expiry Interval fills the slot. */ + ASSERT_EQ(MQTT_REASON_SUCCESS, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5, 1)); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 4, 1)); + ASSERT_TRUE(g_client_closed); +} + +#if BROKER_MAX_STATIC_OFFLINE_DATA_LEN < BROKER_MAX_WILL_PAYLOAD_LEN +/* A Will with no publishable properties must also fit the offline slot. */ +TEST(connect_v5_static_payload_only_will_exceeding_offline_slot_refused) +{ + ASSERT_EQ(MQTT_REASON_SUCCESS, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN, 0)); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1, 0)); + ASSERT_TRUE(g_client_closed); +} + +/* The same limit applies to a v3.1.1 QoS 1 Will, refused with return code + * 0x03 Server unavailable (MQTT 3.1.1 section 3.2.2.3). A QoS 0 Will is never + * queued offline, so it only has to fit BROKER_MAX_WILL_PAYLOAD_LEN. */ +TEST(connect_v311_static_will_exceeding_offline_slot_refused) +{ + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, + v311_will_connack(1, BROKER_MAX_STATIC_OFFLINE_DATA_LEN)); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, + v311_will_connack(1, BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1)); + ASSERT_TRUE(g_client_closed); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, + v311_will_connack(0, BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1)); +} +#endif +#endif + TEST(connect_v5_oversize_will_topic_emits_connack) { MqttBroker broker; @@ -1120,6 +1389,71 @@ TEST(connect_unsupported_level_127_refused) run_unsupported_level(0x7F); } +/* The body of a CONNECT is laid out by its Protocol Level, so a level this + * build does not speak may not decode at all. [MQTT-3.1.2-2] still requires + * CONNACK 0x01 rather than a silent close, which is also what lets an MQTT 5 + * client fall back to 3.1.1 against a broker built without v5. */ +static void run_undecodable_unsupported_level(const byte* connect, + size_t connect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + + 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, connect_len); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 4); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(0x02, g_out_buf[1]); + ASSERT_EQ(0x00, g_out_buf[2]); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_PROTO, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +TEST(connect_unsupported_level_undecodable_refused) +{ + /* Level 6, ClientId length 5 with one byte present: malformed whether + * the body is read with or without a v5 Property Length. */ + static const byte connect[] = { + 0x10, 13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x06, /* unsupported protocol level */ + 0x02, /* CleanSession=1 */ + 0x00, 0x3C, + 0x00, 0x05, 'A' + }; + + run_undecodable_unsupported_level(connect, sizeof(connect)); +} + +#ifndef WOLFMQTT_V5 +TEST(connect_v5_to_non_v5_broker_refused) +{ + /* A well-formed MQTT 5 CONNECT (MQTT 5.0 section 3.1.2) with a Session + * Expiry Interval property. Read as 3.1.1, the Property Length 0x05 and + * the property identifier 0x11 become a 0x0511-byte ClientId length. */ + static const byte connect[] = { + 0x10, 19, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, /* MQTT 5.0 */ + 0x02, /* CleanStart=1 */ + 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, /* Session Expiry = 60 */ + 0x00, 0x01, 'A' + }; + + run_undecodable_unsupported_level(connect, sizeof(connect)); +} +#endif + #ifdef WOLFMQTT_BROKER_AUTH /* Regression: [MQTT-3.1.3.5] Password is Binary Data and may legally * contain 0x00. The broker must not use XSTRLEN-based length recovery on @@ -1386,6 +1720,34 @@ TEST(broker_start_partial_listener_failure_cleans_up) broker.tls_ctx = NULL; MqttBroker_Free(&broker); } + +/* A TLS accept with no tls_ctx fails BrokerClient_Add after the socket was + * recorded in the client. The caller closes the rejected socket, so the + * failed add must not close it too; a second close could hit a reused fd. */ +TEST(broker_add_failure_closes_socket_once) +{ + MqttBroker broker; + MqttBrokerNet net; + + install_mock_net(&net); + reset_mock_clients(1); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + ASSERT_TRUE(broker.tls_ctx == NULL); + + /* Route the next accept through the TLS listener only */ + broker.listen_sock = BROKER_SOCKET_INVALID; + broker.listen_sock_tls = MOCK_LISTEN_SOCK; + + MqttBroker_Step(&broker); + ASSERT_EQ(1, g_accept_count); + ASSERT_EQ(1, g_clients[0].close_count); + + broker.listen_sock = MOCK_LISTEN_SOCK; + broker.listen_sock_tls = BROKER_SOCKET_INVALID; + MqttBroker_Free(&broker); +} #endif /* Defense in depth: the MqttBroker struct is public, so a caller can set @@ -1998,51 +2360,294 @@ TEST(connect_credentials_scrubbed_after_accept) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +#ifdef WOLFMQTT_NONBLOCK +/* A refused CONNECT whose CONNACK cannot be written at once keeps the client + * until the write finishes or times out. Its credentials are not needed for + * that, so they must be wiped as on the accepted path rather than stay in + * rx_buf and the username/password copies for the whole deferred write. + * Only nonblocking builds defer the write; a blocking write completes it. */ +TEST(connect_refused_deferred_connack_scrubs_credentials) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* bc; + /* CONNECT v3.1.1, ClientId "id", Username "alice", Password "wrongPW1" + * (the broker expects "s3cr3tPW"). connect_flags 0xC2 = username + + * password + clean session. */ + static const byte connect[] = { + 0x10, 31, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, + 0xC2, + 0x00, 0x3C, + 0x00, 0x02, 'i', 'd', + 0x00, 0x05, 'a', 'l', 'i', 'c', 'e', + 0x00, 0x08, 'w', 'r', 'o', 'n', 'g', 'P', 'W', '1' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + broker.auth_user = "alice"; + broker.auth_pass = "s3cr3tPW"; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + /* Only one CONNACK byte goes out, so the refusal is deferred. */ + g_clients[0].write_limit_once = 1; + run_broker_one_connect(&broker); + + bc = broker.clients; + ASSERT_NOT_NULL(bc); + ASSERT_EQ(1, (int)bc->connack_refused); + ASSERT_EQ(0, (int)bc->connected); + + ASSERT_EQ(0, (int)bc->password_len); + if (bc->password != NULL) { + ASSERT_EQ(0, region_contains((const byte*)bc->password, 8, + "wrongPW1", 8)); + } + ASSERT_EQ(0, region_contains(bc->rx_buf, (int)sizeof(connect), + "wrongPW1", 8)); + ASSERT_EQ(0, region_contains(bc->rx_buf, (int)sizeof(connect), + "alice", 5)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK */ #endif /* WOLFMQTT_BROKER_AUTH */ -#ifdef WOLFMQTT_V5 -/* Return the Reason Code byte of the first DISCONNECT packet in a captured - * stream, or -1 if none carries a reason code. A v5 DISCONNECT with a - * non-success reason and no properties encodes as 0xE0 ; - * the reason is the first byte of the variable header. Only referenced by the - * v5 cap test, so it lives under WOLFMQTT_V5 to avoid an unused-function - * warning (the build runs with -Werror -Wunused). */ -static int first_disconnect_reason(const byte* buf, size_t len) +#ifndef WOLFMQTT_STATIC_MEMORY +/* One v3.1.1 CONNECT with CleanSession 0 for ClientId "c" (MQTT 3.1.1 + * section 3.1), optionally followed by DISCONNECT, through mock client 0. + * Returns the CONNACK return code, or -1 if none was written. */ +static int session_cap_connect(MqttBroker* broker, int n, int disconnect) { - size_t pos = 0; - while (pos < len) { - byte type = (byte)((buf[pos] >> 4) & 0x0F); - size_t remain = 0; - size_t mult = 1; - size_t hdr_len = 1; - int vbi_complete = 0; - while (pos + hdr_len < len && hdr_len <= 5) { - byte b = buf[pos + hdr_len]; - remain += (size_t)(b & 0x7F) * mult; - hdr_len++; - if ((b & 0x80) == 0) { vbi_complete = 1; break; } - mult *= 128; - } - if (!vbi_complete) { + static const byte disc[] = { 0xE0, 0x00 }; + byte connect[] = { + 0x10, 16, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, /* level 4, CleanSession 0 */ + 0x00, 0x04, 'c', '0', '0', '0' + }; + int i; + + connect[15] = (byte)('0' + (n / 100) % 10); + connect[16] = (byte)('0' + (n / 10) % 10); + connect[17] = (byte)('0' + n % 10); + reset_mock_state(connect, sizeof(connect)); + if (disconnect) { + mock_client_input_append(0, disc, sizeof(disc)); + } + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(broker); + if (g_client_closed) { break; } - if (type == MQTT_PACKET_TYPE_DISCONNECT) { - if (remain >= 1 && pos + hdr_len < len) { - return buf[pos + hdr_len]; - } - return -1; - } - if (remain > len - pos - hdr_len) { - break; + } + if (g_out_len < 4 || g_out_buf[0] != 0x20) { + return -1; + } + return g_out_buf[3]; +} + +static int session_cap_has_orphan(MqttBroker* broker, const char* client_id) +{ + BrokerOrphanSession* o; + + for (o = broker->orphan_sessions; o != NULL; o = o->next) { + if (o->client_id != NULL && XSTRCMP(o->client_id, client_id) == 0) { + return 1; } - pos += hdr_len + remain; } - return -1; + return 0; } -#endif /* WOLFMQTT_V5 */ -#endif /* !WOLFMQTT_STATIC_MEMORY */ -/* [MQTT-4.3.3] / Method B: when the broker receives a duplicate QoS 2 +/* With BROKER_MAX_PERSIST_SESSIONS persistent Sessions held, a CONNECT asking + * for another one must be refused (CONNACK 0x03 Server unavailable, MQTT + * 3.1.1 section 3.2.2.3) rather than accepted and later admitted by evicting + * another client's saved Session. A client resuming its own Session does not + * add one and is still accepted. */ +TEST(connect_persistent_session_cap_refuses_new_session) +{ + MqttBroker broker; + MqttBrokerNet net; + int n; + int rc; + + 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)); + + for (n = 0; n < BROKER_MAX_PERSIST_SESSIONS; n++) { + rc = session_cap_connect(&broker, n, 1); + if (rc != MQTT_CONNECT_ACK_CODE_ACCEPTED) { + FAIL("persistent session below the cap was not accepted"); + } + } + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS, broker.orphan_session_count); + + /* A new ClientId would be one Session too many. */ + rc = session_cap_connect(&broker, BROKER_MAX_PERSIST_SESSIONS, 1); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, rc); + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS, broker.orphan_session_count); + ASSERT_TRUE(session_cap_has_orphan(&broker, "c000")); + + /* The oldest client resumes its own Session at the cap. */ + rc = session_cap_connect(&broker, 0, 0); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, rc); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* Build a CleanSession 0 CONNECT for ClientId "c" into out: v3.1.1 + * (MQTT 3.1.1 section 3.1), or at level 5 a v5 CONNECT with Session Expiry + * Interval 60 (MQTT 5.0 section 3.1.2.11.2). Returns its length. */ +static size_t session_cap_build_connect(byte* out, int n, int v5) +{ + size_t len = 0; + byte rl = (byte)(v5 ? 22 : 16); + + out[len++] = 0x10; + out[len++] = rl; + out[len++] = 0x00; out[len++] = 0x04; + out[len++] = 'M'; out[len++] = 'Q'; out[len++] = 'T'; out[len++] = 'T'; + out[len++] = (byte)(v5 ? 0x05 : 0x04); + out[len++] = 0x00; /* CleanSession / Clean Start 0 */ + out[len++] = 0x00; out[len++] = 0x3C; /* keep alive 60 */ + if (v5) { + out[len++] = 0x05; /* Properties length */ + out[len++] = 0x11; /* Session Expiry Interval */ + out[len++] = 0x00; out[len++] = 0x00; + out[len++] = 0x00; out[len++] = 0x3C; + } + out[len++] = 0x00; out[len++] = 0x04; + out[len++] = 'c'; + out[len++] = (byte)('0' + (n / 100) % 10); + out[len++] = (byte)('0' + (n / 10) % 10); + out[len++] = (byte)('0' + n % 10); + return len; +} + +/* The cap counts connected persistent clients as well as offline Sessions. + * With BROKER_MAX_PERSIST_SESSIONS - 1 offline Sessions and one connected + * persistent client, a new ClientId is refused - with Quota Exceeded 0x97 + * for a v5 client (MQTT 5.0 section 3.2.2.2) - while a second connection + * taking over the connected client's own Session is still accepted. */ +TEST(connect_persistent_session_cap_counts_live_sessions) +{ + MqttBroker broker; + MqttBrokerNet net; + byte connect[32]; + size_t len; + int n; + int i; + int rc; + + 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)); + + for (n = 0; n < BROKER_MAX_PERSIST_SESSIONS - 1; n++) { + rc = session_cap_connect(&broker, n, 1); + if (rc != MQTT_CONNECT_ACK_CODE_ACCEPTED) { + FAIL("persistent session below the cap was not accepted"); + } + } + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS - 1, broker.orphan_session_count); + + reset_mock_clients(3); + /* Client 0 takes the last slot and stays connected. */ + len = session_cap_build_connect(connect, 200, 0); + mock_client_input_append(0, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[0].out_buf[3]); + + /* Client 1, a new ClientId, is one Session too many. */ +#ifdef WOLFMQTT_V5 + len = session_cap_build_connect(connect, 201, 1); +#else + len = session_cap_build_connect(connect, 201, 0); +#endif + mock_client_input_append(1, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].out_len >= 4); +#ifdef WOLFMQTT_V5 + ASSERT_EQ(MQTT_REASON_QUOTA_EXCEEDED, g_clients[1].out_buf[3]); +#else + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, g_clients[1].out_buf[3]); +#endif + ASSERT_TRUE(g_clients[1].closed); + + /* Client 2 takes over client 0's Session ("c200") at the cap. */ + len = session_cap_build_connect(connect, 200, 0); + mock_client_input_append(2, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[2].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[2].out_buf[3]); + ASSERT_TRUE(g_clients[0].closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +#ifdef WOLFMQTT_V5 +/* Return the Reason Code byte of the first DISCONNECT packet in a captured + * stream, or -1 if none carries a reason code. A v5 DISCONNECT with a + * non-success reason and no properties encodes as 0xE0 ; + * the reason is the first byte of the variable header. Only referenced by the + * v5 cap test, so it lives under WOLFMQTT_V5 to avoid an unused-function + * warning (the build runs with -Werror -Wunused). */ +static int first_disconnect_reason(const byte* buf, size_t len) +{ + size_t pos = 0; + while (pos < len) { + byte type = (byte)((buf[pos] >> 4) & 0x0F); + size_t remain = 0; + size_t mult = 1; + size_t hdr_len = 1; + int vbi_complete = 0; + while (pos + hdr_len < len && hdr_len <= 5) { + byte b = buf[pos + hdr_len]; + remain += (size_t)(b & 0x7F) * mult; + hdr_len++; + if ((b & 0x80) == 0) { vbi_complete = 1; break; } + mult *= 128; + } + if (!vbi_complete) { + break; + } + if (type == MQTT_PACKET_TYPE_DISCONNECT) { + if (remain >= 1 && pos + hdr_len < len) { + return buf[pos + hdr_len]; + } + return -1; + } + if (remain > len - pos - hdr_len) { + break; + } + pos += hdr_len + remain; + } + return -1; +} +#endif /* WOLFMQTT_V5 */ + +/* [MQTT-4.3.3] / Method B: when the broker receives a duplicate QoS 2 * PUBLISH carrying a packet ID that's still awaiting PUBREL, it MUST send * another PUBREC to the publisher but MUST NOT re-deliver the application * message to subscribers. This test wires up a subscriber and a publisher @@ -2888,6 +3493,86 @@ TEST(qos2_publish_then_abrupt_close_offline_subscriber) MqttBroker_Free(&broker); } +#ifndef WOLFMQTT_STATIC_MEMORY +/* A QoS 2 PUBLISH whose topic copy fails was never delivered, so its Packet + * Identifier must not stay recorded as awaiting PUBREL. Otherwise the + * retransmission after reconnect is treated as a duplicate and acknowledged + * without delivery, losing the message [MQTT-4.3.3-2]. */ +TEST(qos2_topic_alloc_failure_retransmit_delivered) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 0 */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x00 + }; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* PUBLISH QoS 2 (MQTT 3.1.1 section 3.3): topic "x", packet id 1, + * payload "hi". */ + static const byte publish_q2[] = { + 0x34, 0x07, 0x00, 0x01, 'x', 0x00, 0x01, 'h', 'i' + }; + /* Same PUBLISH retransmitted with DUP set [MQTT-4.4.0-1] */ + static const byte publish_q2_dup[] = { + 0x3C, 0x07, 0x00, 0x01, 'x', 0x00, 0x01, 'h', 'i' + }; + + 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(3); + 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)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* The inbound QoS 2 record allocates first and succeeds; the topic + * copy is the next allocation and fails. */ + broker_test_fail_alloc_after(1); + mock_client_input_append(1, publish_q2, sizeof(publish_q2)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + broker_test_disable_alloc_failure(); + ASSERT_EQ(1, g_alloc_failure_count); + ASSERT_TRUE(g_clients[1].closed); + ASSERT_EQ(0, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + /* Resume the Session and retransmit */ + mock_client_input_append(2, connect_pub, sizeof(connect_pub)); + mock_client_input_append(2, publish_q2_dup, sizeof(publish_q2_dup)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH_REC)); + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + /* The out_q-cap tests are dynamic-memory-only (they inspect out_q_count and * the linked client list); the v5 variant additionally uses the v5-only * MQTT_REASON_QUOTA_EXCEEDED. Guard accordingly so the non-V5 and @@ -5313,18 +5998,13 @@ TEST(disconnect_v5_malformed_fires_will) MqttBroker_Free(&broker); } -#ifndef WOLFMQTT_DYN_PROP /* Connect a v5 publisher with a Will and a v3.1.1 subscriber to its Will - * topic, leave the shared property pool one slot short of the publisher's - * DISCONNECT Properties, then send that DISCONNECT with reason_code. Returns - * the number of PUBLISH packets the subscriber received. */ -static int disconnect_v5_props_pool_exhausted_wills(byte reason_code, - int* closed) + * topic, then send a well-formed DISCONNECT carrying only reason_code. + * Returns the number of PUBLISH packets the subscriber received. */ +static int disconnect_v5_reason_wills(byte reason_code, int* closed) { MqttBroker broker; MqttBrokerNet net; - MqttProp* hold = NULL; - int free_slots = 0; int i; int publishes; static const byte sub_connect[] = { @@ -5350,16 +6030,9 @@ static int disconnect_v5_props_pool_exhausted_wills(byte reason_code, 0x00, 0x03, 'l', 'w', 't', 0x00, 0x03, 'b', 'y', 'e' }; - /* v5 DISCONNECT (MQTT 5.0 section 3.14): Remaining Length 16, Reason - * Code, Property Length 14, then two User Properties (identifier 0x26, - * UTF-8 String Pair "k" / "v"). */ - byte disconnect[] = { - 0xE0, 0x10, - 0x00, - 0x0E, - 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', - 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v' - }; + /* v5 DISCONNECT (MQTT 5.0 section 3.14): Remaining Length 1, Reason + * Code only, Property Length omitted (section 3.14.2.2.1). */ + byte disconnect[] = { 0xE0, 0x01, 0x00 }; disconnect[2] = reason_code; install_mock_net(&net); @@ -5373,37 +6046,13 @@ static int disconnect_v5_props_pool_exhausted_wills(byte reason_code, 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, sizeof(disconnect)); for (i = 0; i < 16; i++) { MqttBroker_Step(&broker); } - /* Hold a property-stack reference of our own (see - * connect_v5_refused_when_shared_sub_property_unavailable), then leave - * a single free slot: the DISCONNECT is well formed, but its second - * User Property cannot be allocated. */ - publishes = -1; - if (MqttProps_Init() == MQTT_CODE_SUCCESS) { - while (MqttProps_Add(&hold) != NULL) { - free_slots++; - } - (void)MqttProps_Free(hold); - hold = NULL; - for (i = 0; i < free_slots - 1; i++) { - (void)MqttProps_Add(&hold); - } - - mock_client_input_append(1, disconnect, sizeof(disconnect)); - for (i = 0; i < 16; i++) { - MqttBroker_Step(&broker); - } - (void)MqttProps_Free(hold); - (void)MqttProps_ShutDown(); - - if (free_slots >= 2) { - publishes = count_packets_of_type(g_clients[0].out_buf, - g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); - } - } + publishes = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); *closed = g_clients[1].closed; MqttBroker_Stop(&broker); @@ -5411,56 +6060,1713 @@ static int disconnect_v5_props_pool_exhausted_wills(byte reason_code, return publishes; } -/* A well-formed v5 DISCONNECT whose Properties cannot be allocated is not a - * Malformed Packet. Reason Code 0x00 is a normal disconnection, so the Will - * is discarded [MQTT-3.14.4-3]. */ -TEST(disconnect_v5_props_pool_exhausted_reason_0_discards_will) +/* Only Reason Code 0x00 discards the Will [MQTT-3.14.4-3]. */ +TEST(disconnect_v5_reason_0_discards_will) { int closed = 0; - ASSERT_EQ(0, disconnect_v5_props_pool_exhausted_wills( - MQTT_REASON_SUCCESS, &closed)); + ASSERT_EQ(0, disconnect_v5_reason_wills(MQTT_REASON_SUCCESS, &closed)); ASSERT_TRUE(closed); } -/* Reason Code 0x04 still publishes the Will when the DISCONNECT Properties - * cannot be allocated [MQTT-3.14.4-3]. */ -TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will) +TEST(disconnect_v5_reason_4_publishes_will) { int closed = 0; - ASSERT_EQ(1, disconnect_v5_props_pool_exhausted_wills( + ASSERT_EQ(1, disconnect_v5_reason_wills( MQTT_REASON_DISCONNECT_W_WILL_MSG, &closed)); ASSERT_TRUE(closed); } -#endif /* !WOLFMQTT_DYN_PROP */ -#endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ -/* Parsed details of a single PUBLISH packet on the wire. found = 0 means - * no PUBLISH was present in the buffer; subsequent fields are zero. */ -typedef struct { - int found; - byte first_byte; /* type | DUP | QoS | RETAIN */ - size_t remain_len; /* fixed-header Remaining Length value */ - word16 packet_id; /* 0 if QoS 0 (no packet identifier on wire) */ -} PublishInfo; +/* Any other Reason Code is not a normal disconnection, so the Will is + * published (MQTT 5.0 section 3.1.2.5, [MQTT-3.1.2-10]). */ +TEST(disconnect_v5_reason_error_publishes_will) +{ + int closed = 0; -/* Find the first PUBLISH packet (high nibble = 0x3) in `buf` and return - * its first byte plus parsed remaining-length and packet identifier. - * Mirrors count_packets_of_type's VBI walk so any malformed input stops - * cleanly instead of overrunning. */ -static PublishInfo first_publish_info(const byte* buf, size_t len) + ASSERT_EQ(1, disconnect_v5_reason_wills(MQTT_REASON_UNSPECIFIED_ERR, + &closed)); + ASSERT_TRUE(closed); +} + +/* Connect a v3.1.1 subscriber to "lwt" and a v5 client "P" with a Will on + * "lwt", then take over "P" from a second v5 connection. Returns the number + * of PUBLISH packets the subscriber received by the time the Will Delay of + * the old connection would have elapsed. */ +static int takeover_v5_wills(word32 session_expiry, word32 will_delay, + int new_clean_start, int* old_closed) { - PublishInfo info; - size_t pos = 0; - XMEMSET(&info, 0, sizeof(info)); - while (pos < len) { - byte type = (byte)((buf[pos] >> 4) & 0x0F); - size_t remain = 0; - size_t mult = 1; - size_t hdr_len = 1; - int vbi_complete = 0; - while (pos + hdr_len < len && hdr_len <= 5) { + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT (MQTT 5.0 section 3.1): flags 0x04 (Will, Clean Start 0), + * keep alive 60, Properties length 5 holding Session Expiry Interval + * (0x11, four byte integer), ClientId "P", Will Properties length 5 + * holding Will Delay Interval (0x18, four byte integer), Will Topic + * "lwt", Will Payload "bye". Remaining Length 35. */ + byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 CONNECT, no Will, Properties length 0, ClientId "P". Flags byte is + * 0x02 for Clean Start 1 or 0x00 for Clean Start 0. Remaining Length 14. */ + byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + old_connect[14] = (byte)(session_expiry >> 24); + old_connect[15] = (byte)(session_expiry >> 16); + old_connect[16] = (byte)(session_expiry >> 8); + old_connect[17] = (byte)session_expiry; + old_connect[23] = (byte)(will_delay >> 24); + old_connect[24] = (byte)(will_delay >> 16); + old_connect[25] = (byte)(will_delay >> 8); + old_connect[26] = (byte)will_delay; + if (new_clean_start) { + new_connect[9] = 0x02; + } + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(3); + 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, old_connect, sizeof(old_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(2, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Let any deferred Will come due */ + g_broker_time_s += will_delay + 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + publishes = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + *old_closed = g_clients[1].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return publishes; +} + +/* Number of packets in the capture after the first packet of type `after` + * that have type `target`, or -1 if no `after` packet is present. */ +static int count_packets_after(const byte* buf, size_t len, byte after, + byte target) +{ + size_t pos = 0; + int seen = 0; + int count = 0; + + while (pos < len) { + byte type = (byte)((buf[pos] >> 4) & 0x0F); + size_t remain = 0; + size_t mult = 1; + size_t hdr_len = 1; + int vbi_complete = 0; + + while (pos + hdr_len < len && hdr_len <= 5) { + byte b = buf[pos + hdr_len]; + remain += (size_t)(b & 0x7F) * mult; + hdr_len++; + if ((b & 0x80) == 0) { + vbi_complete = 1; + break; + } + mult *= 128; + } + if (!vbi_complete || remain > len - pos - hdr_len) { + break; + } + if (seen && type == target) { + count++; + } + if (type == after) { + seen = 1; + } + pos += hdr_len + remain; + } + return seen ? count : -1; +} + +/* [MQTT-3.14.4-1] Nothing may be sent on a connection after its DISCONNECT. + * On a v5 takeover the broker sends the old connection DISCONNECT 0x8E and + * publishes its Will; when the old client subscribes to its own Will topic, + * that copy must not reach it after the DISCONNECT. Will Delay 0 and Session + * Expiry 0, so the Will is published at the takeover. */ +TEST(takeover_v5_own_will_not_sent_after_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v5 CONNECT for "P" with Will "lwt"/"bye", Session Expiry 0, Will + * Delay 0 (layout as in takeover_v5_wills). */ + static const byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE (MQTT 5.0 section 3.8): packet id 1, Properties length + * 0, filter "lwt", options 0 (QoS 0). Remaining Length 9. */ + static const byte old_subscribe[] = { + 0x82, 0x09, + 0x00, 0x01, + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT for "P", Clean Start 1, no Will, no properties. */ + static const byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 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, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[0].closed); + ASSERT_EQ(0, count_packets_after(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_DISCONNECT, + MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) +/* A short write of the old client's own Will copy must not leave the + * Session Taken Over DISCONNECT unsent behind it [MQTT-3.14.4-1]. The QoS 0 + * self-copy is not started on a connection that is closing, so the old client + * receives exactly one complete DISCONNECT 0x8E after its SUBACK. */ +TEST(takeover_v5_own_will_partial_write_still_disconnects) +{ + MqttBroker broker; + MqttBrokerNet net; + size_t before; + int publishes_before; + int i; + /* v5 CONNECT for "P" with Will "lwt"/"bye", Session Expiry 0, Will + * Delay 0 (layout as in takeover_v5_wills). */ + static const byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE (MQTT 5.0 section 3.8): packet id 1, Properties length + * 0, filter "lwt", options 0 (QoS 0). Remaining Length 9. */ + static const byte old_subscribe[] = { + 0x82, 0x09, + 0x00, 0x01, + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT for "P", Clean Start 1, no Will, no properties. */ + static const byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 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, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + before = g_clients[0].out_len; + publishes_before = g_clients[0].publish_writes; + g_clients[0].write_limit_on_publish = 1; + mock_client_input_append(1, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[0].closed); + ASSERT_EQ(publishes_before, g_clients[0].publish_writes); + /* MQTT 5.0 section 3.14: fixed header 0xE0, Remaining Length, Reason + * Code 0x8E Session taken over, and nothing after the frame. */ + ASSERT_TRUE(g_clients[0].out_len >= before + 3); + ASSERT_EQ(0xE0, g_clients[0].out_buf[before]); + ASSERT_EQ(MQTT_REASON_SESSION_TAKEN_OVER, + g_clients[0].out_buf[before + 2]); + ASSERT_EQ(before + 2 + g_clients[0].out_buf[before + 1], + g_clients[0].out_len); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + +/* MQTT 5.0 section 3.1.4: with a zero Will Delay the old Will is published + * because its Network Connection is closed, even though the Session + * survives the takeover. */ +TEST(takeover_v5_zero_will_delay_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(60, 0, 0, &closed)); + ASSERT_TRUE(closed); +} + +/* The Session resumes before the Will Delay elapses, so the Will is not + * sent [MQTT-3.1.3-9]. */ +TEST(takeover_v5_will_delay_resumed_session_discards_will) +{ + int closed = 0; + + ASSERT_EQ(0, takeover_v5_wills(60, 30, 0, &closed)); + ASSERT_TRUE(closed); +} + +/* Clean Start 1 ends the old Session at takeover, which publishes the Will + * ahead of its delay (MQTT 5.0 sections 3.1.3.2.2 and 3.1.4). */ +TEST(takeover_v5_clean_start_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(60, 30, 1, &closed)); + ASSERT_TRUE(closed); +} + +/* A zero Session Expiry ends the old Session at takeover, so the Will is + * published (MQTT 5.0 section 3.1.4). */ +TEST(takeover_v5_zero_session_expiry_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(0, 30, 0, &closed)); + ASSERT_TRUE(closed); +} + +#ifndef WOLFMQTT_DYN_PROP +/* Connect a v5 publisher with a Will and a v3.1.1 subscriber to its Will + * topic, leave the shared property pool one slot short of the publisher's + * DISCONNECT Properties, then send that DISCONNECT with reason_code. Returns + * the number of PUBLISH packets the subscriber received. */ +static int disconnect_v5_props_pool_exhausted_wills(byte reason_code, + int* closed) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttProp* hold = NULL; + int free_slots = 0; + int i; + int publishes; + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* Same v5 CONNECT with Will as disconnect_v5_malformed_fires_will. */ + static const byte pub_connect[] = { + 0x10, 0x19, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x06, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 DISCONNECT (MQTT 5.0 section 3.14): Remaining Length 16, Reason + * Code, Property Length 14, then two User Properties (identifier 0x26, + * UTF-8 String Pair "k" / "v"). */ + byte disconnect[] = { + 0xE0, 0x10, + 0x00, + 0x0E, + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v' + }; + disconnect[2] = reason_code; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + 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)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Hold a property-stack reference of our own (see + * connect_v5_refused_when_shared_sub_property_unavailable), then leave + * a single free slot: the DISCONNECT is well formed, but its second + * User Property cannot be allocated. */ + publishes = -1; + if (MqttProps_Init() == MQTT_CODE_SUCCESS) { + while (MqttProps_Add(&hold) != NULL) { + free_slots++; + } + (void)MqttProps_Free(hold); + hold = NULL; + for (i = 0; i < free_slots - 1; i++) { + (void)MqttProps_Add(&hold); + } + + mock_client_input_append(1, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + (void)MqttProps_Free(hold); + (void)MqttProps_ShutDown(); + + if (free_slots >= 2) { + publishes = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + } + } + *closed = g_clients[1].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return publishes; +} + +/* A well-formed v5 DISCONNECT whose Properties cannot be allocated is not a + * Malformed Packet. Reason Code 0x00 is a normal disconnection, so the Will + * is discarded [MQTT-3.14.4-3]. */ +TEST(disconnect_v5_props_pool_exhausted_reason_0_discards_will) +{ + int closed = 0; + + ASSERT_EQ(0, disconnect_v5_props_pool_exhausted_wills( + MQTT_REASON_SUCCESS, &closed)); + ASSERT_TRUE(closed); +} + +/* Reason Code 0x04 still publishes the Will when the DISCONNECT Properties + * cannot be allocated [MQTT-3.14.4-3]. */ +TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, disconnect_v5_props_pool_exhausted_wills( + MQTT_REASON_DISCONNECT_W_WILL_MSG, &closed)); + ASSERT_TRUE(closed); +} + +/* An error Reason Code publishes the Will when the DISCONNECT Properties + * cannot be allocated (MQTT 5.0 section 3.1.2.5). */ +TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, disconnect_v5_props_pool_exhausted_wills( + MQTT_REASON_UNSPECIFIED_ERR, &closed)); + ASSERT_TRUE(closed); +} +#endif /* !WOLFMQTT_DYN_PROP */ +#endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ + +#ifdef WOLFMQTT_V5 +#if defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_NONBLOCK) + /* Static clients cannot resume a deferred write, so these builds close + * without the error DISCONNECT rather than risk truncating it. */ + #define ERROR_DISCONNECT_REASON(reason) (-1) +#else + #define ERROR_DISCONNECT_REASON(reason) (reason) +#endif + +/* MQTT 5.0 section 4.13.1: a Server that detects a Malformed Packet or + * Protocol Error in a packet other than CONNECT SHOULD send DISCONNECT with + * the Reason Code before closing. Connect a v5 client, send one bad packet, + * and return the Reason Code of the DISCONNECT the broker sent (-1 if none); + * *closed reports whether the connection was closed. */ +static int v5_bad_packet_disconnect_reason(const byte* bad, size_t bad_len, + int* closed) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int reason; + /* v5 CONNECT, CleanStart, keep alive 60, no properties, ClientId "A". */ + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + *closed = 0; + return -2; + } + reset_mock_state(connect, sizeof(connect)); + mock_client_input_append(0, bad, bad_len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + reason = first_disconnect_reason(g_out_buf, g_out_len); + *closed = g_client_closed; + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return reason; +} + +/* [MQTT-2.2.2-2] PUBACK fixed-header flags must be 0000: Malformed Packet. */ +TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect) +{ + static const byte puback[] = { 0x41, 0x02, 0x00, 0x01 }; + int closed = 0; + + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_MALFORMED_PACKET), + v5_bad_packet_disconnect_reason(puback, sizeof(puback), &closed)); + ASSERT_TRUE(closed); +} + +/* [MQTT-3.1.0-2] A second CONNECT is a Protocol Error. */ +TEST(v5_second_connect_sends_protocol_error_disconnect) +{ + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + int closed = 0; + + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_PROTOCOL_ERR), + v5_bad_packet_disconnect_reason(connect, sizeof(connect), &closed)); + ASSERT_TRUE(closed); +} + +/* MQTT 5.0 section 3.12: PINGREQ has no variable header or payload, so a + * non-zero Remaining Length is a Malformed Packet. */ +TEST(v5_pingreq_with_body_sends_malformed_disconnect) +{ + static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; + int closed = 0; + + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_MALFORMED_PACKET), + v5_bad_packet_disconnect_reason(pingreq, sizeof(pingreq), &closed)); + ASSERT_TRUE(closed); +} + +/* MQTT 5.0 section 4.12: without an Authentication Method in CONNECT the + * Client must not send AUTH, so an AUTH here is a Protocol Error. Wire: AUTH + * with Remaining Length 0 (Success, section 3.15.2.1). */ +TEST(v5_unexpected_auth_sends_protocol_error_disconnect) +{ + static const byte auth[] = { 0xF0, 0x00 }; + int closed = 0; + + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_PROTOCOL_ERR), + v5_bad_packet_disconnect_reason(auth, sizeof(auth), &closed)); + ASSERT_TRUE(closed); +} + +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) +/* When the DISCONNECT before an error close cannot be written at once, the + * connection stays open until it is fully written and is closed then, so the + * Reason Code still reaches the client. Every write is held back with + * MQTT_CODE_CONTINUE once the CONNACK is out, then released. */ +TEST(v5_error_close_waits_for_deferred_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + /* PINGREQ with a body: a Malformed Packet (MQTT 5.0 section 3.12). */ + static const byte pingreq[] = { 0xC0, 0x01, 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_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_out_buf[3]); + + g_clients[0].write_continue = 1; + mock_client_input_append(0, pingreq, sizeof(pingreq)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_FALSE(g_client_closed); + ASSERT_EQ(-1, first_disconnect_reason(g_out_buf, g_out_len)); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + first_disconnect_reason(g_out_buf, g_out_len)); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK && !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_NONBLOCK) && defined(WOLFMQTT_STATIC_MEMORY) +/* A static client cannot resume a deferred write, so an error close must not + * start a DISCONNECT it may be unable to finish: a short write would leave a + * truncated packet on the wire before the close. The connection is closed + * with nothing written after the CONNACK. */ +TEST(v5_static_error_close_leaves_no_partial_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + size_t connack_len; + int i; + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + /* PINGREQ with a body: a Malformed Packet (MQTT 5.0 section 3.12). */ + static const byte pingreq[] = { 0xC0, 0x01, 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_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_out_buf[3]); + connack_len = g_out_len; + + g_clients[0].write_limit_once = 1; + mock_client_input_append(0, pingreq, sizeof(pingreq)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_client_closed); + ASSERT_EQ(connack_len, g_out_len); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK && WOLFMQTT_STATIC_MEMORY */ +#endif /* WOLFMQTT_V5 */ + +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* Leave a v5 Session for "P" with Session Expiry 60, resume it with + * reconnect, and return the Session Expiry Interval the resumed client + * ends up with (0 on setup failure). */ +static word32 reclaim_session_expiry(const byte* reconnect, + size_t reconnect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* bc; + word32 expiry = 0; + int i; + /* v5 CONNECT (MQTT 5.0 section 3.1): Clean Start 0, keep alive 60, + * Properties length 5 holding Session Expiry Interval 60 (0x11), + * ClientId "P". Remaining Length 19. */ + static const byte connect_60[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* v5 DISCONNECT, Remaining Length 0: Reason Code 0x00 */ + static const byte disconnect[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return 0; + } + + reset_mock_clients(2); + mock_client_input_append(0, connect_60, sizeof(connect_60)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + if (broker.orphan_sessions != NULL && + broker.orphan_sessions->session_expiry_sec == 60) { + mock_client_input_append(1, reconnect, reconnect_len); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + bc = find_broker_client(&broker, "P"); + if (bc != NULL && broker.orphan_sessions == NULL) { + expiry = bc->session_expiry_sec; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return expiry; +} + +/* [MQTT-3.1.2.11.2] The resuming CONNECT's explicit 0xFFFFFFFF means the + * Session never expires; it must not be replaced by the old interval. */ +TEST(reclaim_keeps_explicit_v5_never_expire) +{ + /* As connect_60 in reclaim_session_expiry, with Session Expiry + * 0xFFFFFFFF */ + static const byte connect_never[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_never, + sizeof(connect_never))); +} + +/* A v3.1.1 CleanSession=0 Session never expires (MQTT 3.1.1 section + * 3.1.2.4), even when it resumes a Session created over v5. */ +TEST(reclaim_v311_resume_never_expires) +{ + static const byte connect_v311[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_v311, + sizeof(connect_v311))); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#ifdef WOLFMQTT_V5 +/* Build a v3.1.1 QoS 0 PUBLISH to "x" that exactly fills the broker receive + * buffer (MQTT 3.1.1 section 3.3): fixed header byte, two byte Remaining + * Length, Topic Name "x", then payload. Forwarded to a v5 subscriber it gains + * a Property Length byte and no longer fits the transmit buffer. */ +static size_t build_full_rx_publish(byte* buf, size_t buf_sz, byte retain) +{ + word32 remain_len = (word32)BROKER_RX_BUF_SZ - 3; + size_t i; + + if (buf_sz < (size_t)BROKER_RX_BUF_SZ || remain_len < 128 || + remain_len > 16383) { + return 0; + } + buf[0] = (byte)(0x30 | (retain ? 0x01 : 0x00)); + buf[1] = (byte)((remain_len & 0x7F) | 0x80); + buf[2] = (byte)(remain_len >> 7); + buf[3] = 0x00; + buf[4] = 0x01; + buf[5] = 'x'; + for (i = 6; i < (size_t)BROKER_RX_BUF_SZ; i++) { + buf[i] = 'A'; + } + return (size_t)BROKER_RX_BUF_SZ; +} + +/* A forwarded PUBLISH that does not fit the subscriber's transmit buffer + * must not be written: a clamped payload under the full Remaining Length + * would desynchronize the subscriber (MQTT 3.1.1 section 2.2.3). Returns the + * subscriber's PUBLISH writes, or -1 on setup failure. */ +static int oversize_forward_publish_writes(int retained, int* sub_closed) +{ + MqttBroker broker; + MqttBrokerNet net; + static byte big[BROKER_RX_BUF_SZ]; + size_t big_len; + int i; + int writes; + /* v5 CONNECT, Clean Start, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 0 */ + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x00 + }; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + + big_len = build_full_rx_publish(big, sizeof(big), (byte)retained); + if (big_len == 0) { + return -1; + } + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(2); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + if (!retained) { + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + } + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, big, big_len); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + if (retained) { + /* The retained copy is delivered on subscribe */ + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + } + + writes = g_clients[0].publish_writes; + *sub_closed = g_clients[0].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return writes; +} + +TEST(live_forward_exceeding_tx_buf_not_sent_truncated) +{ + int closed = 1; + + ASSERT_EQ(0, oversize_forward_publish_writes(0, &closed)); + ASSERT_FALSE(closed); +} + +TEST(retained_delivery_exceeding_tx_buf_not_sent_truncated) +{ + int closed = 1; + + ASSERT_EQ(0, oversize_forward_publish_writes(1, &closed)); + ASSERT_FALSE(closed); +} +#endif /* WOLFMQTT_V5 */ + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_BROKER_WILL) +/* A QoS 0 Will written directly to a subscriber whose transmit buffer cannot + * hold it must be skipped rather than sent with a clamped payload (MQTT 3.1.1 + * section 2.2.3). */ +TEST(will_qos0_exceeding_tx_buf_not_sent_truncated) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* sub_bc; + int i; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt", QoS 0 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x00 + }; + /* v3.1.1 CONNECT, Will Flag (QoS 0) and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x06, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + 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)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + sub_bc = find_broker_client(&broker, "S"); + ASSERT_NOT_NULL(sub_bc); + + /* The Will PUBLISH is 10 bytes: fixed header 2, Topic Name 5, payload 3 */ + sub_bc->tx_buf_len = 8; + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[1].closed); + ASSERT_EQ(0, g_clients[0].publish_writes); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +/* A QoS 1 Will matching an offline persistent subscriber is queued in its + * Session and delivered when it reconnects, as a QoS 1 PUBLISH would be + * [MQTT-3.1.2-4]. */ +TEST(will_qos1_queued_for_offline_persistent_subscriber) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v3.1.1 CONNECT, Will Flag, Will QoS 1 and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 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' + }; + + 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(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + ASSERT_NOT_NULL(broker.orphan_sessions); + + /* The Will publisher drops, firing its Will while "S" is offline */ + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + + /* "S" resumes its Session and receives the queued Will */ + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_BROKER_WILL */ + +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) +/* Returns 1 if needle occurs in hay. */ +static int will_bytes_found(const byte* hay, size_t hay_len, + const byte* needle, size_t needle_len) +{ + size_t i; + + for (i = 0; i + needle_len <= hay_len; i++) { + if (XMEMCMP(hay + i, needle, needle_len) == 0) { + return 1; + } + } + return 0; +} + +#ifndef WOLFMQTT_DYN_PROP +/* Take every free entry of the shared property pool, so a decode through it + * fails as it would under load. Release with MqttProps_Free. */ +static MqttProp* will_test_exhaust_prop_pool(void) +{ + MqttProp* head = NULL; + + while (MqttProps_Add(&head) != NULL) { + } + return head; +} +#endif + +/* Most User Properties will_props_reach_live_subscriber can carry while the + * CONNECT and PUBLISH Remaining Lengths stay one byte long. */ +#define WILL_TEST_MAX_USER_PROPS 12 + +/* A v5 Will carries its Will Properties, other than the Will Delay Interval, + * onto the Will PUBLISH in their original order (MQTT 5.0 section 3.1.3.2, + * [MQTT-3.1.3-10]). The Will Properties are a Content Type "t" followed by + * user_props User Properties "k" = "v". With_delay adds a Will Delay + * Interval; Session Expiry 0 then publishes through the pending-Will path at + * once. With exhaust_pool the shared property pool is full while the Will is + * published, which must not drop or strip it. Returns 1 when a live v5 + * subscriber receives exactly the expected PUBLISH. */ +static int will_props_reach_live_subscriber(int with_delay, int exhaust_pool, + int user_props) +{ + MqttBroker broker; + MqttBrokerNet net; +#ifndef WOLFMQTT_DYN_PROP + MqttProp* held = NULL; +#endif + int i; + int found; + byte connect_pub[128]; + byte expected[128]; + int pos = 0; + int exp_pos = 0; + /* v5 CONNECT, Clean Start, no properties, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 0 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x00 + }; + /* Will Delay Interval (0x18) 5 seconds */ + static const byte will_delay[] = { 0x18, 0x00, 0x00, 0x00, 0x05 }; + /* Content Type (0x03) "t" */ + static const byte content_type[] = { 0x03, 0x00, 0x01, 't' }; + /* User Property (0x26) "k" = "v" */ + static const byte user_prop[] = { + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v' + }; + int pub_props_len; + int props_len; + + if (user_props < 0 || user_props > WILL_TEST_MAX_USER_PROPS) { + return -1; + } + pub_props_len = (int)sizeof(content_type) + + user_props * (int)sizeof(user_prop); + props_len = pub_props_len + (with_delay ? (int)sizeof(will_delay) : 0); + + /* v5 CONNECT (MQTT 5.0 section 3.1): Will Flag and Clean Start, keep + * alive 60, no CONNECT properties, ClientId "P", Will Properties, Will + * Topic "lwt", Will Payload "bye". */ + connect_pub[pos++] = 0x10; + connect_pub[pos++] = (byte)(11 + 3 + 1 + props_len + 5 + 5); + XMEMCPY(&connect_pub[pos], "\x00\x04MQTT\x05\x06\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&connect_pub[pos], "\x00\x01P", 3); + pos += 3; + connect_pub[pos++] = (byte)props_len; + if (with_delay) { + XMEMCPY(&connect_pub[pos], will_delay, sizeof(will_delay)); + pos += (int)sizeof(will_delay); + } + XMEMCPY(&connect_pub[pos], content_type, sizeof(content_type)); + pos += (int)sizeof(content_type); + for (i = 0; i < user_props; i++) { + XMEMCPY(&connect_pub[pos], user_prop, sizeof(user_prop)); + pos += (int)sizeof(user_prop); + } + XMEMCPY(&connect_pub[pos], "\x00\x03lwt\x00\x03" "bye", 10); + pos += 10; + + /* Expected v5 QoS 0 PUBLISH (MQTT 5.0 section 3.3): topic "lwt", the + * Will Properties without the Will Delay Interval, payload "bye". */ + expected[exp_pos++] = 0x30; + expected[exp_pos++] = (byte)(5 + 1 + pub_props_len + 3); + XMEMCPY(&expected[exp_pos], "\x00\x03lwt", 5); + exp_pos += 5; + expected[exp_pos++] = (byte)pub_props_len; + XMEMCPY(&expected[exp_pos], content_type, sizeof(content_type)); + exp_pos += (int)sizeof(content_type); + for (i = 0; i < user_props; i++) { + XMEMCPY(&expected[exp_pos], user_prop, sizeof(user_prop)); + exp_pos += (int)sizeof(user_prop); + } + XMEMCPY(&expected[exp_pos], "bye", 3); + exp_pos += 3; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + 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, (size_t)pos); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } +#ifndef WOLFMQTT_DYN_PROP + if (exhaust_pool) { + held = will_test_exhaust_prop_pool(); + if (held == NULL) { + exhaust_pool = -1; + } + } +#else + if (exhaust_pool) { + exhaust_pool = -1; /* no shared pool to exhaust */ + } +#endif + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } +#ifndef WOLFMQTT_DYN_PROP + if (held != NULL) { + (void)MqttProps_Free(held); + } +#endif + + found = will_bytes_found(g_clients[0].out_buf, g_clients[0].out_len, + expected, (size_t)exp_pos); + if (count_packets_of_type(g_clients[0].out_buf, g_clients[0].out_len, + MQTT_PACKET_TYPE_PUBLISH) != 1 || exhaust_pool < 0) { + found = 0; + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return found; +} + +TEST(will_props_forwarded_to_live_subscriber) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 0, 1)); +} + +TEST(delayed_will_props_forwarded_to_live_subscriber) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 0, 1)); +} + +#ifndef WOLFMQTT_DYN_PROP +#ifdef WOLFMQTT_STATIC_MEMORY +/* User Properties, after the Content Type, in the largest Will this static + * build accepts that will_props_reach_live_subscriber can carry. */ +#define WILL_TEST_STATIC_USER_PROPS_BY_LEN ((BROKER_MAX_WILL_PROPS_LEN - 4) / 7) +#define WILL_TEST_STATIC_USER_PROPS_BY_ENTRIES \ + (BROKER_WILL_PROPS_STACK_ENTRIES - 1) +#define WILL_TEST_STATIC_USER_PROPS_MIN2(a, b) (((a) < (b)) ? (a) : (b)) +#define WILL_TEST_STATIC_USER_PROPS WILL_TEST_STATIC_USER_PROPS_MIN2( \ + WILL_TEST_STATIC_USER_PROPS_MIN2(WILL_TEST_STATIC_USER_PROPS_BY_LEN, \ + WILL_TEST_STATIC_USER_PROPS_BY_ENTRIES), WILL_TEST_MAX_USER_PROPS) +#endif + +/* Will Properties are rebuilt without the shared property pool, so a full + * pool can neither drop the Will nor strip its properties, on the immediate + * or the deferred path. In a static build the largest accepted Will still + * fits the stack entries. */ +TEST(will_props_published_with_prop_pool_exhausted) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, 0)); + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 1, 0)); +#if !defined(WOLFMQTT_STATIC_MEMORY) || BROKER_WILL_PROPS_STACK_ENTRIES >= 2 + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, 1)); + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 1, 1)); +#endif +#ifdef WOLFMQTT_STATIC_MEMORY + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, + WILL_TEST_STATIC_USER_PROPS)); +#endif +} +#endif + +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + BROKER_WILL_PROPS_STACK_ENTRIES < WILL_TEST_MAX_USER_PROPS +/* A dynamic build rebuilds a Will with more properties than the stack + * entries on the heap; all of them still reach the subscriber in order. */ +TEST(will_props_beyond_stack_entries_forwarded) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 0, + WILL_TEST_MAX_USER_PROPS)); +} +#endif + +#if WOLFMQTT_MAX_QOS >= 1 +/* A delayed Will can fire after every client has been freed, which releases + * the last MqttClient_Init reference to the shared property pool. The Will + * Properties must still be decoded: the copy queued for the offline Session + * keeps them (MQTT 5.0 section 3.1.3.2) and delivers them on resume. */ +TEST(delayed_will_props_decoded_with_no_clients) +{ + MqttBroker broker; + MqttBrokerNet net; + const byte* out; + size_t out_len; + size_t pos; + int found = 0; + int i; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT for "P": Will Flag, Will QoS 1, Clean Start 0, Properties + * length 5 holding Session Expiry Interval 60 (0x11), Will Properties + * length 14 holding Will Delay Interval (0x18) 2, Message Expiry + * Interval (0x02) 100 and Content Type (0x03) "t", Will Topic "lwt", + * Will Payload "bye". Remaining Length 44. */ + static const byte connect_pub[] = { + 0x10, 0x2C, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0C, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x0E, + 0x18, 0x00, 0x00, 0x00, 0x02, + 0x02, 0x00, 0x00, 0x00, 0x64, + 0x03, 0x00, 0x01, 't', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* Forwarded QoS 1 PUBLISH (MQTT 5.0 section 3.3), Remaining Length 20: + * topic "lwt", a broker-assigned packet id (skipped), then this. */ + static const byte expected_props[] = { + 0x09, + 0x02, 0x00, 0x00, 0x00, 0x64, + 0x03, 0x00, 0x01, 't', + 'b', 'y', 'e' + }; + + 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)); + + g_broker_time_s = 100; + 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(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Both connections are gone before the Will Delay ends. */ + ASSERT_TRUE(g_clients[0].closed); + ASSERT_TRUE(g_clients[1].closed); + g_broker_time_s += 3; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_clients_active = 3; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + for (pos = 0; pos + 9 + sizeof(expected_props) <= out_len; pos++) { + if ((out[pos] & 0xF7) == 0x32 && out[pos + 1] == 0x14 && + XMEMCMP(&out[pos + 2], "\x00\x03lwt", 5) == 0 && + XMEMCMP(&out[pos + 9], expected_props, + sizeof(expected_props)) == 0) { + found = 1; + break; + } + } + g_broker_time_s = 0; + ASSERT_EQ(1, found); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + +#if WOLFMQTT_MAX_QOS >= 1 +/* A QoS 1 Will with Message Expiry Interval `expiry` is queued for an offline + * v5 subscriber, which resumes after wait_sec. Returns the PUBLISH packets it + * receives and sets *wire_expiry to the forwarded Message Expiry Interval + * (-1 if none). */ +static int queued_will_after_wait(word32 wait_sec, byte expiry, + long* wire_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + size_t pos; + const byte* out; + size_t out_len; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT: Will Flag, Will QoS 1 and Clean Start, no CONNECT + * properties, ClientId "P", Will Properties length 5 holding Message + * Expiry Interval (0x02) 10 (low byte at offset 21, set from expiry), + * Will Topic "lwt", Will Payload "bye". Remaining Length 30. */ + byte connect_pub[] = { + 0x10, 0x1E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0E, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + *wire_expiry = -1; + connect_pub[21] = expiry; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + publishes = count_packets_of_type(out, out_len, + MQTT_PACKET_TYPE_PUBLISH); + /* Forwarded layout: 0x32 0x10, topic "lwt", 2 byte packet id, Properties + * length 5, then 0x02 and the four byte Message Expiry Interval. */ + for (pos = 0; pos + 16 <= out_len; pos++) { + if ((out[pos] & 0xF6) == 0x32 && out[pos + 1] == 0x10 && + out[pos + 2] == 0x00 && out[pos + 3] == 0x03 && + out[pos + 4] == 'l' && out[pos + 9] == 0x05 && + out[pos + 10] == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + *wire_expiry = ((long)out[pos + 11] << 24) | + ((long)out[pos + 12] << 16) | ((long)out[pos + 13] << 8) | + (long)out[pos + 14]; + break; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return publishes; +} + +/* [MQTT-3.3.2-5] A queued Will whose Message Expiry Interval passed before + * the subscriber resumed is deleted, not delivered. */ +TEST(queued_will_expired_before_delivery_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_will_after_wait(11, 10, &wire_expiry)); +} + +/* [MQTT-3.3.2-6] A queued Will is delivered with its Message Expiry Interval + * minus the time it waited. */ +TEST(queued_will_expiry_reduced_by_wait) +{ + long wire_expiry; + + ASSERT_EQ(1, queued_will_after_wait(4, 10, &wire_expiry)); + ASSERT_EQ(6, wire_expiry); +} + +/* MQTT 5.0 section 3.3.2.3.3: an explicit Message Expiry Interval of 0 has + * passed by the time an offline subscriber resumes, so the queued Will is + * deleted rather than treated as never expiring. */ +TEST(queued_will_zero_expiry_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_will_after_wait(1, 0, &wire_expiry)); +} +#endif /* WOLFMQTT_MAX_QOS >= 1 */ +#endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ + +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_BROKER_WILDCARDS) && \ + WOLFMQTT_MAX_QOS >= 2 +/* [MQTT-3.3.4-2] An offline Session whose QoS 1 and QoS 2 subscriptions both + * match a QoS 2 Will gets it at QoS 2. One copy is queued, at the highest + * matching QoS, so a full queue cannot evict that copy for the other. */ +TEST(will_offline_overlapping_subs_queue_max_qos_once) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + size_t pos; + int qos2 = 0; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt" QoS 1, then packet id 2, filter + * "+" QoS 2 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte subscribe_any[] = { + 0x82, 0x06, 0x00, 0x02, 0x00, 0x01, '+', 0x02 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v3.1.1 CONNECT, Will Flag, Will QoS 2 and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x16, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + 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(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, subscribe_any, sizeof(subscribe_any)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH)); + for (pos = 0; pos + 4 < g_clients[2].out_len; pos++) { + if ((g_clients[2].out_buf[pos] & 0xF0) == 0x30 && + g_clients[2].out_buf[pos + 2] == 0x00 && + g_clients[2].out_buf[pos + 3] == 0x03 && + g_clients[2].out_buf[pos + 4] == 'l') { + qos2 = ((g_clients[2].out_buf[pos] & 0x06) == 0x04); + break; + } + } + ASSERT_TRUE(qos2); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) +/* Queue a v5 QoS 1 PUBLISH with Message Expiry Interval `expiry` for an + * offline persistent subscriber, let wait_sec pass, then resume the Session. + * Returns the PUBLISH packets delivered and sets *wire_expiry to the Message + * Expiry Interval of the first one (-1 if not found). */ +static int queued_publish_after_wait(word32 wait_sec, byte expiry, + long* wire_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + size_t pos; + const byte* out; + size_t out_len; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 1 */ + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT, Clean Start 1, Properties length 0, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + /* v5 PUBLISH QoS 1 (MQTT 5.0 section 3.3): topic "x", packet id 1, + * Properties length 5 holding Message Expiry Interval 10 (0x02, low byte + * at offset 12, set from expiry), payload "hi". Remaining Length 13. */ + byte publish_x[] = { + 0x32, 0x0D, + 0x00, 0x01, 'x', + 0x00, 0x01, + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 'h', 'i' + }; + + *wire_expiry = -1; + publish_x[12] = expiry; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + 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, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, publish_x, sizeof(publish_x)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + publishes = count_packets_of_type(out, out_len, + MQTT_PACKET_TYPE_PUBLISH); + /* Forwarded layout: 0x32 0x0D, topic "x", 2 byte packet id, Properties + * length 5, then 0x02 and the four byte Message Expiry Interval. */ + for (pos = 0; pos + 13 <= out_len; pos++) { + if (out[pos] == 0x32 && out[pos + 1] == 0x0D && + out[pos + 2] == 0x00 && out[pos + 3] == 0x01 && + out[pos + 4] == 'x' && out[pos + 7] == 0x05 && + out[pos + 8] == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + *wire_expiry = ((long)out[pos + 9] << 24) | + ((long)out[pos + 10] << 16) | ((long)out[pos + 11] << 8) | + (long)out[pos + 12]; + break; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return publishes; +} + +/* [MQTT-3.3.2-5] A queued copy whose Message Expiry Interval passed before + * onward delivery started is deleted, not delivered. */ +TEST(queued_publish_expired_before_delivery_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_publish_after_wait(11, 10, &wire_expiry)); +} + +/* [MQTT-3.3.2-6] A queued copy is delivered with the received Message Expiry + * Interval minus the time it waited in the Server. */ +TEST(queued_publish_expiry_reduced_by_wait) +{ + long wire_expiry; + + ASSERT_EQ(1, queued_publish_after_wait(4, 10, &wire_expiry)); + ASSERT_EQ(6, wire_expiry); +} + +/* MQTT 5.0 section 3.3.2.3.3: an explicit Message Expiry Interval of 0 is + * not "no expiry". A copy still queued once a second has passed is deleted; + * one whose delivery starts within the same second goes out with 0. */ +TEST(queued_publish_zero_expiry_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_publish_after_wait(1, 0, &wire_expiry)); + ASSERT_EQ(1, queued_publish_after_wait(0, 0, &wire_expiry)); + ASSERT_EQ(0, wire_expiry); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ + +/* Parsed details of a single PUBLISH packet on the wire. found = 0 means + * no PUBLISH was present in the buffer; subsequent fields are zero. */ +typedef struct { + int found; + byte first_byte; /* type | DUP | QoS | RETAIN */ + size_t remain_len; /* fixed-header Remaining Length value */ + word16 packet_id; /* 0 if QoS 0 (no packet identifier on wire) */ +} PublishInfo; + +/* Find the first PUBLISH packet (high nibble = 0x3) in `buf` and return + * its first byte plus parsed remaining-length and packet identifier. + * Mirrors count_packets_of_type's VBI walk so any malformed input stops + * cleanly instead of overrunning. */ +static PublishInfo first_publish_info(const byte* buf, size_t len) +{ + PublishInfo info; + size_t pos = 0; + XMEMSET(&info, 0, sizeof(info)); + while (pos < len) { + byte type = (byte)((buf[pos] >> 4) & 0x0F); + size_t remain = 0; + size_t mult = 1; + size_t hdr_len = 1; + int vbi_complete = 0; + while (pos + hdr_len < len && hdr_len <= 5) { byte b = buf[pos + hdr_len]; remain += (size_t)(b & 0x7F) * mult; hdr_len++; @@ -5499,8 +7805,432 @@ static PublishInfo first_publish_info(const byte* buf, size_t len) } pos += hdr_len + remain; } - return info; + return info; +} + +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + !defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 +/* The old client subscribes at QoS 1 to its own QoS 1 Will topic and is + * taken over with Will Delay 0. The copy for the old connection is queued + * rather than written, so the old connection ends with its DISCONNECT 0x8E + * [MQTT-3.14.4-1]. A resumed Session delivers it once on the new connection + * [MQTT-4.4.0-1]; a Clean Start ends the Session and frees it. Returns the + * PUBLISH count on the new connection, or -1 on a setup or leak failure, and + * sets *first_byte to the first PUBLISH's fixed header byte. */ +static int takeover_qos1_self_will(int new_clean_start, byte* first_byte) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo info; + size_t before; + int publishes_before; + int live_before; + int publishes; + int i; + /* v5 CONNECT for "P": Will Flag, Will QoS 1, Clean Start 0, Properties + * length 5 holding Session Expiry Interval 300 (0x11), Will Properties + * length 0, Will Topic "lwt", Will Payload "bye". Remaining Length 30. */ + static const byte old_connect[] = { + 0x10, 0x1E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0C, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'P', + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte old_subscribe[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + /* v5 CONNECT for "P", Clean Start 0, Session Expiry Interval 300 */ + static const byte resume_connect[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'P' + }; + /* v5 CONNECT for "P", Clean Start 1, no properties */ + static const byte clean_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + *first_byte = 0; + live_before = g_alloc_success_count - g_alloc_free_count; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(2); + mock_client_input_append(0, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + before = g_clients[0].out_len; + publishes_before = g_clients[0].publish_writes; + if (new_clean_start) { + mock_client_input_append(1, clean_connect, sizeof(clean_connect)); + } + else { + mock_client_input_append(1, resume_connect, sizeof(resume_connect)); + } + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* MQTT 5.0 section 3.14: the old connection's output after its SUBACK + * is one DISCONNECT (0xE0) carrying Reason Code 0x8E and nothing else. */ + if (!g_clients[0].closed || + g_clients[0].publish_writes != publishes_before || + g_clients[0].out_len < before + 3 || + g_clients[0].out_buf[before] != 0xE0 || + g_clients[0].out_buf[before + 2] != + MQTT_REASON_SESSION_TAKEN_OVER || + g_clients[0].out_len != + before + 2 + g_clients[0].out_buf[before + 1]) { + publishes = -1; + } + else { + publishes = count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_PUBLISH); + info = first_publish_info(g_clients[1].out_buf, g_clients[1].out_len); + if (info.found) { + *first_byte = info.first_byte; + if (info.packet_id == 0) { + publishes = -1; + } + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + if (g_alloc_success_count - g_alloc_free_count != live_before) { + publishes = -1; + } + return publishes; +} + +TEST(takeover_v5_resumed_session_gets_queued_self_will) +{ + byte first_byte; + + /* MQTT 5.0 section 3.3.1: QoS 1, DUP 0, RETAIN 0 */ + ASSERT_EQ(1, takeover_qos1_self_will(0, &first_byte)); + ASSERT_EQ(0x32, first_byte); +} + +TEST(takeover_v5_clean_start_frees_queued_self_will) +{ + byte first_byte; + + ASSERT_EQ(0, takeover_qos1_self_will(1, &first_byte)); +} +#endif + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) +/* v5 PUBLISH QoS 1 on "x" with Message Expiry Interval 10 (MQTT 5.0 + * section 3.3.2.3.3), packet id `pid`, payload "hi". Remaining Length 13. */ +static void expiry_publish_frame(byte* out, byte pid) +{ + static const byte frame[] = { + 0x32, 0x0D, + 0x00, 0x01, 'x', + 0x00, 0x01, + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 'h', 'i' + }; + XMEMCPY(out, frame, sizeof(frame)); + out[6] = pid; +} + +/* A connected v5 subscriber with Receive Maximum 1 holds one message in + * flight; a second queued behind it expires before the window opens again, + * so after the PUBACK it is deleted instead of delivered [MQTT-3.3.2-5]. */ +TEST(queued_publish_expired_behind_inflight_cap_dropped) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo first; + byte publish[15]; + byte puback[4]; + int i; + /* v5 CONNECT, Clean Start 1, Properties length 3 holding Receive + * Maximum 1 (0x21), ClientId "R". Remaining Length 17. */ + static const byte connect_sub[] = { + 0x10, 0x11, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x03, 0x21, 0x00, 0x01, + 0x00, 0x01, 'R' + }; + 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' + }; + + 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)); + + g_broker_time_s = 100; + 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)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + expiry_publish_frame(publish, 2); + mock_client_input_append(1, publish, sizeof(publish)); + 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)); + first = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); + ASSERT_TRUE(first.found); + + /* The queued copy expires, then the window opens. */ + g_broker_time_s += 11; + puback[0] = 0x40; + puback[1] = 0x02; + puback[2] = (byte)(first.packet_id >> 8); + puback[3] = (byte)first.packet_id; + mock_client_input_append(0, puback, sizeof(puback)); + 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)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; +} + +/* [MQTT-4.4.0-1] An unacknowledged QoS 1 PUBLISH is re-sent with DUP set when + * the Session resumes. It was already delivered once, so its Message Expiry + * no longer applies: the retransmission goes out even after the interval has + * passed. */ +TEST(inflight_publish_resent_after_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo resent; + byte publish[15]; + int i; + /* v5 CONNECT, Clean Start 0, Session Expiry Interval 300, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 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)); + + g_broker_time_s = 100; + reset_mock_clients(3); + 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)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Delivered once, left unacknowledged, then the subscriber leaves. */ + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += 11; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + resent = first_publish_info(g_clients[2].out_buf, g_clients[2].out_len); + ASSERT_TRUE(resent.found); + ASSERT_NE(0, resent.first_byte & 0x08); /* DUP */ + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; +} + +#ifdef WOLFMQTT_BROKER_PERSIST +static int g_expiry_outq_dels; + +static int expiry_kv_put(void* ctx, byte ns, const byte* key, word16 key_len, + const byte* blob, word32 blob_len) +{ + (void)ctx; (void)ns; (void)key; (void)key_len; (void)blob; + (void)blob_len; + return MQTT_CODE_SUCCESS; +} + +static int expiry_kv_get(void* ctx, byte ns, const byte* key, word16 key_len, + byte* out, word32* inout_len) +{ + (void)ctx; (void)ns; (void)key; (void)key_len; (void)out; + (void)inout_len; + return MQTT_CODE_ERROR_NOT_FOUND; +} + +static int expiry_kv_del(void* ctx, byte ns, const byte* key, word16 key_len) +{ + (void)ctx; (void)key; (void)key_len; + if (ns == BROKER_PERSIST_NS_OUTQ) { + g_expiry_outq_dels++; + } + return MQTT_CODE_SUCCESS; +} + +static int expiry_kv_iter(void* ctx, byte ns, MqttBrokerPersist_IterCb cb, + void* cb_ctx) +{ + (void)ctx; (void)ns; (void)cb; (void)cb_ctx; + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int expiry_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* Queue a QoS 1 copy with Message Expiry 10 for an offline persistent + * subscriber, let wait_sec pass and resume the Session. Returns the number + * of OUTQ records the broker deleted. */ +static int expiry_outq_dels_after_wait(int wait_sec) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + byte publish[15]; + int i; + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + hooks.kv_put = expiry_kv_put; + hooks.kv_get = expiry_kv_get; + hooks.kv_del = expiry_kv_del; + hooks.kv_iter = expiry_kv_iter; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = expiry_derive_key; +#endif + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_SetPersistHooks(&broker, &hooks) != + MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + 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, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + g_expiry_outq_dels = 0; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return g_expiry_outq_dels; +} + +/* An expired queued copy is deleted from the persistent store too, so a + * restart cannot replay it [MQTT-3.3.2-5]. Measured against the same flow + * where the copy has not expired and is only delivered. */ +TEST(queued_publish_expired_deletes_persisted_record) +{ + int expired_dels = expiry_outq_dels_after_wait(11); + int live_dels = expiry_outq_dels_after_wait(4); + + ASSERT_TRUE(expired_dels >= 0); + ASSERT_TRUE(live_dels >= 0); + ASSERT_TRUE(expired_dels > live_dels); } +#endif /* WOLFMQTT_BROKER_PERSIST */ +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ + #ifdef WOLFMQTT_STATIC_MEMORY /* Reads the broker's outbound in-flight table directly; the library helper @@ -8903,6 +11633,7 @@ TEST(disconnect_v5_session_expiry_raise_sends_protocol_error) 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) @@ -10481,28 +13212,151 @@ TEST(persist_partial_publish_restart_keeps_dup) MqttBrokerNet net; MqttBrokerPersistHooks hooks; PersistOrderStore store; - BrokerClient* sub_bc; - PublishInfo info; - word16 packet_id; + BrokerClient* sub_bc; + PublishInfo info; + word16 packet_id; + 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 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 publish_x[] = { + 0x32, 0x08, + 0x00, 0x01, 'x', 0x00, 0x07, 'A', 'B', 'C' + }; + + install_mock_net(&net); + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&restored, 0, sizeof(restored)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&store, 0, sizeof(store)); + hooks.kv_put = persist_order_put; + hooks.kv_get = persist_order_get; + hooks.kv_iter = persist_order_iter; + hooks.ctx = &store; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&source, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&source, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&source)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&source)); + reset_mock_clients(2); + mock_client_input_append(0, connect_pub, sizeof(connect_pub)); + mock_client_input_append(1, connect_sub, sizeof(connect_sub)); + mock_client_input_append(1, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&source); + } + sub_bc = find_broker_client(&source, "S"); + ASSERT_NOT_NULL(sub_bc); + + g_clients[1].write_limit_once = 1; + mock_client_input_append(0, publish_x, sizeof(publish_x)); + (void)MqttBroker_Step(&source); + ASSERT_NOT_NULL(sub_bc->out_q_head); + ASSERT_EQ(1, sub_bc->out_q_head->retransmit_dup); + packet_id = sub_bc->out_q_head->packet_id; + + g_clients[1].write_err = 1; + (void)MqttBroker_Step(&source); + ASSERT_TRUE(g_clients[1].closed); + ASSERT_EQ(1, store.outq_count); + + MqttBroker_Stop(&source); + MqttBroker_Free(&source); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&restored, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, + MqttBroker_SetPersistHooks(&restored, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&restored)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&restored)); + reset_mock_clients(1); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&restored); + } + + info = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); + ASSERT_TRUE(info.found); + ASSERT_EQ(0x3A, info.first_byte); + ASSERT_EQ(packet_id, info.packet_id); + + MqttBroker_Stop(&restored); + MqttBroker_Free(&restored); +} +#endif + +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + WOLFMQTT_MAX_QOS >= 1 +/* A Will queued for an offline v5 Session keeps its Properties across a + * broker restart (MQTT 5.0 section 3.1.3.2), and the forwarded Message + * Expiry Interval is reduced by the time waited [MQTT-3.3.2-6]. */ +TEST(persist_restart_keeps_queued_will_props) +{ + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistOrderStore store; + const byte* out; + size_t out_len; + size_t pos; + int found = 0; int i; - static const byte connect_pub[] = { - 0x10, 0x0D, - 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x04, 0x02, 0x00, 0x3C, - 0x00, 0x01, 'P' - }; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ static const byte connect_sub[] = { - 0x10, 0x0D, + 0x10, 0x13, 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x04, 0x00, 0x00, 0x3C, + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, 0x00, 0x01, 'S' }; - static const byte subscribe_x[] = { - 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x01 + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 }; - static const byte publish_x[] = { - 0x32, 0x08, - 0x00, 0x01, 'x', 0x00, 0x07, 'A', 'B', 'C' + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT: Will Flag, Will QoS 1 and Clean Start, no CONNECT + * properties, ClientId "P", Will Properties length 16 holding Message + * Expiry Interval (0x02) 60, Content Type (0x03) "t" and User Property + * (0x26) "k"="v", Will Topic "lwt", Will Payload "bye". + * Remaining Length 41. */ + static const byte connect_pub[] = { + 0x10, 0x29, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0E, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x10, + 0x02, 0x00, 0x00, 0x00, 0x3C, + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* Forwarded PUBLISH QoS 1 (MQTT 5.0 section 3.3), Remaining Length 27: + * topic "lwt", a broker-assigned packet id (skipped), then this. The + * Message Expiry Interval is 60 less the 4 seconds waited. */ + static const byte expected_props[] = { + 0x10, + 0x02, 0x00, 0x00, 0x00, 0x38, + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 'b', 'y', 'e' }; install_mock_net(&net); @@ -10518,35 +13372,28 @@ TEST(persist_partial_publish_restart_keeps_dup) hooks.derive_key = persist_order_derive_key; #endif + g_broker_time_s = 100; ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&source, &net)); ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&source, &hooks)); ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&source)); ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&source)); reset_mock_clients(2); - mock_client_input_append(0, connect_pub, sizeof(connect_pub)); - mock_client_input_append(1, connect_sub, sizeof(connect_sub)); - mock_client_input_append(1, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&source); + } + g_clients[1].read_err = 1; for (i = 0; i < 16; i++) { (void)MqttBroker_Step(&source); } - sub_bc = find_broker_client(&source, "S"); - ASSERT_NOT_NULL(sub_bc); - - g_clients[1].write_limit_once = 1; - mock_client_input_append(0, publish_x, sizeof(publish_x)); - (void)MqttBroker_Step(&source); - ASSERT_NOT_NULL(sub_bc->out_q_head); - ASSERT_EQ(1, sub_bc->out_q_head->retransmit_dup); - packet_id = sub_bc->out_q_head->packet_id; - - g_clients[1].write_err = 1; - (void)MqttBroker_Step(&source); - ASSERT_TRUE(g_clients[1].closed); ASSERT_EQ(1, store.outq_count); - MqttBroker_Stop(&source); MqttBroker_Free(&source); + g_broker_time_s += 4; ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&restored, &net)); ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&restored, &hooks)); @@ -10558,10 +13405,19 @@ TEST(persist_partial_publish_restart_keeps_dup) (void)MqttBroker_Step(&restored); } - info = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); - ASSERT_TRUE(info.found); - ASSERT_EQ(0x3A, info.first_byte); - ASSERT_EQ(packet_id, info.packet_id); + out = g_clients[0].out_buf; + out_len = g_clients[0].out_len; + for (pos = 0; pos + 9 + sizeof(expected_props) <= out_len; pos++) { + if ((out[pos] & 0xF7) == 0x32 && out[pos + 1] == 0x1B && + XMEMCMP(&out[pos + 2], "\x00\x03lwt", 5) == 0 && + XMEMCMP(&out[pos + 9], expected_props, + sizeof(expected_props)) == 0) { + found = 1; + break; + } + } + g_broker_time_s = 0; + ASSERT_EQ(1, found); MqttBroker_Stop(&restored); MqttBroker_Free(&restored); @@ -10877,9 +13733,287 @@ TEST(persist_restore_packet_id_wrap_preserves_fifo) ASSERT_NOT_NULL(orphan->out_q_tail); ASSERT_EQ(2, orphan->out_q_tail->packet_id); - MqttBroker_Free(&restored_again); - MqttBroker_Free(&source); + MqttBroker_Free(&restored_again); + MqttBroker_Free(&source); +} + +/* An OUTQ record is keyed by client_id || 0x00 || Packet Identifier and also + * carries the identifier in its body. Restore must only load a record whose + * two copies agree and are non-zero ([MQTT-2.2.1-3]: a QoS > 0 PUBLISH has a + * non-zero Packet Identifier). A record whose key disagrees with its body + * could not be deleted by the identifier it would be acknowledged under, and + * could collide with another queued entry. */ +TEST(persist_restore_rejects_outq_packet_id_mismatch) +{ + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistOrderStore store; + BrokerOutPub out; + BrokerOrphanSession* orphan; + PersistOrderRecord* rec; + char topic[] = "t"; + + install_mock_net(&net); + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&restored, 0, sizeof(restored)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&store, 0, sizeof(store)); + hooks.kv_put = persist_order_put; + hooks.kv_get = persist_order_get; + hooks.kv_iter = persist_order_iter; + hooks.ctx = &store; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&source, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&source, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&source)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_PutOrphanSession(&source, + "cid", MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1)); + + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.qos = MQTT_QOS_1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + /* Packet Identifier 1: valid. */ + out.enqueue_seq = 1; + out.packet_id = 1; + ASSERT_EQ(MQTT_CODE_SUCCESS, + BrokerPersist_PutOutPub(&source, "cid", &out)); + /* Packet Identifier 2, then its key retagged to 3. */ + out.enqueue_seq = 2; + out.packet_id = 2; + ASSERT_EQ(MQTT_CODE_SUCCESS, + BrokerPersist_PutOutPub(&source, "cid", &out)); + /* Packet Identifier 0 in both key and body. */ + out.enqueue_seq = 3; + out.packet_id = 0; + (void)BrokerPersist_PutOutPub(&source, "cid", &out); + ASSERT_EQ(3, store.outq_count); + + rec = &store.outq[1]; + ASSERT_EQ(3 + 3, (int)rec->key_len); /* "cid" 0x00 pid_hi pid_lo */ + ASSERT_EQ(0x02, rec->key[5]); + rec->key[5] = 0x03; + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&restored, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, + MqttBroker_SetPersistHooks(&restored, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&restored)); + + orphan = restored.orphan_sessions; + ASSERT_NOT_NULL(orphan); + ASSERT_EQ(1, orphan->out_q_count); + ASSERT_NOT_NULL(orphan->out_q_head); + ASSERT_EQ(1, orphan->out_q_head->packet_id); + + MqttBroker_Free(&restored); + MqttBroker_Free(&source); +} + +typedef struct PersistSubsCount { + int puts; + int dels; +} PersistSubsCount; + +static int persist_subs_count_put(void* ctx, byte ns, const byte* key, + word16 key_len, const byte* blob, word32 blob_len) +{ + (void)key; + (void)key_len; + (void)blob; + (void)blob_len; + if (ns == BROKER_PERSIST_NS_SUBS) { + ((PersistSubsCount*)ctx)->puts++; + } + return MQTT_CODE_SUCCESS; +} + +static int persist_subs_count_get(void* ctx, byte ns, const byte* key, + word16 key_len, byte* out, word32* inout_len) +{ + (void)ctx; + (void)ns; + (void)key; + (void)key_len; + (void)out; + (void)inout_len; + return MQTT_CODE_ERROR_NOT_FOUND; +} + +static int persist_subs_count_del(void* ctx, byte ns, const byte* key, + word16 key_len) +{ + (void)key; + (void)key_len; + if (ns == BROKER_PERSIST_NS_SUBS) { + ((PersistSubsCount*)ctx)->dels++; + } + return MQTT_CODE_SUCCESS; +} + +static int persist_subs_count_iter(void* ctx, byte ns, + MqttBrokerPersist_IterCb cb, void* cb_ctx) +{ + (void)ctx; + (void)ns; + (void)cb; + (void)cb_ctx; + return MQTT_CODE_SUCCESS; +} + +/* SUBSCRIBE and UNSUBSCRIBE change the Session's subscription list before + * the SUBACK or UNSUBACK is written, so the persisted list must follow the + * change even when that write fails and the connection closes; otherwise a + * restart restores the list from before the change. */ +TEST(persist_subs_written_when_ack_write_fails) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistSubsCount count; + int i; + int puts_before; + /* v3.1.1 CONNECT, CleanSession=0, keep alive 60, ClientId "S" */ + static const byte connect_s[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 1 (MQTT 3.1.1 section 3.8) */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x01 + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&count, 0, sizeof(count)); + hooks.kv_put = persist_subs_count_put; + hooks.kv_get = persist_subs_count_get; + hooks.kv_del = persist_subs_count_del; + hooks.kv_iter = persist_subs_count_iter; + hooks.ctx = &count; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&broker, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect_s, sizeof(connect_s)); + run_broker_one_connect(&broker); + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_CONNECT_ACK)); + + /* The SUBACK write fails and the connection closes. */ + puts_before = count.puts; + g_clients[0].write_err_pending = 1; + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(count.puts > puts_before); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#ifdef WOLFMQTT_NONBLOCK +/* A SUBACK or UNSUBACK whose write is deferred must still shadow-write the + * persistent session's subscription list; otherwise a restart restores the + * list from before the change. */ +TEST(persist_subs_written_when_ack_deferred) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistSubsCount count; + int i; + int puts_before; + int dels_before; + /* v3.1.1 CONNECT, CleanSession=0, keep alive 60, ClientId "S" */ + static const byte connect_s[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 1 (MQTT 3.1.1 section 3.8) */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x01 + }; + /* UNSUBSCRIBE packet id 2, filter "x" (MQTT 3.1.1 section 3.10) */ + static const byte unsubscribe_x[] = { + 0xA2, 0x05, 0x00, 0x02, 0x00, 0x01, 'x' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&count, 0, sizeof(count)); + hooks.kv_put = persist_subs_count_put; + hooks.kv_get = persist_subs_count_get; + hooks.kv_del = persist_subs_count_del; + hooks.kv_iter = persist_subs_count_iter; + hooks.ctx = &count; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&broker, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect_s, sizeof(connect_s)); + run_broker_one_connect(&broker); + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_CONNECT_ACK)); + + /* SUBACK write defers */ + puts_before = count.puts; + g_clients[0].write_continue = 1; + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(puts_before + 1, count.puts); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_SUBSCRIBE_ACK)); + + /* UNSUBACK write defers; the now empty list is deleted */ + dels_before = count.dels; + g_clients[0].write_continue = 1; + mock_client_input_append(0, unsubscribe_x, sizeof(unsubscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(dels_before + 1, count.dels); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_UNSUBSCRIBE_ACK)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); } +#endif /* WOLFMQTT_NONBLOCK */ #endif static int persist_test_stream_open(void* ctx, byte ns, const byte* key, @@ -11198,130 +14332,938 @@ static int persist_test_iter_cb(const byte* key, word16 key_len, capture->key_len = key_len; capture->blob_len = blob_len; } - capture->calls++; - return 0; + capture->calls++; + return 0; +} + +TEST(persist_parent_component_rejected_as_bad_argument) +{ + MqttBrokerPersistHooks hooks; + int rc; + + XMEMSET(&hooks, 0, sizeof(hooks)); + rc = MqttBrokerNet_PersistPosix_Init(&hooks, + "../wolfmqtt-persist-invalid"); + if (rc == MQTT_CODE_SUCCESS) { + MqttBrokerNet_PersistPosix_Free(&hooks); + } + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + +TEST(persist_iter_skips_fifo_without_blocking) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + char fifo_path[680]; + MqttBrokerPersistHooks hooks; + PersistIterCapture capture; + char* root; + pid_t child; + pid_t waited; + int status = 0; + int paths_ok = 0; + int init_rc; + + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&capture, 0, sizeof(capture)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && + snprintf(fifo_path, sizeof(fifo_path), "%s/1234.bin", + ns_path) > 0) { + paths_ok = 1; + } + ASSERT_TRUE(paths_ok); + ASSERT_EQ(0, mkdir(ns_path, 0700)); + ASSERT_EQ(0, mkfifo(fifo_path, 0600)); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + child = fork(); + ASSERT_TRUE(child >= 0); + if (child == 0) { + int iter_rc; + + (void)alarm(2); + iter_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, &capture); + _exit((iter_rc == MQTT_CODE_SUCCESS && capture.calls == 0) ? 0 : 1); + } + waited = waitpid(child, &status, 0); + + MqttBrokerNet_PersistPosix_Free(&hooks); + (void)unlink(fifo_path); + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ((int)child, (int)waited); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); +} + +/* kv_iter and kv_del take the same opaque key bytes, so any key the POSIX + * backend yields while iterating must be removable through kv_del; the static + * DelOutQueue loop repeats iterate-and-delete until nothing matches and would + * otherwise never finish. The broker names records in lowercase hex, so a + * record file named in uppercase hex (created outside the broker) must not + * be yielded as a key that kv_del then cannot find. */ +TEST(persist_iter_keys_are_deletable) +{ + static const byte blob[] = "x"; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + char rec_path[680]; + MqttBrokerPersistHooks hooks; + PersistIterCapture capture; + char* root; + FILE* f; + int paths_ok = 0; + int init_rc; + int iter1_rc = MQTT_CODE_ERROR_SYSTEM; + int del_rc = MQTT_CODE_SUCCESS; + int iter2_rc = MQTT_CODE_ERROR_SYSTEM; + int iter2_calls = -1; + + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&capture, 0, sizeof(capture)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && + snprintf(rec_path, sizeof(rec_path), "%s/ABCD.bin", + ns_path) > 0) { + paths_ok = 1; + } + ASSERT_TRUE(paths_ok); + ASSERT_EQ(0, mkdir(ns_path, 0700)); + f = fopen(rec_path, "wb"); + ASSERT_NOT_NULL(f); + ASSERT_EQ(sizeof(blob), fwrite(blob, 1, sizeof(blob), f)); + ASSERT_EQ(0, fclose(f)); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + iter1_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, + &capture); + if (capture.calls > 0) { + del_rc = hooks.kv_del(hooks.ctx, 7, capture.key, + capture.key_len); + } + XMEMSET(&capture, 0, sizeof(capture)); + iter2_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, + &capture); + iter2_calls = capture.calls; + MqttBrokerNet_PersistPosix_Free(&hooks); + } + + (void)unlink(rec_path); + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, iter1_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, del_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, iter2_rc); + ASSERT_EQ(0, iter2_calls); +} + +#ifdef __MACH__ +/* macOS exposes /tmp as a trusted operating-system symlink to /private/tmp. + * A persistence root below that standard alias must initialize without + * weakening the no-follow checks on namespaces or record files. */ +TEST(persist_root_below_macos_tmp_alias_accepted) +{ + char root_template[] = "/tmp/wolfmqtt-persist-XXXXXX"; + MqttBrokerPersistHooks hooks; + char* root; + int rc; + + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + + rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (rc == MQTT_CODE_SUCCESS) { + MqttBrokerNet_PersistPosix_Free(&hooks); + } + ASSERT_EQ(0, rmdir(root)); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +} +#endif + +static int persist_test_init_root_mode(mode_t mode) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBrokerPersistHooks hooks; + char* root; + int rc; + + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + if (root == NULL) { + return MQTT_CODE_ERROR_SYSTEM; + } + if (chmod(root, mode) != 0) { + (void)rmdir(root); + return MQTT_CODE_ERROR_SYSTEM; + } + rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (rc == MQTT_CODE_SUCCESS) { + MqttBrokerNet_PersistPosix_Free(&hooks); + } + (void)rmdir(root); + return rc; +} + +TEST(persist_root_readable_permissions_accepted) +{ + ASSERT_EQ(MQTT_CODE_SUCCESS, persist_test_init_root_mode(0755)); +} + +TEST(persist_root_writable_permissions_rejected) +{ + ASSERT_EQ(MQTT_CODE_ERROR_SYSTEM, persist_test_init_root_mode(0777)); +} + +#ifndef WOLFMQTT_STATIC_MEMORY +/* Remove root// left by the POSIX backend. */ +static void persist_remove_tree(const char* root) +{ + char dir_path[640]; + char rec_path[1024]; + struct dirent* ent; + struct dirent* sub; + DIR* d; + DIR* sd; + + d = opendir(root); + if (d == NULL) { + return; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') { + continue; + } + if (snprintf(dir_path, sizeof(dir_path), "%s/%s", root, + ent->d_name) <= 0) { + continue; + } + sd = opendir(dir_path); + if (sd == NULL) { + (void)unlink(dir_path); + continue; + } + while ((sub = readdir(sd)) != NULL) { + if (sub->d_name[0] != '.' && + snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, + sub->d_name) > 0) { + (void)unlink(rec_path); + } + } + (void)closedir(sd); + (void)rmdir(dir_path); + } + (void)closedir(d); + (void)rmdir(root); +} + +/* Whether the captured freed allocation was all zero. */ +static int persist_captured_free_zeroed(void) +{ + size_t i; + + for (i = 0; i < g_capture_freed_len; i++) { + if (g_capture_freed[i] != 0) { + return 0; + } + } + return 1; +} + +/* A 40-character topic: its heap copy is 41 bytes, a size nothing else + * allocates during these restores, so the free capture sees only it. */ +#define PERSIST_SCRUB_TOPIC "secret/plant-7/valve-control/setpoint-01" + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int scrub_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* A 37-byte payload: an allocation size nothing else uses in these restores, + * so the targeted allocation failure hits the payload copy. */ +#define PERSIST_SCRUB_PAYLOAD_LEN 37 + +/* Persist one record through `put`, then restore it with the payload copy's + * allocation failing and the freed topic copy captured. Returns 1 if the + * topic copy was freed and all zero, 0 if it was freed with data left, -1 if + * the flow did not reach that point. */ +static int persist_restore_alloc_failure_scrub(int outq) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; +#ifdef WOLFMQTT_BROKER_RETAINED + BrokerRetainedMsg rm; +#endif + BrokerOutPub out; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[PERSIST_SCRUB_PAYLOAD_LEN]; + char* root; + int rc; + int result = -1; + + install_mock_net(&net); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(payload, 'p', sizeof(payload)); + root = mkdtemp(root_template); + if (root == NULL) { + return -1; + } + if (MqttBrokerNet_PersistPosix_Init(&hooks, root) != MQTT_CODE_SUCCESS) { + persist_remove_tree(root); + return -1; + } +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = scrub_derive_key; +#endif + + XMEMSET(&source, 0, sizeof(source)); + rc = MqttBroker_Init(&source, &net); + if (rc == MQTT_CODE_SUCCESS) { + rc = MqttBroker_SetPersistHooks(&source, &hooks); + } + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&source); + } + if (rc == MQTT_CODE_SUCCESS && outq) { + rc = BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1); + if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + } +#ifdef WOLFMQTT_BROKER_RETAINED + else if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&rm, 0, sizeof(rm)); + rm.topic = topic; + rm.payload = payload; + rm.payload_len = (word32)sizeof(payload); + rm.qos = MQTT_QOS_0; + rm.store_time = (WOLFMQTT_BROKER_TIME_T)1; + rc = BrokerPersist_PutRetained(&source, &rm); + } +#else + else { + rc = MQTT_CODE_ERROR_BAD_ARG; /* no retained store in this build */ + } +#endif + MqttBroker_Free(&source); + + if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + g_alloc_fail_size = sizeof(payload); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + (void)BrokerPersist_Restore(&restored); + if (g_capture_free_seen) { + result = persist_captured_free_zeroed(); + } + } + g_alloc_fail_size = 0; + broker_test_disable_free_capture(); + MqttBroker_Free(&restored); + } + MqttBrokerNet_PersistPosix_Free(&hooks); + persist_remove_tree(root); + return result; +} + +#ifdef WOLFMQTT_BROKER_RETAINED +/* When the payload copy cannot be allocated while restoring a retained + * message, the topic copy already made is wiped before it is freed. */ +TEST(persist_restore_retained_alloc_failure_scrubs_topic) +{ + ASSERT_EQ(1, persist_restore_alloc_failure_scrub(0)); +} +#endif + +/* The same for a queued outbound message of an offline Session. */ +TEST(persist_restore_outq_alloc_failure_scrubs_topic) +{ + ASSERT_EQ(1, persist_restore_alloc_failure_scrub(1)); +} + +#ifndef WOLFMQTT_BROKER_PERSIST_ENCRYPT +/* Drop the last cut bytes of the only record in root/ and lower the + * header's body length to match (record header: magic(4) version(2) kind(1) + * wrap(1) body_len(4, big-endian)), so the record stays self-consistent and + * only the payload length inside the body overruns. Returns 0 on success. */ +static int persist_truncate_only_record(const char* root, byte ns, + long cut) +{ + char dir_path[640]; + char rec_path[1024]; + byte hdr[12]; + struct stat st; + struct dirent* ent; + DIR* d; + FILE* f; + word32 body_len; + int rc = -1; + + if (snprintf(dir_path, sizeof(dir_path), "%s/%u", root, + (unsigned)ns) <= 0) { + return -1; + } + d = opendir(dir_path); + if (d == NULL) { + return -1; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] != '.') { + break; + } + } + if (ent != NULL && + snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, + ent->d_name) > 0 && + stat(rec_path, &st) == 0 && st.st_size > cut + (long)sizeof(hdr)) { + f = fopen(rec_path, "r+b"); + if (f != NULL) { + if (fread(hdr, 1, sizeof(hdr), f) == sizeof(hdr)) { + body_len = ((word32)hdr[8] << 24) | ((word32)hdr[9] << 16) | + ((word32)hdr[10] << 8) | (word32)hdr[11]; + body_len -= (word32)cut; + hdr[8] = (byte)(body_len >> 24); + hdr[9] = (byte)(body_len >> 16); + hdr[10] = (byte)(body_len >> 8); + hdr[11] = (byte)body_len; + if (fseek(f, 0, SEEK_SET) == 0 && + fwrite(hdr, 1, sizeof(hdr), f) == sizeof(hdr)) { + rc = 0; + } + } + if (fclose(f) != 0) { + rc = -1; + } + } + if (rc == 0 && truncate(rec_path, st.st_size - cut) != 0) { + rc = -1; + } + } + (void)closedir(d); + return rc; } -TEST(persist_parent_component_rejected_as_bad_argument) +#ifdef WOLFMQTT_BROKER_RETAINED +/* Restore copies a persisted topic to the heap before it reads the payload. + * When the record turns out to be malformed the copy is freed; topic strings + * are application data the broker scrubs before freeing elsewhere, so it must + * be wiped first. The record is cut short on disk so its declared payload + * overruns the file. */ +TEST(persist_restore_malformed_retained_scrubs_topic) { + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; MqttBrokerPersistHooks hooks; - int rc; + BrokerRetainedMsg rm; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[8] = { 'p', 'a', 'y', 'l', 'o', 'a', 'd', '!' }; + char* root; + int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int cut_rc = -1; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int seen = 0; + int zeroed = 0; + int retained_empty = 0; + install_mock_net(&net); XMEMSET(&hooks, 0, sizeof(hooks)); - rc = MqttBrokerNet_PersistPosix_Init(&hooks, - "../wolfmqtt-persist-invalid"); - if (rc == MQTT_CODE_SUCCESS) { + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&rm, 0, sizeof(rm)); + rm.topic = topic; + rm.payload = payload; + rm.payload_len = (word32)sizeof(payload); + rm.qos = MQTT_QOS_0; + rm.store_time = (WOLFMQTT_BROKER_TIME_T)1; + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutRetained(&source, &rm); + } + MqttBroker_Free(&source); + cut_rc = persist_truncate_only_record(root, + BROKER_PERSIST_NS_RETAINED, (long)sizeof(payload)); + + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + retained_empty = (restored.retained == NULL); + MqttBroker_Free(&restored); MqttBrokerNet_PersistPosix_Free(&hooks); } - ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + persist_remove_tree(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(0, cut_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, restore_rc); + ASSERT_TRUE(retained_empty); + ASSERT_TRUE(seen); + ASSERT_TRUE(zeroed); } -TEST(persist_iter_skips_fifo_without_blocking) +#endif /* WOLFMQTT_BROKER_RETAINED */ + +/* The same for a queued outbound message of an offline Session. */ +TEST(persist_restore_malformed_outq_scrubs_topic) { char root_template[] = "./wolfmqtt-persist-XXXXXX"; - char ns_path[640]; - char fifo_path[680]; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; MqttBrokerPersistHooks hooks; - PersistIterCapture capture; + BrokerOutPub out; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[8] = { 'p', 'a', 'y', 'l', 'o', 'a', 'd', '!' }; char* root; - pid_t child; - pid_t waited; - int status = 0; - int paths_ok = 0; int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int cut_rc = -1; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int seen = 0; + int zeroed = 0; + int queue_len = -1; + install_mock_net(&net); XMEMSET(&hooks, 0, sizeof(hooks)); - XMEMSET(&capture, 0, sizeof(capture)); root = mkdtemp(root_template); ASSERT_NOT_NULL(root); - if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && - snprintf(fifo_path, sizeof(fifo_path), "%s/1234.bin", - ns_path) > 0) { - paths_ok = 1; + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS && + BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + MqttBroker_Free(&source); + cut_rc = persist_truncate_only_record(root, BROKER_PERSIST_NS_OUTQ, + (long)sizeof(payload)); + + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + if (restored.orphan_sessions != NULL) { + queue_len = restored.orphan_sessions->out_q_count; + } + MqttBroker_Free(&restored); + MqttBrokerNet_PersistPosix_Free(&hooks); } - ASSERT_TRUE(paths_ok); - ASSERT_EQ(0, mkdir(ns_path, 0700)); - ASSERT_EQ(0, mkfifo(fifo_path, 0600)); + persist_remove_tree(root); - init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); - child = fork(); - ASSERT_TRUE(child >= 0); - if (child == 0) { - int iter_rc; + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(0, cut_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, restore_rc); + ASSERT_EQ(0, queue_len); + ASSERT_TRUE(seen); + ASSERT_TRUE(zeroed); +} - (void)alarm(2); - iter_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, &capture); - _exit((iter_rc == MQTT_CODE_SUCCESS && capture.calls == 0) ? 0 : 1); +#ifdef WOLFMQTT_V5 +/* Ways to corrupt a schema 6 NS_OUTQ record body (layout in + * BrokerPersist_PutOutPub): flags(1) at offset 5, topic_len(2) at 28, then + * topic, payload_len(4), payload, props_len(4) and props. */ +enum { + OUTQ_MUT_RESERVED_FLAG, /* set reserved flags bit 1 */ + OUTQ_MUT_EXPIRY_FLAG, /* set has_expiry with no expiry property */ + OUTQ_MUT_PROPS_LEN_LONG, /* props_len one more than the bytes left */ + OUTQ_MUT_PROPS_LEN_SHORT,/* props_len one less than the bytes left */ + OUTQ_MUT_BAD_PROP_ID, /* property identifier 0x7F is not defined */ + OUTQ_MUT_PROP_TRAILING /* a stray byte after the last property */ +}; + +static word32 persist_rd32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; +} + +static void persist_wr32(byte* p, word32 v) +{ + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; +} + +/* Rewrite the only record in root/ with one corruption. The record + * header is magic(4) version(2) kind(1) wrap(1) body_len(4). Returns 0. */ +static int persist_mutate_only_outq(const char* root, int kind) +{ + char dir_path[640]; + char rec_path[1024]; + byte rec[512]; + struct dirent* ent; + DIR* d; + FILE* f; + size_t len = 0; + size_t body = 12; + size_t props_len_off; + word32 topic_len; + word32 payload_len; + word32 props_len; + int rc = -1; + + if (snprintf(dir_path, sizeof(dir_path), "%s/%u", root, + (unsigned)BROKER_PERSIST_NS_OUTQ) <= 0) { + return -1; + } + d = opendir(dir_path); + if (d == NULL) { + return -1; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] != '.') { + break; + } + } + if (ent != NULL && snprintf(rec_path, sizeof(rec_path), "%s/%s", + dir_path, ent->d_name) > 0) { + f = fopen(rec_path, "rb"); + if (f != NULL) { + len = fread(rec, 1, sizeof(rec) - 1, f); + if (fclose(f) == 0 && len > body + 30) { + rc = 0; + } + } + } + (void)closedir(d); + if (rc != 0) { + return -1; } - waited = waitpid(child, &status, 0); - MqttBrokerNet_PersistPosix_Free(&hooks); - (void)unlink(fifo_path); - (void)rmdir(ns_path); - (void)rmdir(root); + topic_len = ((word32)rec[body + 28] << 8) | rec[body + 29]; + payload_len = persist_rd32(&rec[body + 30 + topic_len]); + props_len_off = body + 34 + topic_len + payload_len; + if (props_len_off + 4 > len) { + return -1; + } + props_len = persist_rd32(&rec[props_len_off]); + switch (kind) { + case OUTQ_MUT_RESERVED_FLAG: + rec[body + 5] |= 0x02; + break; + case OUTQ_MUT_EXPIRY_FLAG: + rec[body + 5] |= 0x01; + break; + case OUTQ_MUT_PROPS_LEN_LONG: + persist_wr32(&rec[props_len_off], props_len + 1); + break; + case OUTQ_MUT_PROPS_LEN_SHORT: + persist_wr32(&rec[props_len_off], props_len - 1); + break; + case OUTQ_MUT_BAD_PROP_ID: + rec[props_len_off + 4] = 0x7F; + break; + case OUTQ_MUT_PROP_TRAILING: + /* 0x01 starts a Payload Format Indicator with no value byte. */ + rec[len++] = 0x01; + persist_wr32(&rec[props_len_off], props_len + 1); + persist_wr32(&rec[8], persist_rd32(&rec[8]) + 1); + break; + default: + return -1; + } - ASSERT_EQ((int)child, (int)waited); - ASSERT_TRUE(WIFEXITED(status)); - ASSERT_EQ(0, WEXITSTATUS(status)); + rc = -1; + f = fopen(rec_path, "wb"); + if (f != NULL) { + if (fwrite(rec, 1, len, f) == len) { + rc = 0; + } + if (fclose(f) != 0) { + rc = -1; + } + } + return rc; } -#ifdef __MACH__ -/* macOS exposes /tmp as a trusted operating-system symlink to /private/tmp. - * A persistence root below that standard alias must initialize without - * weakening the no-follow checks on namespaces or record files. */ -TEST(persist_root_below_macos_tmp_alias_accepted) +/* A 53-byte payload, a size nothing else allocates during these restores. */ +#define PERSIST_SCRUB_PAYLOAD \ + "payload-secret-0123456789-0123456789-0123456789-01234" + +/* Persist a v5 QoS 1 OUTQ record carrying a Payload Format Indicator, + * corrupt it with `kind`, and restore it. The record must be skipped without + * failing the restore. capture_size picks the heap copy whose free must be + * scrubbed (0 when the record is rejected before any copy is made). Returns + * 1 when all of that holds. */ +static int persist_restore_mutated_outq(int kind, size_t capture_size) { - char root_template[] = "/tmp/wolfmqtt-persist-XXXXXX"; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; MqttBrokerPersistHooks hooks; + BrokerOutPub out; + MqttProp prop; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[] = PERSIST_SCRUB_PAYLOAD; char* root; - int rc; + int ok = 0; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int queue_len = -1; + int seen = 0; + int zeroed = 0; + install_mock_net(&net); XMEMSET(&hooks, 0, sizeof(hooks)); root = mkdtemp(root_template); - ASSERT_NOT_NULL(root); + if (root == NULL) { + return 0; + } + if (MqttBrokerNet_PersistPosix_Init(&hooks, root) == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&out, 0, sizeof(out)); + XMEMSET(&prop, 0, sizeof(prop)); + prop.type = MQTT_PROP_PAYLOAD_FORMAT_IND; + prop.data_byte = 1; + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + out.props = ∝ + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS && + BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_5, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + MqttBroker_Free(&source); - rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); - if (rc == MQTT_CODE_SUCCESS) { + if (put_rc == MQTT_CODE_SUCCESS && + persist_mutate_only_outq(root, kind) == 0) { + XMEMSET(&restored, 0, sizeof(restored)); + if (capture_size != 0) { + broker_test_capture_free(capture_size); + } + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + if (restored.orphan_sessions != NULL) { + queue_len = restored.orphan_sessions->out_q_count; + } + MqttBroker_Free(&restored); + ok = (restore_rc == MQTT_CODE_SUCCESS && queue_len == 0 && + (capture_size == 0 || (seen && zeroed))); + } MqttBrokerNet_PersistPosix_Free(&hooks); } - ASSERT_EQ(0, rmdir(root)); - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + persist_remove_tree(root); + return ok; } -#endif -static int persist_test_init_root_mode(mode_t mode) +/* Schema 6 restore rejects each malformed OUTQ record on its own, skipping + * it instead of failing the whole restore, and wipes any topic or payload + * copy it already made. */ +TEST(persist_restore_rejects_malformed_outq_props) +{ + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_RESERVED_FLAG, 0)); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROPS_LEN_LONG, + sizeof(PERSIST_SCRUB_TOPIC))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROPS_LEN_SHORT, + sizeof(PERSIST_SCRUB_TOPIC))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_BAD_PROP_ID, + sizeof(PERSIST_SCRUB_PAYLOAD))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROP_TRAILING, + sizeof(PERSIST_SCRUB_PAYLOAD))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_EXPIRY_FLAG, + sizeof(PERSIST_SCRUB_PAYLOAD))); +} +#endif /* WOLFMQTT_V5 */ +#endif /* !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ +#endif /* !WOLFMQTT_STATIC_MEMORY */ + +/* The POSIX backend names a record ".bin" and writes it through + * ".bin.tmp", so a key can be at most (NAME_MAX - 8) / 2 bytes. A + * longer key must be refused up front as MQTT_CODE_ERROR_OUT_OF_BUFFER, the + * backend's documented "key too long" result, not surface later as a generic + * system error from the filesystem. */ +TEST(persist_key_longer_than_name_limit_rejected) { + byte key[2 * 256]; + static const byte value[] = "v"; char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; MqttBrokerPersistHooks hooks; + byte out[8]; + word32 out_len; char* root; - int rc; + int max_len = (NAME_MAX - 8) / 2; + int init_rc; + int fit_put_rc = MQTT_CODE_ERROR_SYSTEM; + int fit_del_rc = MQTT_CODE_ERROR_SYSTEM; + int long_put_rc = MQTT_CODE_SUCCESS; + int long_get_rc = MQTT_CODE_SUCCESS; + int long_del_rc = MQTT_CODE_SUCCESS; + int path_ok; XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(key, 'k', sizeof(key)); + ASSERT_TRUE(max_len + 1 <= (int)sizeof(key)); root = mkdtemp(root_template); - if (root == NULL) { - return MQTT_CODE_ERROR_SYSTEM; - } - if (chmod(root, mode) != 0) { - (void)rmdir(root); - return MQTT_CODE_ERROR_SYSTEM; - } - rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); - if (rc == MQTT_CODE_SUCCESS) { + ASSERT_NOT_NULL(root); + path_ok = (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0); + ASSERT_TRUE(path_ok); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + fit_put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)max_len, + value, (word32)sizeof(value)); + fit_del_rc = hooks.kv_del(hooks.ctx, 7, key, (word16)max_len); + long_put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)(max_len + 1), + value, (word32)sizeof(value)); + out_len = (word32)sizeof(out); + long_get_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)(max_len + 1), + out, &out_len); + long_del_rc = hooks.kv_del(hooks.ctx, 7, key, (word16)(max_len + 1)); MqttBrokerNet_PersistPosix_Free(&hooks); } + (void)rmdir(ns_path); (void)rmdir(root); - return rc; -} -TEST(persist_root_readable_permissions_accepted) -{ - ASSERT_EQ(MQTT_CODE_SUCCESS, persist_test_init_root_mode(0755)); + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, fit_put_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, fit_del_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_put_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_get_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_del_rc); } -TEST(persist_root_writable_permissions_rejected) +/* kv_get fills at most *inout_len bytes. A record larger than that must be + * reported as MQTT_CODE_ERROR_OUT_OF_BUFFER rather than returned truncated as + * if it were the whole record; the META check relies on that result to treat + * an oversized record as a schema mismatch. */ +TEST(persist_get_reports_record_larger_than_buffer) { - ASSERT_EQ(MQTT_CODE_ERROR_SYSTEM, persist_test_init_root_mode(0777)); + static const byte key[] = { 0x9a }; + static const byte value[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + char rec_path[680]; + MqttBrokerPersistHooks hooks; + byte out[sizeof(value)]; + word32 out_len; + char* root; + int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int short_rc = MQTT_CODE_SUCCESS; + int exact_rc = MQTT_CODE_ERROR_SYSTEM; + word32 exact_len = 0; + int paths_ok = 0; + + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && + snprintf(rec_path, sizeof(rec_path), "%s/9a.bin", + ns_path) > 0) { + paths_ok = 1; + } + ASSERT_TRUE(paths_ok); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)sizeof(key), + value, (word32)sizeof(value)); + out_len = 4; + short_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)sizeof(key), + out, &out_len); + out_len = (word32)sizeof(out); + exact_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)sizeof(key), + out, &out_len); + exact_len = out_len; + MqttBrokerNet_PersistPosix_Free(&hooks); + } + + (void)unlink(rec_path); + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, short_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, exact_rc); + ASSERT_EQ((word32)sizeof(value), exact_len); + ASSERT_EQ(0, XMEMCMP(out, value, sizeof(value))); } /* A rename or unlink is not durable until its namespace directory is @@ -12590,6 +16532,16 @@ int main(int argc, char** argv) #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) RUN_TEST(connect_v5_oversize_will_payload_emits_connack); #ifdef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_oversize_will_props_emits_connack); + #if BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5 < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(connect_v5_static_will_exceeding_offline_slot_refused); + #endif + #if BROKER_MAX_STATIC_OFFLINE_DATA_LEN < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(connect_v5_static_payload_only_will_exceeding_offline_slot_refused); + RUN_TEST(connect_v311_static_will_exceeding_offline_slot_refused); + #endif RUN_TEST(connect_v5_oversize_will_topic_emits_connack); #endif #endif @@ -12608,6 +16560,14 @@ int main(int argc, char** argv) RUN_TEST(connect_unsupported_level_3_refused); RUN_TEST(connect_unsupported_level_6_refused); RUN_TEST(connect_unsupported_level_127_refused); + RUN_TEST(connect_unsupported_level_undecodable_refused); +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_persistent_session_cap_refuses_new_session); + RUN_TEST(connect_persistent_session_cap_counts_live_sessions); +#endif +#ifndef WOLFMQTT_V5 + RUN_TEST(connect_v5_to_non_v5_broker_refused); +#endif #ifdef WOLFMQTT_BROKER_AUTH RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); RUN_TEST(connect_v311_binary_password_exact_match_accepted); @@ -12624,10 +16584,14 @@ int main(int argc, char** argv) RUN_TEST(broker_set_auth_pass_shorter_second_no_residue); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_credentials_scrubbed_after_accept); +#ifdef WOLFMQTT_NONBLOCK + RUN_TEST(connect_refused_deferred_connack_scrubs_credentials); +#endif #endif #endif #ifdef ENABLE_MQTT_TLS RUN_TEST(broker_start_partial_listener_failure_cleans_up); + RUN_TEST(broker_add_failure_closes_socket_once); #endif #ifdef WOLFMQTT_V5 RUN_TEST(connect_v5_emptyid_assigned_id_emitted); @@ -12651,6 +16615,7 @@ int main(int argc, char** argv) RUN_TEST(qos2_publish_with_offline_durable_subscriber); RUN_TEST(qos2_publish_then_abrupt_close_offline_subscriber); #ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(qos2_topic_alloc_failure_retransmit_delivered); #ifdef WOLFMQTT_V5 RUN_TEST(online_qos1_flood_disconnects_slow_v5_subscriber); #endif /* WOLFMQTT_V5 */ @@ -12716,9 +16681,70 @@ int main(int argc, char** argv) #endif #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) RUN_TEST(disconnect_v5_malformed_fires_will); + RUN_TEST(disconnect_v5_reason_0_discards_will); + RUN_TEST(disconnect_v5_reason_4_publishes_will); + RUN_TEST(disconnect_v5_reason_error_publishes_will); + RUN_TEST(takeover_v5_zero_will_delay_publishes_will); + RUN_TEST(takeover_v5_own_will_not_sent_after_disconnect); +#if !defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(takeover_v5_resumed_session_gets_queued_self_will); + RUN_TEST(takeover_v5_clean_start_frees_queued_self_will); +#endif +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(takeover_v5_own_will_partial_write_still_disconnects); +#endif + RUN_TEST(takeover_v5_will_delay_resumed_session_discards_will); + RUN_TEST(takeover_v5_clean_start_publishes_will); + RUN_TEST(takeover_v5_zero_session_expiry_publishes_will); #ifndef WOLFMQTT_DYN_PROP RUN_TEST(disconnect_v5_props_pool_exhausted_reason_0_discards_will); RUN_TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will); + RUN_TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will); +#endif +#endif +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(reclaim_keeps_explicit_v5_never_expire); + RUN_TEST(reclaim_v311_resume_never_expires); +#endif +#ifdef WOLFMQTT_V5 + RUN_TEST(live_forward_exceeding_tx_buf_not_sent_truncated); + RUN_TEST(retained_delivery_exceeding_tx_buf_not_sent_truncated); +#endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_BROKER_WILL) + RUN_TEST(will_qos0_exceeding_tx_buf_not_sent_truncated); + RUN_TEST(will_qos1_queued_for_offline_persistent_subscriber); +#endif +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) + RUN_TEST(will_props_forwarded_to_live_subscriber); + RUN_TEST(delayed_will_props_forwarded_to_live_subscriber); +#if WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(delayed_will_props_decoded_with_no_clients); +#endif +#ifndef WOLFMQTT_DYN_PROP + RUN_TEST(will_props_published_with_prop_pool_exhausted); +#endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + BROKER_WILL_PROPS_STACK_ENTRIES < WILL_TEST_MAX_USER_PROPS + RUN_TEST(will_props_beyond_stack_entries_forwarded); +#endif +#if WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(queued_will_expired_before_delivery_dropped); + RUN_TEST(queued_will_expiry_reduced_by_wait); + RUN_TEST(queued_will_zero_expiry_dropped); +#endif +#endif +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_BROKER_WILDCARDS) && \ + WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(will_offline_overlapping_subs_queue_max_qos_once); +#endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) + RUN_TEST(queued_publish_expired_before_delivery_dropped); + RUN_TEST(queued_publish_expiry_reduced_by_wait); + RUN_TEST(queued_publish_zero_expiry_dropped); + RUN_TEST(queued_publish_expired_behind_inflight_cap_dropped); + RUN_TEST(inflight_publish_resent_after_expiry); +#ifdef WOLFMQTT_BROKER_PERSIST + RUN_TEST(queued_publish_expired_deletes_persisted_record); #endif #endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) @@ -12821,6 +16847,16 @@ int main(int argc, char** argv) #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_v5_receive_max_zero_protocol_error); RUN_TEST(disconnect_v5_session_expiry_raise_sends_protocol_error); +#endif + RUN_TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect); + RUN_TEST(v5_second_connect_sends_protocol_error_disconnect); + RUN_TEST(v5_pingreq_with_body_sends_malformed_disconnect); + RUN_TEST(v5_unexpected_auth_sends_protocol_error_disconnect); +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(v5_error_close_waits_for_deferred_disconnect); +#endif +#if defined(WOLFMQTT_NONBLOCK) && defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(v5_static_error_close_leaves_no_partial_disconnect); #endif #endif #ifdef WOLFMQTT_V5 @@ -12925,12 +16961,20 @@ int main(int argc, char** argv) #endif RUN_TEST(persist_parent_component_rejected_as_bad_argument); RUN_TEST(persist_iter_skips_fifo_without_blocking); + RUN_TEST(persist_iter_keys_are_deletable); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(persist_restore_packet_id_wrap_preserves_fifo); + RUN_TEST(persist_restore_rejects_outq_packet_id_mismatch); + RUN_TEST(persist_subs_written_when_ack_write_fails); #ifdef WOLFMQTT_NONBLOCK + RUN_TEST(persist_subs_written_when_ack_deferred); RUN_TEST(persist_partial_publish_restart_keeps_dup); #endif RUN_TEST(persist_mixed_qos_queue_preserves_fifo); + #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(persist_restart_keeps_queued_will_props); + #endif RUN_TEST(orphan_reclaim_keeps_persisted_outq); #endif RUN_TEST(persist_stream_only_hooks_rejected); @@ -12942,6 +16986,23 @@ int main(int argc, char** argv) RUN_TEST(persist_root_writable_permissions_rejected); RUN_TEST(persist_put_rejects_existing_temp_symlink); RUN_TEST(persist_put_del_report_namespace_fsync_failure); + RUN_TEST(persist_get_reports_record_larger_than_buffer); + RUN_TEST(persist_key_longer_than_name_limit_rejected); +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(persist_restore_retained_alloc_failure_scrubs_topic); +#endif + RUN_TEST(persist_restore_outq_alloc_failure_scrubs_topic); +#ifndef WOLFMQTT_BROKER_PERSIST_ENCRYPT +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(persist_restore_malformed_retained_scrubs_topic); +#endif + RUN_TEST(persist_restore_malformed_outq_scrubs_topic); +#ifdef WOLFMQTT_V5 + RUN_TEST(persist_restore_rejects_malformed_outq_props); +#endif +#endif +#endif RUN_TEST(persist_iter_reports_unreadable_record); RUN_TEST(persist_iter_skips_socket_and_symlink_entries); #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT diff --git a/tests/test_broker_log.c b/tests/test_broker_log.c new file mode 100644 index 000000000..b803e8b57 --- /dev/null +++ b/tests/test_broker_log.c @@ -0,0 +1,255 @@ +/* Broker log sanitization tests. The broker is built with logging enabled and + * PRINTF redirected into a capture buffer, so a test can check what reaches + * the log sink. */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include +#include +#include + +#include "wolfmqtt/mqtt_client.h" +#include "wolfmqtt/mqtt_broker.h" + +/* The broker sources call these in every configuration. */ +static char g_log[4096]; +static size_t g_log_len; +static unsigned long g_time_s; + +void wolfmqtt_test_log(const char* fmt, ...) +{ + va_list ap; + int n; + + if (g_log_len >= sizeof(g_log) - 1) { + return; + } + va_start(ap, fmt); + n = vsnprintf(g_log + g_log_len, sizeof(g_log) - g_log_len, fmt, ap); + va_end(ap); + if (n > 0) { + g_log_len += (size_t)n; + if (g_log_len > sizeof(g_log) - 1) { + g_log_len = sizeof(g_log) - 1; + } + } +} + +unsigned long wolfmqtt_test_broker_time_s(void) +{ + return g_time_s; +} + +#if defined(WOLFMQTT_BROKER_PERSIST) && !defined(WOLFMQTT_STATIC_MEMORY) && \ + !defined(WOLFMQTT_BROKER_NO_LOG) + +#include +#include +#include + +static int net_listen(void* ctx, BROKER_SOCKET_T* sock, word16 port, + int backlog) +{ + (void)ctx; (void)sock; (void)port; (void)backlog; + return MQTT_CODE_SUCCESS; +} + +static int net_accept(void* ctx, BROKER_SOCKET_T listen_sock, + BROKER_SOCKET_T* client_sock) +{ + (void)ctx; (void)listen_sock; (void)client_sock; + return MQTT_CODE_CONTINUE; +} + +static int net_read(void* ctx, BROKER_SOCKET_T sock, byte* buf, int buf_len, + int timeout_ms) +{ + (void)ctx; (void)sock; (void)buf; (void)buf_len; (void)timeout_ms; + return MQTT_CODE_CONTINUE; +} + +static int net_write(void* ctx, BROKER_SOCKET_T sock, const byte* buf, + int buf_len, int timeout_ms) +{ + (void)ctx; (void)sock; (void)buf; (void)timeout_ms; + return buf_len; +} + +static int net_close(void* ctx, BROKER_SOCKET_T sock) +{ + (void)ctx; (void)sock; + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int test_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* Remove root// as laid out by the POSIX backend. */ +static void remove_store(const char* root) +{ + char path[512]; + char sub[1024]; + DIR* d; + DIR* sd; + struct dirent* e; + struct dirent* se; + + d = opendir(root); + if (d == NULL) { + return; + } + while ((e = readdir(d)) != NULL) { + if (e->d_name[0] == '.') { + continue; + } + (void)snprintf(path, sizeof(path), "%s/%s", root, e->d_name); + sd = opendir(path); + if (sd != NULL) { + while ((se = readdir(sd)) != NULL) { + if (se->d_name[0] == '.') { + continue; + } + (void)snprintf(sub, sizeof(sub), "%s/%s", path, se->d_name); + (void)unlink(sub); + } + (void)closedir(sd); + (void)rmdir(path); + } + else { + (void)unlink(path); + } + } + (void)closedir(d); + (void)rmdir(root); +} + +static int init_broker(MqttBroker* broker, MqttBrokerNet* net, + MqttBrokerPersistHooks* hooks) +{ + int rc; + + XMEMSET(broker, 0, sizeof(*broker)); + rc = MqttBroker_Init(broker, net); + if (rc == MQTT_CODE_SUCCESS) { + rc = MqttBroker_SetPersistHooks(broker, hooks); + } + return rc; +} + +/* A persisted Session whose expiry passed while the broker was down is + * logged when the restore sweep drops it. The Client Identifier is chosen by + * the remote client and may hold control bytes (MQTT 5.0 section 1.5.4 only + * forbids U+0000), so it must reach the log escaped: a raw CR/LF would forge + * log lines and a raw ESC would drive the operator's terminal. */ +static int test_restore_expired_session_logs_escaped_client_id(void) +{ + static const char cid[] = "x\x1b[31m\ny"; + char root_template[] = "./wolfmqtt-log-XXXXXX"; + MqttBroker first; + MqttBroker second; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + char* root; + int rc; + int fail = 0; + + XMEMSET(&net, 0, sizeof(net)); + net.listen = net_listen; + net.accept = net_accept; + net.read = net_read; + net.write = net_write; + net.close = net_close; + XMEMSET(&hooks, 0, sizeof(hooks)); + + root = mkdtemp(root_template); + if (root == NULL) { + fprintf(stderr, "mkdtemp failed\n"); + return 1; + } + rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "persist init returned %d\n", rc); + remove_store(root); + return 1; + } +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = test_derive_key; +#endif + + /* First run: stamp META and persist an orphan Session that expires one + * second after it was orphaned at time 1. */ + g_time_s = 1; + rc = init_broker(&first, &net, &hooks); + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&first); + } + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_PutOrphanSession(&first, cid, + MQTT_CONNECT_PROTOCOL_LEVEL_5, 1, (WOLFMQTT_BROKER_TIME_T)1); + } + MqttBroker_Free(&first); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "first broker setup returned %d\n", rc); + fail = 1; + } + + /* Restart well after the expiry. */ + if (!fail) { + g_time_s = 100; + rc = init_broker(&second, &net, &hooks); + g_log_len = 0; + g_log[0] = '\0'; + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&second); + } + MqttBroker_Free(&second); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "restore returned %d\n", rc); + fail = 1; + } + } + + if (!fail) { + if (strstr(g_log, "expired session") == NULL) { + fprintf(stderr, "expiry not logged: %s\n", g_log); + fail = 1; + } + else if (strchr(g_log, 0x1b) != NULL || + strchr(g_log, '\n') != NULL) { + fprintf(stderr, "raw control bytes reached the log\n"); + fail = 1; + } + else if (strstr(g_log, "client_id=x\\e[31m\\ny") == NULL) { + fprintf(stderr, "client_id not escaped as expected: %s\n", + g_log); + fail = 1; + } + } + + MqttBrokerNet_PersistPosix_Free(&hooks); + remove_store(root); + return fail; +} + +int main(void) +{ + return test_restore_expired_session_logs_escaped_client_id(); +} + +#else + +int main(void) +{ + /* Needs the dynamic-memory persistence restore with logging enabled. */ + return 77; +} + +#endif diff --git a/tests/test_broker_log_config.h b/tests/test_broker_log_config.h new file mode 100644 index 000000000..00c2a8933 --- /dev/null +++ b/tests/test_broker_log_config.h @@ -0,0 +1,17 @@ +/* Prototypes for the hooks test_broker_log builds the broker against: PRINTF + * is redirected to wolfmqtt_test_log and the broker clock to + * wolfmqtt_test_broker_time_s. */ +#ifndef WOLFMQTT_TEST_BROKER_LOG_CONFIG_H +#define WOLFMQTT_TEST_BROKER_LOG_CONFIG_H + +/* Declared printf-like so a non-literal fmt can be passed on to vsnprintf + * (clang -Wformat-nonliteral) and the broker's PRINTF calls are checked. */ +#if defined(__GNUC__) || defined(__clang__) +void wolfmqtt_test_log(const char* fmt, ...) + __attribute__((format(printf, 1, 2))); +#else +void wolfmqtt_test_log(const char* fmt, ...); +#endif +unsigned long wolfmqtt_test_broker_time_s(void); + +#endif diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index bfa74e224..1aa8af144 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -309,6 +309,60 @@ TEST(deinit_after_init) ASSERT_TRUE(1); } +TEST(net_disconnect_wait_null_client) +{ + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, MqttClient_NetDisconnect_Wait(NULL)); +} + +/* Without other threads the waiting disconnect closes the transport like + * MqttClient_NetDisconnect. */ +TEST(net_disconnect_wait_closes_transport) +{ + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + + rc = MqttClient_NetDisconnect_Wait(&test_client); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + (MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT))); +} + +/* tx_buf and rx_buf belong to the caller, and the bundled examples free them + * before calling MqttClient_DeInit, so DeInit must not write to them: doing + * so would write into freed heap memory. Marker bytes must survive DeInit. */ +TEST(deinit_leaves_caller_buffers_untouched) +{ + int rc; + int i; + int tx_kept = 1; + int rx_kept = 1; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + XMEMSET(test_tx_buf, 0x5A, sizeof(test_tx_buf)); + XMEMSET(test_rx_buf, 0x5A, sizeof(test_rx_buf)); + + MqttClient_DeInit(&test_client); + test_client_inited = 0; + + for (i = 0; i < (int)sizeof(test_tx_buf); i++) { + if (test_tx_buf[i] != 0x5A) { + tx_kept = 0; + } + } + for (i = 0; i < (int)sizeof(test_rx_buf); i++) { + if (test_rx_buf[i] != 0x5A) { + rx_kept = 0; + } + } + ASSERT_TRUE(tx_kept); + ASSERT_TRUE(rx_kept); +} + /* ============================================================================ * MqttClient_Connect Tests * ============================================================================ */ @@ -1143,7 +1197,8 @@ TEST(auth_without_connect_method_rejected) * Authentication Method property. Exercises MqttConnect_HasAuthMethod and its * per-connection assignment in MqttClient_Connect rather than poking * client->auth_method_set directly. */ -static int run_connect_v5_with_auth_method(int with_method) +static int run_connect_v5_with_auth_method_write(int with_method, + MqttNetWriteCb write_cb) { int rc; int i; @@ -1152,7 +1207,7 @@ static int run_connect_v5_with_auth_method(int with_method) /* v5 CONNACK: accepted, Session Present = 0, no properties. */ static const byte connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; - test_net.write = mock_net_write_accept; + test_net.write = write_cb; test_net.read = mock_net_read_canned; XMEMCPY(g_canned_buf, connack, sizeof(connack)); g_canned_len = (int)sizeof(connack); @@ -1177,6 +1232,12 @@ static int run_connect_v5_with_auth_method(int with_method) return rc; } +static int run_connect_v5_with_auth_method(int with_method) +{ + return run_connect_v5_with_auth_method_write(with_method, + mock_net_write_accept); +} + /* Positive control: after a CONNECT that did carry an Authentication Method, * MqttClient_Auth must not be blocked by the new guard and the AUTH must reach * the wire. Guards against the guard over-rejecting legitimate re-auth. The @@ -1248,6 +1309,258 @@ TEST(auth_with_connect_method_allowed) MqttClient_PropsFree(auth.props); } +#ifdef WOLFMQTT_NONBLOCK +/* Start a v5 CONNECT that carries an Authentication Method and return once it + * is on the wire but no CONNACK has arrived (the canned reader has nothing to + * deliver yet). */ +static int start_connect_v5_auth_pending(MqttConnect* connect, + MqttProp* method_prop) +{ + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + g_canned_len = 0; + g_canned_pos = 0; + + XMEMSET(connect, 0, sizeof(*connect)); + connect->keep_alive_sec = 60; + connect->clean_session = 1; + connect->client_id = "test_client"; + XMEMSET(method_prop, 0, sizeof(*method_prop)); + method_prop->type = MQTT_PROP_AUTH_METHOD; + method_prop->data_str.str = (char*)"SCRAM-SHA-256"; + method_prop->data_str.len = (word16)XSTRLEN("SCRAM-SHA-256"); + connect->props = method_prop; + + return MqttClient_Connect(&test_client, connect); +} + +/* [MQTT-3.1.2-30] After a CONNECT with an Authentication Method the Client + * MUST NOT send any packet other than AUTH or DISCONNECT until it receives + * CONNACK. A nonblocking MqttClient_Connect returns to the caller in that + * window, so PUBLISH, SUBSCRIBE and PINGREQ must be refused there, and + * allowed again once the CONNACK has been processed. */ +TEST(connect_auth_pending_blocks_other_packets) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + MqttPublish publish; + MqttSubscribe subscribe; + MqttTopic topic; + MqttPing ping; + /* v5 CONNACK: accepted, Session Present = 0, no properties. */ + static const byte connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + ASSERT_NE(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_CONNECT_SENT)); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(&topic, 0, sizeof(topic)); + topic.topic_filter = "t"; + subscribe.packet_id = 1; + subscribe.topic_count = 1; + subscribe.topics = &topic; + rc = MqttClient_Subscribe(&test_client, &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + XMEMSET(&ping, 0, sizeof(ping)); + rc = MqttClient_Ping_ex(&test_client, &ping); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + ASSERT_EQ(0, g_frames_written); + + /* The CONNACK arrives: the handshake completes and PUBLISH is allowed. */ + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + 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); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} + +/* DISCONNECT stays allowed while the authentication exchange is pending + * [MQTT-3.1.2-30]. */ +TEST(connect_auth_pending_allows_disconnect) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + g_frames_written = 0; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Disconnect(&test_client); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} + +/* UNSUBSCRIBE is refused in the [MQTT-3.1.2-30] window like PUBLISH, and the + * automatic keep-alive does not send a PINGREQ there even when one is due. */ +TEST(connect_auth_pending_blocks_unsubscribe_and_keepalive) +{ + int rc; + MqttConnect connect; + MqttProp method_prop; + MqttUnsubscribe unsub; + MqttTopic topic; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + g_frames_written = 0; + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(&topic, 0, sizeof(topic)); + topic.topic_filter = "t"; + unsub.packet_id = 2; + unsub.topic_count = 1; + unsub.topics = &topic; + rc = MqttClient_Unsubscribe(&test_client, &unsub); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + /* A keep-alive ping is due: nothing has been sent since time 0. */ + test_client.keep_alive_sec = 60; + test_client.last_tx_time = 0; + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(0, g_frames_written); +} + +/* AUTH stays allowed in the window [MQTT-3.1.2-30]: it is how the exchange + * proceeds. */ +TEST(connect_auth_pending_allows_auth) +{ + int rc; + MqttConnect connect; + MqttProp method_prop; + MqttAuth auth; + MqttProp* prop; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + XMEMSET(&auth, 0, sizeof(auth)); + auth.reason_code = MQTT_REASON_CONT_AUTH; + prop = MqttClient_PropsAdd(&auth.props); + ASSERT_NOT_NULL(prop); + prop->type = MQTT_PROP_AUTH_METHOD; + prop->data_str.str = (char*)"SCRAM-SHA-256"; + prop->data_str.len = (word16)XSTRLEN("SCRAM-SHA-256"); + + g_frames_written = 0; + rc = MqttClient_Auth(&test_client, &auth); + ASSERT_NE(MQTT_CODE_ERROR_STAT, rc); + ASSERT_TRUE(g_frames_written > 0); + + MqttClient_PropsFree(auth.props); +} + +/* The window belongs to its Network Connection: after one that never got its + * CONNACK is closed, a new connection whose CONNECT has no Authentication + * Method is not held back by it. */ +TEST(connect_auth_pending_cleared_by_reconnect) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + MqttPublish publish; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_NetDisconnect(&test_client)); + rc = run_connect_v5_with_auth_method(0); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} +#endif /* WOLFMQTT_NONBLOCK */ + +static int g_auth_pending_at_connect_write; + +static int mock_net_write_note_auth_pending(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + if (buf != NULL && buf_len > 0 && + ((buf[0] >> 4) & 0x0F) == MQTT_PACKET_TYPE_CONNECT) { + g_auth_pending_at_connect_write = + (test_client.flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0; + } + return mock_net_write_accept(context, buf, buf_len, timeout_ms); +} + +/* [MQTT-3.1.2-30] The window must already be closed to other senders when + * the CONNECT reaches the peer; raising it only after the write would let + * another thread send in between, or leave it stuck if a reader processed the + * CONNACK first. The write callback records the flag as the CONNECT goes out, + * and the CONNACK then clears it. */ +TEST(connect_auth_pending_raised_before_connect_write) +{ + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + g_auth_pending_at_connect_write = 0; + + rc = run_connect_v5_with_auth_method_write(1, + mock_net_write_note_auth_pending); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(1, g_auth_pending_at_connect_write); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_AUTH_PENDING)); +} + /* MQTT 3.1.1 section 2.2.1 lists packet type 15 as Reserved/Forbidden; AUTH * exists only from v5. A session negotiated at protocol level 4 must treat a * received type-15 packet as a protocol violation [MQTT-4.8.0-1] even in a @@ -1470,10 +1783,10 @@ TEST(connect_refused_connack_preserves_v5_defaults) int rc; int i; MqttConnect connect; - /* Same properties as the acceptance test, but return_code=0x05 - * (not authorized). */ + /* Same properties as the acceptance test, but reason code 0x87 (Not + * authorized, MQTT 5.0 section 3.2.2.2). */ static const byte connack[] = { - 0x20, 0x0C, 0x00, 0x05, 0x09, + 0x20, 0x0C, 0x00, 0x87, 0x09, 0x24, 0x01, 0x25, 0x00, 0x27, 0x00, 0x00, 0x04, 0x00 @@ -1500,7 +1813,7 @@ TEST(connect_refused_connack_preserves_v5_defaults) } ASSERT_EQ(MQTT_CODE_ERROR_CONNECT_REFUSED, rc); - ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH, connect.ack.return_code); + ASSERT_EQ(MQTT_REASON_NOT_AUTHORIZED, connect.ack.return_code); /* Pre-CONNACK defaults established by MqttClient_Connect must be intact. */ ASSERT_EQ((byte)WOLFMQTT_MAX_QOS, test_client.max_qos); ASSERT_EQ(1, test_client.retain_avail); @@ -2457,37 +2770,221 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) subscribe.topics[0].return_code); } -#ifdef WOLFMQTT_V5 -/* A v5 UNSUBACK whose per-topic reason code has the high bit set means the - * broker refused the unsubscribe; MqttClient_Unsubscribe must surface that as - * MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED rather than success. */ -TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected) +/* MqttClient_Connect resets its ack wait state so one object can be reused + * across exchanges; Subscribe, Unsubscribe and Publish leave their ack wait + * state at PAYLOAD after a completed exchange. A second request on the same + * completed object must read its own ack from the network instead of + * re-dispatching what the first exchange left in rx_buf. Both acks are queued + * up front (v3.1.1 wire forms, MQTT 3.1.1 sections 3.4, 3.9 and 3.11); the + * second must be consumed and must be the one reported. */ +static void reuse_setup(const byte* acks, int acks_len) { int rc; - int i; - MqttUnsubscribe unsub; - MqttTopic topics[1]; - /* v5 UNSUBACK: type=0xB0, remain=4, packet_id=43, props_len=0, - * reason=0x87 (NOT_AUTHORIZED). */ - static const byte unsuback[] = { 0xB0, 0x04, 0x00, 0x2B, 0x00, 0x87 }; rc = test_init_client(); ASSERT_EQ(MQTT_CODE_SUCCESS, rc); test_client_connect_sent(); - test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; - +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif test_net.write = mock_net_write_accept; test_net.read = mock_net_read_canned; - XMEMCPY(g_canned_buf, unsuback, sizeof(unsuback)); - g_canned_len = (int)sizeof(unsuback); + XMEMCPY(g_canned_buf, acks, (size_t)acks_len); + g_canned_len = acks_len; g_canned_pos = 0; +} - XMEMSET(&unsub, 0, sizeof(unsub)); +TEST(subscribe_reused_object_reads_new_suback) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[1]; + /* SUBACK id 1 granting QoS 0, then SUBACK id 2 granting QoS 1. */ + static const byte acks[] = { + 0x90, 0x03, 0x00, 0x01, 0x00, + 0x90, 0x03, 0x00, 0x02, 0x01 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&subscribe, 0, sizeof(subscribe)); XMEMSET(topics, 0, sizeof(topics)); topics[0].topic_filter = "test/topic"; - unsub.packet_id = 43; - unsub.topic_count = 1; - unsub.topics = topics; + topics[0].qos = MQTT_QOS_1; + subscribe.topic_count = 1; + subscribe.topics = topics; + + subscribe.packet_id = 1; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(0x00, topics[0].return_code); + + subscribe.packet_id = 2; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(2, subscribe.ack.packet_id); + ASSERT_EQ(0x01, topics[0].return_code); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + +TEST(unsubscribe_reused_object_reads_new_unsuback) +{ + int rc; + int i; + MqttUnsubscribe unsub; + MqttTopic topics[1]; + /* UNSUBACK id 3, then UNSUBACK id 4. */ + static const byte acks[] = { + 0xB0, 0x02, 0x00, 0x03, + 0xB0, 0x02, 0x00, 0x04 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + unsub.topic_count = 1; + unsub.topics = topics; + + unsub.packet_id = 3; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + unsub.packet_id = 4; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, unsub.ack.packet_id); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + +TEST(publish_qos1_reused_object_reads_new_puback) +{ + int rc; + int i; + MqttPublish publish; + byte payload[1] = { 'x' }; + /* PUBACK id 5, then PUBACK id 6. */ + static const byte acks[] = { + 0x40, 0x02, 0x00, 0x05, + 0x40, 0x02, 0x00, 0x06 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)sizeof(payload); + + publish.packet_id = 5; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + publish.packet_id = 6; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(6, publish.resp.packet_id); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + +/* [MQTT-3.9.3-1] A SUBACK carries one return code per Topic Filter in the + * SUBSCRIBE. A count mismatch is a protocol violation, so the client must + * stop treating the connection as usable (MQTT 5.0 section 4.13.1), as + * MqttClient_Unsubscribe does for the matching UNSUBACK case. */ +TEST(subscribe_too_few_return_codes_clears_connected) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[2]; + /* SUBACK v3.1.1: type=0x90, remain=3, packet_id=46, one return code + * (granted QoS 0) for two requested filters. */ + static const byte suback[] = { 0x90, 0x03, 0x00, 0x2E, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED); + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, suback, sizeof(suback)); + g_canned_len = (int)sizeof(suback); + g_canned_pos = 0; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/one"; + topics[0].qos = MQTT_QOS_0; + topics[1].topic_filter = "test/two"; + topics[1].qos = MQTT_QOS_0; + subscribe.packet_id = 46; + subscribe.topic_count = 2; + subscribe.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +#ifdef WOLFMQTT_V5 +/* A v5 UNSUBACK whose per-topic reason code has the high bit set means the + * broker refused the unsubscribe; MqttClient_Unsubscribe must surface that as + * MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED rather than success. */ +TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected) +{ + int rc; + int i; + MqttUnsubscribe unsub; + MqttTopic topics[1]; + /* v5 UNSUBACK: type=0xB0, remain=4, packet_id=43, props_len=0, + * reason=0x87 (NOT_AUTHORIZED). */ + static const byte unsuback[] = { 0xB0, 0x04, 0x00, 0x2B, 0x00, 0x87 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); + 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, unsuback, sizeof(unsuback)); + g_canned_len = (int)sizeof(unsuback); + g_canned_pos = 0; + + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + unsub.packet_id = 43; + unsub.topic_count = 1; + unsub.topics = topics; rc = MQTT_CODE_CONTINUE; for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { @@ -3103,6 +3600,59 @@ TEST(wait_message_puback_survives_shared_ack_overwrite) } #endif /* WOLFMQTT_MULTITHREAD && WOLFMQTT_NONBLOCK && WOLFMQTT_MAX_QOS >= 1 */ +#if !defined(WOLFMQTT_NONBLOCK) && WOLFMQTT_MAX_QOS >= 1 +static int g_zero_write_calls; + +/* A blocking transport that accepts nothing, e.g. a send interrupted before + * any byte left. */ +static int mock_net_write_zero(void *context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; (void)buf; (void)buf_len; (void)timeout_ms; + g_zero_write_calls++; + return 0; +} + +static int mock_msg_cb_zero_write(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; (void)msg; (void)msg_new; (void)msg_done; + return MQTT_CODE_SUCCESS; +} + +/* A blocking PUBACK write that sends zero bytes did not acknowledge the + * PUBLISH [MQTT-4.3.2-2], so it must be reported as a network error instead + * of success. */ +TEST(wait_message_zero_byte_puback_write_is_error) +{ + int rc; + /* v3.1.1 QoS 1 PUBLISH: topic "a", packet id 1, payload "h". */ + static const byte publish_id1[] = { + 0x32, 0x06, 0x00, 0x01, 'a', 0x00, 0x01, 'h' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = mock_msg_cb_zero_write; + + g_zero_write_calls = 0; + test_net.write = mock_net_write_zero; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, publish_id1, sizeof(publish_id1)); + g_canned_len = (int)sizeof(publish_id1); + g_canned_pos = 0; + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + + ASSERT_EQ(1, g_zero_write_calls); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); +} +#endif /* !WOLFMQTT_NONBLOCK && WOLFMQTT_MAX_QOS >= 1 */ + /* MqttClient_WaitType dispatches on the stat of the object it is handed, which * for the CONNACK wait is mc_connect->ack. Left at MQTT_MSG_PAYLOAD after a * completed handshake, a second MqttClient_Connect on the same MqttConnect @@ -4238,6 +4788,106 @@ TEST(publish_refused_when_replay_pool_full_on_persistent_session) ASSERT_EQ(0, frames); } +/* Feed fixed v3.1.1 PUBREC frames (MQTT 3.1.1 section 3.5: 0x50, Remaining + * Length 2, Packet Identifier) through MqttClient_WaitMessage until all are + * consumed. */ +static int run_wait_pubrecs(const byte* frames, int frames_len) +{ + int rc = MQTT_CODE_SUCCESS; + int i; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, frames, (size_t)frames_len); + g_canned_len = frames_len; + g_canned_pos = 0; + for (i = 0; i < 40 && g_canned_pos < g_canned_len; i++) { + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + if (rc != MQTT_CODE_SUCCESS && rc != MQTT_CODE_CONTINUE) { + break; + } + } + return rc; +} + +/* The replay pool holds this client's own unacknowledged QoS > 0 PUBLISHes + * [MQTT-4.4.0-1]. A PUBREC naming an identifier the client never published + * is not part of its Session state (MQTT 5.0 section 3.6.2.1 calls it Packet + * Identifier not found), so it must not take a pool slot: a server sending + * one per slot would otherwise make every later QoS > 0 publish on this + * persistent Session fail for lack of room. */ +TEST(unsolicited_pubrec_does_not_fill_replay_pool) +{ + int rc; + int i; + MqttConnect connect; + MqttPublish publish; + byte pubrecs[4 * MQTT_MAX_REPLAY_MSGS]; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + ASSERT_EQ(MQTT_CODE_SUCCESS, run_initial_connect(&connect)); + + ASSERT_TRUE((int)sizeof(pubrecs) <= (int)sizeof(g_canned_buf)); + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + pubrecs[4 * i] = 0x50; + pubrecs[4 * i + 1] = 0x02; + pubrecs[4 * i + 2] = 0x02; + pubrecs[4 * i + 3] = (byte)(0x01 + i); /* ids 0x0201.. */ + } + rc = run_wait_pubrecs(pubrecs, (int)sizeof(pubrecs)); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(g_canned_len, g_canned_pos); + + init_qos_publish(&publish, MQTT_QOS_1, 0x0300, "t", payload, + (word32)(sizeof(payload) - 1)); + rc = run_publish_unacked(&publish); + /* Sent and left unacknowledged, not refused for lack of a slot. */ + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); +} + +/* A PUBREC is the QoS 2 response; for the identifier of an outstanding QoS 1 + * PUBLISH it does not advance that exchange. The QoS 1 message is still + * unacknowledged, so a resumed Session must re-send the PUBLISH + * [MQTT-4.4.0-1], not a PUBREL. */ +TEST(pubrec_for_qos1_id_keeps_publish_replay) +{ + int rc; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + /* PUBREC for Packet Identifier 0x0110, the QoS 1 publish below. */ + static const byte pubrec[] = { 0x50, 0x02, 0x01, 0x10 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + ASSERT_EQ(MQTT_CODE_SUCCESS, run_initial_connect(&connect)); + + init_qos_publish(&publish, MQTT_QOS_1, 0x0110, "sensor/temp", payload, + (word32)(sizeof(payload) - 1)); + rc = run_publish_unacked(&publish); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + + (void)run_wait_pubrecs(pubrec, (int)sizeof(pubrec)); + ASSERT_EQ(g_canned_len, g_canned_pos); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_NetDisconnect(&test_client)); + replay_mock_reset(); + rc = run_reconnect_with(&connect, 1, replay_mock_write); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + ASSERT_EQ(2, g_replay_frames); + ASSERT_EQ(MQTT_PACKET_TYPE_CONNECT, g_replay_types[0]); + ASSERT_EQ(MQTT_PACKET_TYPE_PUBLISH, g_replay_types[1]); +} + #ifdef WOLFMQTT_V5 /* MQTT 5.0 section 3.2.2.3.2: a Session Expiry Interval in CONNACK replaces * the one requested in CONNECT. When the server negotiates it down to 0 the @@ -4290,16 +4940,28 @@ TEST(publish_allowed_past_replay_pool_when_connack_ends_session) } #endif /* WOLFMQTT_V5 */ -/* A CleanSession 1 Session ends with the Network Connection, so nothing is - * ever replayed and a full pool must not limit how many messages are in - * flight. */ -TEST(publish_allowed_past_replay_pool_on_clean_session) +#if WOLFMQTT_MAX_QOS >= 2 +/* A QoS 2 PUBLISH can leave without a replay slot (here a CleanSession 1 + * connection sends it unretained while the pool is full). When its PUBREC + * arrives after a slot has freed, the PUBREL state must still be recorded + * for it [MQTT-4.4.0-1]: the client's own in-flight reservation awaiting + * PUBCOMP identifies it as this client's QoS 2 message. */ +TEST(pubrec_for_unslotted_qos2_records_pubrel) { int rc; int i; - int frames = -1; + int found = 0; MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + /* PUBACK for id 1 (MQTT 3.1.1 section 3.4), then PUBREC for id + * MQTT_MAX_REPLAY_MSGS + 1 (section 3.5). */ + byte acks[8] = { 0x40, 0x02, 0x00, 0x01, 0x50, 0x02, 0x00, 0x00 }; + word16 qos2_id = (word16)(MQTT_MAX_REPLAY_MSGS + 1); + + acks[6] = (byte)(qos2_id >> 8); + acks[7] = (byte)qos2_id; rc = test_init_client(); ASSERT_EQ(MQTT_CODE_SUCCESS, rc); @@ -4321,13 +4983,68 @@ TEST(publish_allowed_past_replay_pool_on_clean_session) } ASSERT_EQ(MQTT_CODE_SUCCESS, rc); - rc = run_publish_past_replay_pool(&frames); + /* Fill the pool with QoS 1 messages, then send the QoS 2 one. */ + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + init_qos_publish(&publish, MQTT_QOS_1, (word16)(i + 1), "t", + payload, (word32)(sizeof(payload) - 1)); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, run_publish_unacked(&publish)); + } + init_qos_publish(&publish, MQTT_QOS_2, qos2_id, "t", payload, + (word32)(sizeof(payload) - 1)); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, run_publish_unacked(&publish)); - ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); - ASSERT_EQ(1, frames); -} + rc = run_wait_pubrecs(acks, (int)sizeof(acks)); + ASSERT_EQ(g_canned_len, g_canned_pos); -#endif /* !WOLFMQTT_NO_SESSION_REPLAY && WOLFMQTT_MAX_QOS >= 1 */ + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + if (test_client.replay[i].packet_id == qos2_id && + test_client.replay[i].qos == MQTT_QOS_2 && + test_client.replay[i].pubrelSent) { + found = 1; + } + } + ASSERT_TRUE(found); +} +#endif /* WOLFMQTT_MAX_QOS >= 2 */ + +/* A CleanSession 1 Session ends with the Network Connection, so nothing is + * ever replayed and a full pool must not limit how many messages are in + * flight. */ +TEST(publish_allowed_past_replay_pool_on_clean_session) +{ + int rc; + int i; + int frames = -1; + MqttConnect connect; + static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + 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 = "replay_client"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + rc = run_publish_past_replay_pool(&frames); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(1, frames); +} + +#endif /* !WOLFMQTT_NO_SESSION_REPLAY && WOLFMQTT_MAX_QOS >= 1 */ /* ============================================================================ @@ -4930,6 +5647,113 @@ TEST(publish_stream_cb_nonblock_resume_no_tail_drop) } #endif /* WOLFMQTT_NONBLOCK */ +static byte g_connect_wire[512]; +static int g_connect_wire_len; + +static int mock_net_write_capture(void *context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; (void)timeout_ms; + if (buf != NULL && buf_len > 0 && + g_connect_wire_len + buf_len <= (int)sizeof(g_connect_wire)) { + XMEMCPY(g_connect_wire + g_connect_wire_len, buf, (size_t)buf_len); + g_connect_wire_len += buf_len; + } + return buf_len; /* accept the whole chunk */ +} + +/* A PUBLISH whose fixed and variable header exactly fill tx_buf leaves no room + * for payload in the first write. The whole payload must then follow in later + * writes instead of the client issuing an empty write after the header. */ +TEST(publish_header_fills_tx_buf_sends_payload) +{ + int rc; + int i; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + /* CONNACK v3.1.1: type=0x20, remain=2, flags=0x00, return_code=0x00. */ + static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + /* MQTT 3.1.1 section 3.3: PUBLISH QoS 0, Remaining Length 8, Topic Name + * "t" (length 1), no Packet Identifier, payload "hello". */ + static const byte expected[] = { + 0x30, 0x08, 0x00, 0x01, 't', 'h', 'e', 'l', 'l', 'o' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + + 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); + + /* Fixed header (2) plus Topic Name (3) exactly fill tx_buf */ + test_client.tx_buf_len = 5; + test_net.write = mock_net_write_capture; + g_connect_wire_len = 0; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && (rc == MQTT_CODE_CONTINUE || + rc == MQTT_CODE_PUB_CONTINUE); i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + test_client.tx_buf_len = TEST_TX_BUF_SIZE; + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ((int)sizeof(expected), g_connect_wire_len); + ASSERT_EQ(0, XMEMCMP(expected, g_connect_wire, sizeof(expected))); +} + +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) +/* WaitType stops a read twice. The repeat, after the first released lockRecv, + * must not reset the read state of a reader that has since taken it, or that + * reader would no longer be known to own lockRecv. */ +TEST(read_stop_repeat_keeps_new_reader_state) +{ + MqttMsgStat first; + MqttMsgStat second; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + XMEMSET(&first, 0, sizeof(first)); + XMEMSET(&second, 0, sizeof(second)); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttReadStart(&test_client, &first)); + MqttReadStop(&test_client, &first); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttReadStart(&test_client, &second)); + MqttReadStop(&test_client, &first); + + ASSERT_EQ(1, test_client.read.isActive); + ASSERT_EQ(1, test_client.read_owner_valid); + ASSERT_EQ(1, second.isReadActive); + MqttReadStop(&test_client, &second); + ASSERT_EQ(0, test_client.read.isActive); + ASSERT_EQ(0, test_client.read_owner_valid); +} +#endif + #if defined(WOLFMQTT_V5) && WOLFMQTT_MAX_QOS >= 1 /* Drives one QoS>0 publish to completion against the canned-response mock and * returns the final MqttClient_Publish result. The caller stages the broker's @@ -5498,21 +6322,6 @@ TEST(connect_resume_declined_clears_qos2_dedup) ASSERT_EQ(0, test_client.recv_qos2_pending[0]); } -static byte g_connect_wire[512]; -static int g_connect_wire_len; - -static int mock_net_write_capture(void *context, const byte* buf, int buf_len, - int timeout_ms) -{ - (void)context; (void)timeout_ms; - if (buf != NULL && buf_len > 0 && - g_connect_wire_len + buf_len <= (int)sizeof(g_connect_wire)) { - XMEMCPY(g_connect_wire + g_connect_wire_len, buf, (size_t)buf_len); - g_connect_wire_len += buf_len; - } - return buf_len; /* accept the whole chunk */ -} - /* Point *props at the property block of the captured v5 CONNECT and return its * length, or -1 if the frame is truncated. Layout: type byte, Remaining Length * (VBI), 10-byte variable header ("MQTT" + level + flags + keep alive), then @@ -5544,6 +6353,7 @@ static int connect_wire_props(const byte* wire, int len, const byte** props) return prop_len; } + /* [MQTT-3.3.4] The inbound QoS 2 dedup table holds MQTT_MAX_RECV_QOS2 packet * ids; without an advertised Receive Maximum a server may keep more QoS 2 * PUBLISHes un-PUBRELed than fit, and the ids that go untracked lose duplicate @@ -5958,70 +6768,810 @@ TEST(publish_writeonly_v5_receive_max_released_on_pubrec_reject) rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); } - /* The reserved unit was credited back despite the rejection. */ - ASSERT_EQ(1, test_client.server_recv_max); -#ifdef WOLFMQTT_NONBLOCK - /* Under NONBLOCK the write-only pending response is left for the reader, so - * it must be completed with the rejection (so a poll observes it instead of - * spinning) and its quota back-reference cleared. In a blocking build the - * give-up path already removed it, so there is nothing left to inspect. */ - ASSERT_NOT_NULL(test_client.firstPendResp); - ASSERT_TRUE(test_client.firstPendResp->packetDone); - ASSERT_EQ(MQTT_CODE_ERROR_PUBLISH_REJECTED, - test_client.firstPendResp->packet_ret); - ASSERT_NULL(test_client.firstPendResp->recvQuotaStat); + /* The reserved unit was credited back despite the rejection. */ + ASSERT_EQ(1, test_client.server_recv_max); +#ifdef WOLFMQTT_NONBLOCK + /* Under NONBLOCK the write-only pending response is left for the reader, so + * it must be completed with the rejection (so a poll observes it instead of + * spinning) and its quota back-reference cleared. In a blocking build the + * give-up path already removed it, so there is nothing left to inspect. */ + ASSERT_NOT_NULL(test_client.firstPendResp); + ASSERT_TRUE(test_client.firstPendResp->packetDone); + ASSERT_EQ(MQTT_CODE_ERROR_PUBLISH_REJECTED, + test_client.firstPendResp->packet_ret); + ASSERT_NULL(test_client.firstPendResp->recvQuotaStat); + + /* The documented contract, exercised through the public API rather than the + * internal node: the publisher's next poll must return the rejection, not + * another MQTT_CODE_CONTINUE. */ + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish_WriteOnly(&test_client, &publish, NULL); + } + ASSERT_EQ(MQTT_CODE_ERROR_PUBLISH_REJECTED, rc); +#endif +} +#endif /* WOLFMQTT_MAX_QOS >= 2 */ +#endif /* WOLFMQTT_NONBLOCK */ + +#ifndef WOLFMQTT_NONBLOCK +/* In a multithread build without NONBLOCK, MqttClient_Publish_WriteOnly reports + * SUCCESS while the ack is still outstanding. The caller may then free its + * MqttPublish (here on the stack), so no pending response may still reference + * it. Because the ack cannot be tracked to completion in this build, no + * Receive Maximum unit is reserved for it, so none can be orphaned. */ +TEST(publish_writeonly_v5_no_dangling_pendresp_on_success) +{ + int rc; + MqttPublish publish; /* automatic storage: must not stay referenced */ + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + test_client.server_recv_max = 1; + test_client.server_recv_max_negotiated = 1; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.packet_id = 23; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + rc = MqttClient_Publish_WriteOnly(&test_client, &publish, NULL); + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + /* No dangling reference to the soon-freed stack object. */ + ASSERT_NULL(test_client.firstPendResp); + /* No unit was reserved, so the quota is untouched. */ + ASSERT_EQ(1, test_client.server_recv_max); +} + +#if defined(WOLFMQTT_PROPERTY_CB) && \ + (defined(__MACH__) || defined(WOLFMQTT_POSIX_SEMAPHORES)) +/* A reader thread that matches a PUBACK to a write-only publish decodes into + * that publish object outside lockClient. The publisher must not return, and so + * let its caller free the object, until that reader is done with it. The v5 + * property callback runs inside the decode, so it parks the reader there. */ +typedef struct WriteOnlyAckRaceCtx { + pthread_mutex_t mutex; + pthread_cond_t cond; + MqttPublish publish; + int ack_pos; + int published; + int in_decode; + int publish_returned; + int returned_during_decode; + int sync_error; + int publish_rc; + int acks; /* copies of g_race_puback to serve */ + int prop_calls; + int next_read; /* reader entered the read after the acks */ + int returned_during_next_read; + int reader_done; +} WriteOnlyAckRaceCtx; + +/* PUBACK, MQTT 5.0 section 3.4: Packet Identifier 23, Reason Code 0x00 and a + * Reason String property (0x1F) "ok" so the property callback runs. */ +static const byte g_race_puback[] = { + 0x40, 0x09, /* PUBACK, Remaining Length 9 */ + 0x00, 0x17, /* Packet Identifier 23 */ + 0x00, /* Reason Code: Success */ + 0x05, /* Property Length 5 */ + 0x1F, 0x00, 0x02, /* Reason String, length 2 */ + 'o', 'k' +}; + +/* Wait on the race condition variable until *flag is set or one second + * passes. Called with race->mutex held. */ +static void writeonly_ack_race_wait(WriteOnlyAckRaceCtx* race, + const int* flag) +{ + struct timespec deadline; + int wait_rc = 0; + + if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + race->sync_error = 1; + return; + } + deadline.tv_sec++; + while (*flag == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&race->cond, &race->mutex, + &deadline); + } + if (wait_rc != 0 && wait_rc != ETIMEDOUT) { + race->sync_error = 1; + } +} + +/* Holds the PUBLISH write until the reader is inside the PUBACK decode, so the + * publisher checks its pending response at the worst moment. */ +static int mock_net_write_writeonly_ack_race(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + + (void)timeout_ms; + if (buf_len > 0 && (buf[0] >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->published = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->in_decode); + if (race->in_decode == 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + } + return buf_len; +} + +/* Serves the PUBACK once the PUBLISH is out, then reports an idle timeout. */ +static int mock_net_read_writeonly_ack_race(void *context, byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + int n; + + (void)timeout_ms; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + writeonly_ack_race_wait(race, &race->published); + n = (int)sizeof(g_race_puback) - race->ack_pos; + if (race->published == 0 || n <= 0) { + n = MQTT_CODE_ERROR_TIMEOUT; + } + else { + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_race_puback + race->ack_pos, n); + race->ack_pos += n; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return n; +} + +/* Runs on the reader thread while it decodes into the publisher's object. */ +static int writeonly_ack_race_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_decode = race->publish_returned; + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return MQTT_CODE_SUCCESS; +} + +static void* writeonly_ack_race_reader_thread(void* arg) +{ + (void)arg; + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + return NULL; +} + +static void* writeonly_ack_race_publish_thread(void* arg) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)arg; + int rc; + + rc = MqttClient_Publish_WriteOnly(&test_client, &race->publish, NULL); + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return NULL; + } + race->publish_rc = rc; + race->publish_returned = 1; + if (pthread_cond_broadcast(&race->cond) != 0 || + pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + } + return NULL; +} + +TEST(publish_writeonly_waits_for_reader_decoding_ack) +{ + static WriteOnlyAckRaceCtx race; + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(&race, 0, sizeof(race)); + ASSERT_EQ(0, pthread_mutex_init(&race.mutex, NULL)); + ASSERT_EQ(0, pthread_cond_init(&race.cond, NULL)); + + race.publish.qos = MQTT_QOS_1; + race.publish.packet_id = 23; + race.publish.topic_name = "test/topic"; + race.publish.buffer = payload; + race.publish.total_len = (word32)(sizeof(payload) - 1); + race.publish.buffer_len = race.publish.total_len; + race.publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = ∽̱ + test_net.write = mock_net_write_writeonly_ack_race; + test_net.read = mock_net_read_writeonly_ack_race; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_SetPropertyCallback(&test_client, + writeonly_ack_race_prop_cb, &race)); + + ASSERT_EQ(0, pthread_create(&reader_thread, NULL, + writeonly_ack_race_reader_thread, &race)); + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, &race)); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(0, pthread_join(reader_thread, NULL)); + + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.in_decode); + /* The publisher did not return while the reader was inside its object. */ + ASSERT_EQ(0, race.returned_during_decode); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); + + ASSERT_EQ(0, pthread_cond_destroy(&race.cond)); + ASSERT_EQ(0, pthread_mutex_destroy(&race.mutex)); +} + +/* Serves race->acks copies of g_race_puback once the PUBLISH is out. The read + * after them waits for the publisher to return, as a dedicated reader blocks + * in its next read, then reports an idle timeout. */ +static int mock_net_read_writeonly_acks(void *context, byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + int total; + int n; + int i; + + (void)timeout_ms; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + writeonly_ack_race_wait(race, &race->published); + total = (int)sizeof(g_race_puback) * race->acks; + n = total - race->ack_pos; + if (race->published == 0) { + n = MQTT_CODE_ERROR_TIMEOUT; + } + else if (n <= 0) { + race->next_read = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_next_read = race->publish_returned; + n = MQTT_CODE_ERROR_TIMEOUT; + } + else { + if (n > buf_len) { + n = buf_len; + } + for (i = 0; i < n; i++) { + buf[i] = g_race_puback[(race->ack_pos + i) % + (int)sizeof(g_race_puback)]; + } + race->ack_pos += n; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return n; +} + +/* Parks the reader in the decode of the last ack until the publisher returns + * or one second passes. Earlier acks pass straight through. */ +static int writeonly_last_ack_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->prop_calls++; + if (race->prop_calls == race->acks) { + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_decode = race->publish_returned; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return MQTT_CODE_SUCCESS; +} + +/* Sets up a write-only QoS 1 publish raced against a WaitMessage reader that + * receives race->acks PUBACKs for it, runs both threads and joins them. */ +static int writeonly_acks_run(WriteOnlyAckRaceCtx* race, int acks, + MqttPropertyCb prop_cb) +{ + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + + if (test_init_client() != MQTT_CODE_SUCCESS) { + return -1; + } + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(race, 0, sizeof(*race)); + if (pthread_mutex_init(&race->mutex, NULL) != 0 || + pthread_cond_init(&race->cond, NULL) != 0) { + return -1; + } + race->acks = acks; + race->publish.qos = MQTT_QOS_1; + race->publish.packet_id = 23; + race->publish.topic_name = "test/topic"; + race->publish.buffer = payload; + race->publish.total_len = (word32)(sizeof(payload) - 1); + race->publish.buffer_len = race->publish.total_len; + race->publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = race; + test_net.write = mock_net_write_writeonly_ack_race; + test_net.read = mock_net_read_writeonly_acks; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + if (MqttClient_SetPropertyCallback(&test_client, prop_cb, race) != + MQTT_CODE_SUCCESS) { + return -1; + } + if (pthread_create(&reader_thread, NULL, + writeonly_ack_race_reader_thread, race) != 0) { + return -1; + } + if (pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, race) != 0) { + (void)pthread_join(reader_thread, NULL); + return -1; + } + if (pthread_join(publish_thread, NULL) != 0 || + pthread_join(reader_thread, NULL) != 0) { + return -1; + } + if (pthread_cond_destroy(&race->cond) != 0 || + pthread_mutex_destroy(&race->mutex) != 0) { + return -1; + } + return 0; +} + +/* Lets the publisher reach its wait before the reader leaves the decode. */ +static int writeonly_brief_ack_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + struct timespec pause; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->prop_calls++; + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + pause.tv_sec = 0; + pause.tv_nsec = 100 * 1000 * 1000; + (void)nanosleep(&pause, NULL); + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_SEM_SIGNAL +/* A dedicated reader retakes lockRecv for its next blocking read as soon as it + * finishes the ack. A publisher waiting for that decode to end must be woken + * by it, not left waiting until the reader's next read returns. */ +TEST(publish_writeonly_not_held_by_reader_next_read) +{ + static WriteOnlyAckRaceCtx race; + + ASSERT_EQ(0, writeonly_acks_run(&race, 1, writeonly_brief_ack_prop_cb)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.in_decode); + ASSERT_EQ(1, race.next_read); + ASSERT_EQ(1, race.returned_during_next_read); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); +} +#endif + +/* A duplicate PUBACK matches the entry the first one already completed, and + * the reader decodes it into the same publish object. The publisher must not + * take the completed entry and return while that decode runs. */ +TEST(publish_writeonly_waits_for_duplicate_ack_decode) +{ + static WriteOnlyAckRaceCtx race; + + ASSERT_EQ(0, writeonly_acks_run(&race, 2, writeonly_last_ack_prop_cb)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(2, race.prop_calls); + ASSERT_EQ(1, race.in_decode); + ASSERT_EQ(0, race.returned_during_decode); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); +} + +/* Holds the PUBLISH write until the reader has failed on the PUBACK. */ +static int mock_net_write_after_reader(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + + (void)timeout_ms; + if (buf_len > 0 && (buf[0] >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->published = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->reader_done); + if (race->reader_done == 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + } + return buf_len; +} + +/* Fails the decode of the PUBACK after the reader has claimed the entry. */ +static int failing_ack_prop_cb(MqttClient* client, MqttProp* head, void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + race->prop_calls++; + return MQTT_CODE_ERROR_SYSTEM; +} + +static void* failing_ack_reader_thread(void* arg) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)arg; + + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return NULL; + } + race->reader_done = 1; + if (pthread_cond_broadcast(&race->cond) != 0 || + pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + } + return NULL; +} + +/* A reader that fails while decoding into another thread's entry must release + * its claim, or the owner would wait forever for the decode to end. */ +TEST(publish_writeonly_after_reader_decode_error) +{ + static WriteOnlyAckRaceCtx race; + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int returned; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(&race, 0, sizeof(race)); + ASSERT_EQ(0, pthread_mutex_init(&race.mutex, NULL)); + ASSERT_EQ(0, pthread_cond_init(&race.cond, NULL)); + race.publish.qos = MQTT_QOS_1; + race.publish.packet_id = 23; + race.publish.topic_name = "test/topic"; + race.publish.buffer = payload; + race.publish.total_len = (word32)(sizeof(payload) - 1); + race.publish.buffer_len = race.publish.total_len; + race.publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = ∽̱ + race.acks = 1; + test_net.write = mock_net_write_after_reader; + test_net.read = mock_net_read_writeonly_acks; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_SetPropertyCallback(&test_client, + failing_ack_prop_cb, &race)); + + ASSERT_EQ(0, pthread_create(&reader_thread, NULL, + failing_ack_reader_thread, &race)); + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, &race)); + ASSERT_EQ(0, pthread_join(reader_thread, NULL)); + + ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &deadline)); + deadline.tv_sec += 2; + ASSERT_EQ(0, pthread_mutex_lock(&race.mutex)); + while (race.publish_returned == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&race.cond, &race.mutex, &deadline); + } + returned = race.publish_returned; + ASSERT_EQ(0, pthread_mutex_unlock(&race.mutex)); + if (!returned) { + /* Stuck waiting: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + } + ASSERT_EQ(1, returned); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.prop_calls); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); + ASSERT_EQ(0, pthread_cond_destroy(&race.cond)); + ASSERT_EQ(0, pthread_mutex_destroy(&race.mutex)); +} + +#ifdef WOLFMQTT_THREAD_ID_T +/* A property callback runs on the thread that holds lockRecv while it decodes + * an ack. Client calls from it that would wait on that lock must refuse with + * MQTT_CODE_ERROR_STAT rather than self-deadlock. The publish runs on its own + * thread so a deadlock fails the test instead of hanging it. */ +#define CB_REENTRY_CANCEL 0 +#define CB_REENTRY_NET_DISCONNECT 1 + +typedef struct CbReentryCtx { + pthread_mutex_t mutex; + pthread_cond_t cond; + MqttPublish publish; + int action; + int cb_called; + int cb_rc; + int publish_rc; + int done; +} CbReentryCtx; + +static int cb_reentry_prop_cb(MqttClient* client, MqttProp* head, void* ctx) +{ + CbReentryCtx* reentry = (CbReentryCtx*)ctx; - /* The documented contract, exercised through the public API rather than the - * internal node: the publisher's next poll must return the rejection, not - * another MQTT_CODE_CONTINUE. */ - rc = MQTT_CODE_CONTINUE; - for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { - rc = MqttClient_Publish_WriteOnly(&test_client, &publish, NULL); + (void)head; + reentry->cb_called = 1; + if (reentry->action == CB_REENTRY_CANCEL) { + reentry->cb_rc = MqttClient_CancelMessage(client, + (MqttObject*)&reentry->publish); } - ASSERT_EQ(MQTT_CODE_ERROR_PUBLISH_REJECTED, rc); -#endif + else { + reentry->cb_rc = MqttClient_NetDisconnect_Wait(client); + } + return MQTT_CODE_SUCCESS; } -#endif /* WOLFMQTT_MAX_QOS >= 2 */ -#endif /* WOLFMQTT_NONBLOCK */ -#ifndef WOLFMQTT_NONBLOCK -/* In a multithread build without NONBLOCK, MqttClient_Publish_WriteOnly reports - * SUCCESS while the ack is still outstanding. The caller may then free its - * MqttPublish (here on the stack), so no pending response may still reference - * it. Because the ack cannot be tracked to completion in this build, no - * Receive Maximum unit is reserved for it, so none can be orphaned. */ -TEST(publish_writeonly_v5_no_dangling_pendresp_on_success) +static void* cb_reentry_publish_thread(void* arg) { + CbReentryCtx* reentry = (CbReentryCtx*)arg; int rc; - MqttPublish publish; /* automatic storage: must not stay referenced */ + + rc = MqttClient_Publish(&test_client, &reentry->publish); + (void)pthread_mutex_lock(&reentry->mutex); + reentry->publish_rc = rc; + reentry->done = 1; + (void)pthread_cond_broadcast(&reentry->cond); + (void)pthread_mutex_unlock(&reentry->mutex); + return NULL; +} + +/* Runs a blocking QoS 1 publish whose PUBACK carries a property, with the + * property callback doing reentry->action. Returns 0 once the publish + * finished, or -1 if it is still stuck after two seconds. */ +static int cb_reentry_run(CbReentryCtx* reentry, int action) +{ static byte payload[] = "hello"; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int finished; - rc = test_init_client(); - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + if (test_init_client() != MQTT_CODE_SUCCESS) { + return -1; + } test_client_connect_sent(); test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; - test_client.server_recv_max = 1; - test_client.server_recv_max_negotiated = 1; + XMEMSET(reentry, 0, sizeof(*reentry)); + if (pthread_mutex_init(&reentry->mutex, NULL) != 0 || + pthread_cond_init(&reentry->cond, NULL) != 0) { + return -1; + } + reentry->action = action; + reentry->cb_rc = MQTT_CODE_SUCCESS; + reentry->publish.qos = MQTT_QOS_1; + reentry->publish.packet_id = 23; + reentry->publish.topic_name = "test/topic"; + reentry->publish.buffer = payload; + reentry->publish.total_len = (word32)(sizeof(payload) - 1); + reentry->publish.buffer_len = reentry->publish.total_len; test_net.write = mock_net_write_accept; - test_net.read = mock_net_read; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, g_race_puback, sizeof(g_race_puback)); + g_canned_len = (int)sizeof(g_race_puback); + g_canned_pos = 0; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + if (MqttClient_SetPropertyCallback(&test_client, cb_reentry_prop_cb, + reentry) != MQTT_CODE_SUCCESS) { + return -1; + } - XMEMSET(&publish, 0, sizeof(publish)); - publish.qos = MQTT_QOS_1; - publish.packet_id = 23; - publish.topic_name = "test/topic"; - publish.buffer = payload; - publish.total_len = (word32)(sizeof(payload) - 1); - publish.buffer_len = publish.total_len; + if (pthread_create(&publish_thread, NULL, cb_reentry_publish_thread, + reentry) != 0 || + clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + return -1; + } + deadline.tv_sec += 2; + (void)pthread_mutex_lock(&reentry->mutex); + while (reentry->done == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&reentry->cond, &reentry->mutex, + &deadline); + } + finished = reentry->done; + (void)pthread_mutex_unlock(&reentry->mutex); + if (!finished) { + /* Deadlocked: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + return -1; + } + (void)pthread_join(publish_thread, NULL); + (void)pthread_cond_destroy(&reentry->cond); + (void)pthread_mutex_destroy(&reentry->mutex); + return 0; +} - rc = MqttClient_Publish_WriteOnly(&test_client, &publish, NULL); +TEST(cancel_from_own_ack_property_cb_refuses) +{ + static CbReentryCtx reentry; - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); - /* No dangling reference to the soon-freed stack object. */ + ASSERT_EQ(0, cb_reentry_run(&reentry, CB_REENTRY_CANCEL)); + ASSERT_EQ(1, reentry.cb_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, reentry.cb_rc); + /* The refused cancel left the exchange intact, so it completes. */ + ASSERT_EQ(MQTT_CODE_SUCCESS, reentry.publish_rc); ASSERT_NULL(test_client.firstPendResp); - /* No unit was reserved, so the quota is untouched. */ - ASSERT_EQ(1, test_client.server_recv_max); } + +TEST(net_disconnect_wait_from_property_cb_refuses) +{ + static CbReentryCtx reentry; + + ASSERT_EQ(0, cb_reentry_run(&reentry, CB_REENTRY_NET_DISCONNECT)); + ASSERT_EQ(1, reentry.cb_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, reentry.cb_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, reentry.publish_rc); +} + +static pthread_mutex_t g_pub_cb_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_pub_cb_cond = PTHREAD_COND_INITIALIZER; +static int g_pub_cb_disconnect_rc; +static int g_pub_cb_disconnect_called; +static int g_pub_cb_disconnect_done; + +/* Runs while the publish holds lockSend. */ +static int pub_cb_net_disconnect_wait(MqttPublish* publish) +{ + g_pub_cb_disconnect_called = 1; + g_pub_cb_disconnect_rc = MqttClient_NetDisconnect_Wait(&test_client); + XMEMSET(publish->buffer, 'x', publish->buffer_len); + return (int)publish->buffer_len; +} + +static void* pub_cb_disconnect_thread(void* arg) +{ + MqttPublish* publish = (MqttPublish*)arg; + + (void)MqttClient_Publish_ex(&test_client, publish, + pub_cb_net_disconnect_wait); + (void)pthread_mutex_lock(&g_pub_cb_mutex); + g_pub_cb_disconnect_done = 1; + (void)pthread_cond_broadcast(&g_pub_cb_cond); + (void)pthread_mutex_unlock(&g_pub_cb_mutex); + return NULL; +} + +TEST(net_disconnect_wait_from_publish_payload_cb_refuses) +{ + static byte payload[8]; + static MqttPublish publish; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int finished; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + test_client_connect_sent(); + test_net.write = mock_net_write_accept; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + g_pub_cb_disconnect_rc = MQTT_CODE_SUCCESS; + g_pub_cb_disconnect_called = 0; + g_pub_cb_disconnect_done = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + publish.buffer = payload; + publish.buffer_len = (word32)sizeof(payload); + publish.total_len = (word32)sizeof(payload); + + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + pub_cb_disconnect_thread, &publish)); + ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &deadline)); + deadline.tv_sec += 2; + ASSERT_EQ(0, pthread_mutex_lock(&g_pub_cb_mutex)); + while (g_pub_cb_disconnect_done == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&g_pub_cb_cond, &g_pub_cb_mutex, + &deadline); + } + finished = g_pub_cb_disconnect_done; + ASSERT_EQ(0, pthread_mutex_unlock(&g_pub_cb_mutex)); + if (!finished) { + /* Deadlocked: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + } + ASSERT_EQ(1, finished); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(1, g_pub_cb_disconnect_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, g_pub_cb_disconnect_rc); +} +#endif /* WOLFMQTT_THREAD_ID_T */ +#endif /* WOLFMQTT_PROPERTY_CB && (__MACH__ || WOLFMQTT_POSIX_SEMAPHORES) */ #endif /* !WOLFMQTT_NONBLOCK */ #ifdef WOLFMQTT_NONBLOCK @@ -6624,6 +8174,92 @@ TEST(wait_message_v5_props_null_msg_cb_frees_props) } #endif /* WOLFMQTT_V5 */ +/* Message callback that fails on call number g_msg_cb_fail_on (1-based). */ +static int g_msg_cb_fail_calls; +static int g_msg_cb_fail_on; +static int test_failing_message_cb(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; (void)msg; (void)msg_new; (void)msg_done; + g_msg_cb_fail_calls++; + return (g_msg_cb_fail_calls == g_msg_cb_fail_on) ? + MQTT_CODE_ERROR_CALLBACK : MQTT_CODE_SUCCESS; +} + +/* Drive a PUBLISH whose payload spans several rx_buf reads into a message + * callback that fails on call fail_on, with the transport marked connected. + * Returns the terminal WaitMessage result. */ +static int run_wait_message_cb_fails_mid_payload(int fail_on) +{ + /* rx_buf smaller than the payload, so it is delivered in chunks */ + static byte small_rx_buf[16]; + int rc; + int i; + /* v3.1.1 QoS 0 PUBLISH: type=0x30, Remaining Length 33, topic "a" + * (0x0001 'a'), 30-byte payload (no packet id for QoS 0). */ + static const byte publish_qos0[] = { + 0x30, 0x21, 0x00, 0x01, 'a', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + + rc = MqttClient_Init(&test_client, &test_net, test_failing_message_cb, + test_tx_buf, TEST_TX_BUF_SIZE, small_rx_buf, + (int)sizeof(small_rx_buf), TEST_CMD_TIMEOUT_MS); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + test_client_inited = 1; +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + + g_msg_cb_fail_calls = 0; + g_msg_cb_fail_on = fail_on; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, publish_qos0, sizeof(publish_qos0)); + g_canned_len = (int)sizeof(publish_qos0); + 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); + } + return rc; +} + +/* A message callback error with payload still unread leaves the rest of the + * PUBLISH in the transport, so the next read would parse payload bytes as a + * fixed header. MqttMsgCb documents that a non-success return causes a net + * disconnect: the client must stop reporting the connection as usable. */ +TEST(wait_message_cb_error_first_chunk_clears_connected) +{ + int rc; + + rc = run_wait_message_cb_fails_mid_payload(1); + + ASSERT_EQ(MQTT_CODE_ERROR_CALLBACK, rc); + ASSERT_EQ(1, g_msg_cb_fail_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +TEST(wait_message_cb_error_later_chunk_clears_connected) +{ + int rc; + + rc = run_wait_message_cb_fails_mid_payload(2); + + ASSERT_EQ(MQTT_CODE_ERROR_CALLBACK, rc); + ASSERT_EQ(2, g_msg_cb_fail_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + /* Counts deliveries for the positive-control test below. */ static int g_msg_cb_calls; static int test_accept_message_cb(MqttClient* client, MqttMessage* msg, @@ -6634,6 +8270,43 @@ static int test_accept_message_cb(MqttClient* client, MqttMessage* msg, return MQTT_CODE_SUCCESS; } +/* MqttClient_WaitMessage waits on the client's shared msg object. Once a + * packet that needs no reply (here an unsolicited PUBACK, MQTT 3.1.1 section + * 3.4) has been handled, the next call must read the next packet, not handle + * the PUBACK again. Frames: PUBACK id 9, then a QoS 0 PUBLISH on "t". */ +TEST(wait_message_reads_each_packet_once) +{ + int rc; + static const byte frames[] = { + 0x40, 0x02, 0x00, 0x09, + 0x30, 0x04, 0x00, 0x01, 't', 'x' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = test_accept_message_cb; + g_msg_cb_calls = 0; + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, frames, sizeof(frames)); + g_canned_len = (int)sizeof(frames); + g_canned_pos = 0; + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, g_canned_pos); + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(g_canned_len, g_canned_pos); + ASSERT_EQ(1, g_msg_cb_calls); +} + #ifdef WOLFMQTT_NONBLOCK static int g_partial_vbi_read_step; static byte g_partial_vbi_publish[131]; @@ -6709,12 +8382,103 @@ TEST(wait_message_timeout_preserves_partial_vbi) } #endif /* WOLFMQTT_NONBLOCK */ +#ifndef WOLFMQTT_NONBLOCK +/* Blocking-build read mock: hands out g_timeout_frame up to g_timeout_after + * bytes in total, then reports MQTT_CODE_ERROR_TIMEOUT. */ +static const byte* g_timeout_frame; +static int g_timeout_after; +static int g_timeout_pos; + +static int mock_net_read_then_timeout(void *context, byte* buf, int buf_len, + int timeout_ms) +{ + int n; + + (void)context; + (void)timeout_ms; + n = g_timeout_after - g_timeout_pos; + if (n <= 0) { + return MQTT_CODE_ERROR_TIMEOUT; + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_timeout_frame + g_timeout_pos, (size_t)n); + g_timeout_pos += n; + return n; +} + +static int run_wait_timeout_after(const byte* frame, int after) +{ + int rc; + + rc = test_init_client(); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = test_accept_message_cb; + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + test_net.read = mock_net_read_then_timeout; + g_timeout_frame = frame; + g_timeout_after = after; + g_timeout_pos = 0; + g_msg_cb_calls = 0; + return MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); +} + +/* v3.1.1 QoS 0 PUBLISH: Remaining Length 5, topic "ab", payload "x". */ +static const byte g_timeout_publish[] = { + 0x30, 0x05, 0x00, 0x02, 'a', 'b', 'x' +}; + +/* A blocking read cannot resume a packet: once it times out after consuming + * part of one, the rest of that packet would be read as a new packet. The + * wait must fail the connection, not report an ordinary idle timeout. Here + * one byte of the fixed header arrives before the timeout. */ +TEST(wait_message_blocking_timeout_mid_header_fails_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 1); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(0, g_msg_cb_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* The same once the fixed header is complete and part of the body is in. */ +TEST(wait_message_blocking_timeout_mid_body_fails_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 4); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(0, g_msg_cb_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* A timeout before any byte of a packet arrives is an ordinary idle wait and + * leaves the connection up. */ +TEST(wait_message_blocking_idle_timeout_keeps_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 0); + + ASSERT_EQ(MQTT_CODE_ERROR_TIMEOUT, rc); + ASSERT_NE(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} +#endif /* !WOLFMQTT_NONBLOCK */ + #ifdef WOLFMQTT_V5 /* The client advertises Topic Alias Maximum 0 and keeps no inbound alias table, * so an empty Topic Name paired with a Topic Alias is a non-conforming server * PUBLISH that cannot be resolved; it must be rejected, not delivered to the * callback with a zero-length topic. This fixed wire fixture reaches the * preliminary packet decode used by MqttClient_WaitMessage. */ + TEST(wait_message_v5_empty_topic_with_alias_rejected) { int rc; @@ -8005,6 +9769,9 @@ void run_mqtt_client_tests(void) /* MqttClient_DeInit tests */ RUN_TEST(deinit_null_client); RUN_TEST(deinit_after_init); + RUN_TEST(deinit_leaves_caller_buffers_untouched); + RUN_TEST(net_disconnect_wait_null_client); + RUN_TEST(net_disconnect_wait_closes_transport); /* MqttClient_Connect tests */ RUN_TEST(connect_null_client); @@ -8031,6 +9798,14 @@ void run_mqtt_client_tests(void) RUN_TEST(connect_v5_scrubs_connack_auth_data_from_rx_buf); RUN_TEST(auth_without_connect_method_rejected); RUN_TEST(auth_with_connect_method_allowed); +#ifdef WOLFMQTT_NONBLOCK + RUN_TEST(connect_auth_pending_blocks_other_packets); + RUN_TEST(connect_auth_pending_allows_disconnect); + RUN_TEST(connect_auth_pending_blocks_unsubscribe_and_keepalive); + RUN_TEST(connect_auth_pending_allows_auth); + RUN_TEST(connect_auth_pending_cleared_by_reconnect); +#endif + RUN_TEST(connect_auth_pending_raised_before_connect_write); RUN_TEST(wait_message_v311_rejects_auth_packet_type); RUN_TEST(wait_message_v5_accepts_auth_packet_type); RUN_TEST(wait_message_v311_rejects_disconnect_packet_type); @@ -8072,6 +9847,9 @@ void run_mqtt_client_tests(void) #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_NONBLOCK) && \ WOLFMQTT_MAX_QOS >= 1 RUN_TEST(wait_message_puback_survives_shared_ack_overwrite); +#endif +#if !defined(WOLFMQTT_NONBLOCK) && WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(wait_message_zero_byte_puback_write_is_error); #endif RUN_TEST(net_connect_clears_connect_sent_flag); RUN_TEST(publish_after_disconnect_rejected); @@ -8080,6 +9858,11 @@ void run_mqtt_client_tests(void) #if !defined(WOLFMQTT_NO_SESSION_REPLAY) && WOLFMQTT_MAX_QOS >= 1 RUN_TEST(reconnect_replays_unacked_qos1_publish); RUN_TEST(publish_refused_when_replay_pool_full_on_persistent_session); + RUN_TEST(unsolicited_pubrec_does_not_fill_replay_pool); + RUN_TEST(pubrec_for_qos1_id_keeps_publish_replay); +#if WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(pubrec_for_unslotted_qos2_records_pubrel); +#endif RUN_TEST(publish_allowed_past_replay_pool_on_clean_session); #ifdef WOLFMQTT_V5 RUN_TEST(publish_allowed_past_replay_pool_when_connack_ends_session); @@ -8123,6 +9906,10 @@ void run_mqtt_client_tests(void) RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); + RUN_TEST(subscribe_too_few_return_codes_clears_connected); + RUN_TEST(subscribe_reused_object_reads_new_suback); + RUN_TEST(unsubscribe_reused_object_reads_new_unsuback); + RUN_TEST(publish_qos1_reused_object_reads_new_puback); #ifdef WOLFMQTT_V5 RUN_TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected); RUN_TEST(unsubscribe_too_few_reason_codes_is_malformed); @@ -8209,9 +9996,26 @@ void run_mqtt_client_tests(void) RUN_TEST(disconnect_clears_recv_quota_ownership); #else RUN_TEST(publish_writeonly_v5_no_dangling_pendresp_on_success); +#if defined(WOLFMQTT_PROPERTY_CB) && \ + (defined(__MACH__) || defined(WOLFMQTT_POSIX_SEMAPHORES)) + RUN_TEST(publish_writeonly_waits_for_reader_decoding_ack); +#ifdef WOLFMQTT_SEM_SIGNAL + RUN_TEST(publish_writeonly_not_held_by_reader_next_read); +#endif + RUN_TEST(publish_writeonly_waits_for_duplicate_ack_decode); + RUN_TEST(publish_writeonly_after_reader_decode_error); +#ifdef WOLFMQTT_THREAD_ID_T + RUN_TEST(cancel_from_own_ack_property_cb_refuses); + RUN_TEST(net_disconnect_wait_from_property_cb_refuses); + RUN_TEST(net_disconnect_wait_from_publish_payload_cb_refuses); #endif #endif #endif +#endif +#endif +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + RUN_TEST(read_stop_repeat_keeps_new_reader_state); +#endif #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_NONBLOCK) #if WOLFMQTT_MAX_QOS >= 1 RUN_TEST(publish_writeonly_rejects_duplicate_in_flight_packet_id); @@ -8226,6 +10030,8 @@ void run_mqtt_client_tests(void) RUN_TEST(wait_message_qos1_null_msg_cb_errors_no_ack); RUN_TEST(wait_message_qos0_null_msg_cb_errors); RUN_TEST(wait_message_qos2_null_msg_cb_errors_no_ack); + RUN_TEST(wait_message_cb_error_first_chunk_clears_connected); + RUN_TEST(wait_message_cb_error_later_chunk_clears_connected); #if (WOLFMQTT_MAX_QOS >= 2) && (MQTT_MAX_RECV_QOS2 < 65535) RUN_TEST(wait_message_qos2_full_dedup_table_v311_errors_no_ack); #ifdef WOLFMQTT_V5 @@ -8245,7 +10051,13 @@ void run_mqtt_client_tests(void) #endif #ifdef WOLFMQTT_NONBLOCK RUN_TEST(wait_message_timeout_preserves_partial_vbi); +#else + RUN_TEST(wait_message_blocking_timeout_mid_header_fails_connection); + RUN_TEST(wait_message_blocking_timeout_mid_body_fails_connection); + RUN_TEST(wait_message_blocking_idle_timeout_keeps_connection); #endif + RUN_TEST(wait_message_reads_each_packet_once); + RUN_TEST(publish_header_fills_tx_buf_sends_payload); RUN_TEST(wait_message_qos0_publish_no_ack); RUN_TEST(wait_message_qos1_publish_acks_puback); RUN_TEST(wait_message_qos2_publish_acks_pubrec); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index d85fb6cbd..c2d050063 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -312,6 +312,8 @@ typedef struct PacketReadFixture { int data_len; int pos; int calls; + int max_chunk; /* bytes delivered per read, 0 for no limit */ + int extra; /* added to the reported length to simulate over-report */ } PacketReadFixture; static int packet_read_fixture(void* context, byte* buf, int buf_len, @@ -325,12 +327,15 @@ static int packet_read_fixture(void* context, byte* buf, int buf_len, if (read_len > buf_len) { read_len = buf_len; } + if (fixture->max_chunk > 0 && read_len > fixture->max_chunk) { + read_len = fixture->max_chunk; + } if (read_len <= 0) { return MQTT_CODE_ERROR_NETWORK; } XMEMCPY(buf, fixture->data + fixture->pos, (size_t)read_len); fixture->pos += read_len; - return read_len; + return read_len + fixture->extra; } static int packet_net_connect(void* context, const char* host, word16 port, @@ -411,6 +416,242 @@ TEST(packet_read_rejects_buffer_shorter_than_max_header) MqttClient_DeInit(&client); } +static int packet_write_count(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + PacketReadFixture* fixture = (PacketReadFixture*)context; + + (void)buf; + (void)timeout_ms; + fixture->calls++; + return buf_len + fixture->extra; +} + +static int packet_io_client_init(MqttClient* client, MqttNet* net, + PacketReadFixture* fixture, byte* tx, int tx_len, byte* rx, int rx_len) +{ + XMEMSET(net, 0, sizeof(*net)); + XMEMSET(fixture, 0, sizeof(*fixture)); + net->context = fixture; + net->connect = packet_net_connect; + net->read = packet_read_fixture; + net->write = packet_write_count; + net->disconnect = packet_net_disconnect; + return MqttClient_Init(client, net, NULL, tx, tx_len, rx, rx_len, 0); +} + +/* A corrupted negative socket position must be rejected before it is used + * as a buffer index or in the remaining-length arithmetic. */ +TEST(packet_read_write_reject_negative_socket_pos) +{ + static const byte ping_resp[] = { 0xD0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = ping_resp; + fixture.data_len = (int)sizeof(ping_resp); + + client.read.pos = -1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + XMEMSET(buf, 0, sizeof(buf)); + client.write.pos = -1; + rc = MqttPacket_Write(&client, buf, 2); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + MqttClient_DeInit(&client); +} + +/* Resuming in MQTT_PK_READ with corrupted header/remaining lengths must fail + * without reading, rather than computing an out-of-range return length. */ +TEST(packet_read_rejects_corrupt_resume_state) +{ + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = -1; + client.packet.remain_len = 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = (int)sizeof(buf) + 1; + client.packet.remain_len = 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = 2; + client.packet.remain_len = -1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_EQ(0, fixture.calls); + + MqttClient_DeInit(&client); +} + +/* The running byte totals saturate instead of overflowing a signed int. */ +TEST(packet_read_write_total_saturates) +{ + static const byte ping_resp[] = { 0xD0, 0x00 }; + static const byte ping_req[] = { 0xC0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = ping_resp; + fixture.data_len = (int)sizeof(ping_resp); + + client.read.total = 0x7FFFFFFF - 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ((int)sizeof(ping_resp), rc); + ASSERT_EQ(0x7FFFFFFF, client.read.total); + ASSERT_EQ(0, client.read.pos); + + XMEMCPY(buf, ping_req, sizeof(ping_req)); + client.write.total = 0x7FFFFFFF; + rc = MqttPacket_Write(&client, buf, (int)sizeof(ping_req)); + ASSERT_EQ((int)sizeof(ping_req), rc); + ASSERT_EQ(0x7FFFFFFF, client.write.total); + ASSERT_EQ(0, client.write.pos); + + MqttClient_DeInit(&client); +} + +/* A callback that reports more bytes than requested must be clamped: the + * returned length stays exact and nothing outside the buffer is touched. */ +TEST(packet_read_write_clamp_over_reported_length) +{ + /* PUBLISH QoS 0: fixed header 0x30, Remaining Length 3, + * Topic Name length 0x0001, Topic Name "a" (MQTT 5.0 section 3.3) */ + static const byte publish[] = { 0x30, 0x03, 0x00, 0x01, 0x61 }; + static const byte ping_req[] = { 0xC0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte guarded[10]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = publish; + fixture.data_len = (int)sizeof(publish); + fixture.extra = 5; + + XMEMSET(guarded, 0xA5, sizeof(guarded)); + rc = MqttPacket_Read(&client, &guarded[1], (int)sizeof(guarded) - 2, 0); + ASSERT_EQ((int)sizeof(publish), rc); + ASSERT_EQ(0, XMEMCMP(&guarded[1], publish, sizeof(publish))); + ASSERT_EQ(0xA5, guarded[0]); + ASSERT_EQ(0xA5, guarded[sizeof(publish) + 1]); + ASSERT_EQ(0xA5, guarded[sizeof(guarded) - 1]); + ASSERT_EQ(0, client.read.pos); + ASSERT_EQ((int)sizeof(publish), fixture.pos); + + XMEMCPY(guarded, ping_req, sizeof(ping_req)); + rc = MqttPacket_Write(&client, guarded, (int)sizeof(ping_req)); + ASSERT_EQ((int)sizeof(ping_req), rc); + ASSERT_EQ(0, client.write.pos); + + MqttClient_DeInit(&client); +} + +/* One byte per callback: blocking builds reassemble the packet in a single + * MqttPacket_Read; non-blocking builds resume from read.pos on each call. */ +TEST(packet_read_reassembles_one_byte_reads) +{ + /* PUBLISH QoS 0 with Remaining Length 128, encoded as the two-byte + * Variable Byte Integer 0x80 0x01 [MQTT-1.5.5]. Variable header is + * Topic Name length 0x0001 and "a"; the remaining 125 bytes are + * payload (MQTT 5.0 section 3.3). */ + byte publish[3 + 128]; + byte client_rx[16]; + byte client_tx[16]; + byte rx[160]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + int i; +#ifdef WOLFMQTT_NONBLOCK + int prev_pos = 0; + int iters = 0; +#endif + + publish[0] = 0x30; + publish[1] = 0x80; + publish[2] = 0x01; + publish[3] = 0x00; + publish[4] = 0x01; + publish[5] = 0x61; + for (i = 6; i < (int)sizeof(publish); i++) { + publish[i] = (byte)i; + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = publish; + fixture.data_len = (int)sizeof(publish); + fixture.max_chunk = 1; + + XMEMSET(rx, 0, sizeof(rx)); +#ifdef WOLFMQTT_NONBLOCK + do { + rc = MqttPacket_Read(&client, rx, (int)sizeof(rx), 0); + if (rc == MQTT_CODE_CONTINUE) { + /* progress either continues the current read or starts the + * next one */ + ASSERT_TRUE(client.read.pos == prev_pos + 1 || + client.read.pos == 1); + prev_pos = client.read.pos; + } + iters++; + } while (rc == MQTT_CODE_CONTINUE && iters <= (int)sizeof(publish)); +#else + rc = MqttPacket_Read(&client, rx, (int)sizeof(rx), 0); +#endif + ASSERT_EQ((int)sizeof(publish), rc); + ASSERT_EQ(0, XMEMCMP(rx, publish, sizeof(publish))); + ASSERT_EQ(0, client.read.pos); + ASSERT_EQ((int)sizeof(publish), fixture.calls); + + MqttClient_DeInit(&client); +} + /* ============================================================================ * UTF-8 well-formedness validation [MQTT-1.5.3-1] * @@ -1540,6 +1781,35 @@ TEST(encode_publish_v5_response_topic_wildcard_rejected) MqttProps_Free(pub.props); } +/* MqttProps_FindType is public API: it must return the first property of the + * requested type, NULL when the list has none, and NULL for an empty list. */ +TEST(props_find_type) +{ + MqttProp* head = NULL; + MqttProp* first; + MqttProp* second; + MqttProp* third; + + ASSERT_NULL(MqttProps_FindType(NULL, MQTT_PROP_USER_PROP)); + + first = MqttProps_Add(&head); + ASSERT_TRUE(first != NULL); + first->type = MQTT_PROP_CONTENT_TYPE; + second = MqttProps_Add(&head); + ASSERT_TRUE(second != NULL); + second->type = MQTT_PROP_USER_PROP; + third = MqttProps_Add(&head); + ASSERT_TRUE(third != NULL); + third->type = MQTT_PROP_USER_PROP; + + ASSERT_TRUE(MqttProps_FindType(head, MQTT_PROP_CONTENT_TYPE) == first); + /* The first match wins when a type repeats. */ + ASSERT_TRUE(MqttProps_FindType(head, MQTT_PROP_USER_PROP) == second); + ASSERT_NULL(MqttProps_FindType(head, MQTT_PROP_RESP_TOPIC)); + + MqttProps_Free(head); +} + /* A single message may not carry more than the internal * MQTT_MAX_PROPS (default 30) properties; otherwise a peer can saturate the * shared property pool. 40 User Property entries exceeds that cap. The cap @@ -2119,6 +2389,36 @@ TEST(decode_auth_v5_props_cannot_cross_packet_end) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(auth.props); } + +/* MQTT 5.0 section 3.15.2.2.2: it is a Protocol Error to omit the + * Authentication Method from an AUTH packet's properties. */ +TEST(decode_auth_v5_missing_auth_method_rejected) +{ + /* Remaining Length 6: Reason 0x18 (Continue authentication), Property + * Length 4, Reason String (0x1F) "r", no Authentication Method. */ + byte cont[] = { + 0xF0, 0x06, MQTT_REASON_CONT_AUTH, 0x04, + MQTT_PROP_REASON_STR, 0x00, 0x01, 'r' + }; + /* Same properties with Reason 0x00 (Success): the long form carries a + * property block, so the Authentication Method is still required. */ + byte success[] = { + 0xF0, 0x06, MQTT_REASON_SUCCESS, 0x04, + MQTT_PROP_REASON_STR, 0x00, 0x01, 'r' + }; + MqttAuth auth; + int rc; + + XMEMSET(&auth, 0, sizeof(auth)); + rc = MqttDecode_Auth(cont, (int)sizeof(cont), &auth); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(auth.props); + + XMEMSET(&auth, 0, sizeof(auth)); + rc = MqttDecode_Auth(success, (int)sizeof(success), &auth); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(auth.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-3.2.2-1] / [MQTT-3.2.2-4] CONNACK Flags receive-side validation. @@ -2201,6 +2501,37 @@ TEST(decode_connack_refused_without_session_present_accepted) ASSERT_TRUE(rc > 0); } +/* MQTT 3.1.1 section 3.2.2.3 assigns return codes 0-5 and reserves 6-255, so + * a CONNACK carrying a reserved code is a protocol violation, not an + * ordinary refusal. */ +TEST(decode_connack_v311_reserved_return_code_rejected) +{ + ASSERT_TRUE(decode_connack_flags(0x00, + MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH) > 0); /* 0x05, last assigned */ + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, + decode_connack_flags(0x00, 0x06)); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, + decode_connack_flags(0x00, 0xFF)); +} + +#ifdef WOLFMQTT_V5 +/* MQTT 5.0 section 3.2.2.2 lists the CONNACK Reason Codes; 0x01 is not one + * of them. Wire: CONNACK, Remaining Length 3, flags 0, reason 0x01, + * Property Length 0. */ +TEST(decode_connack_v5_unassigned_reason_code_rejected) +{ + byte buf[] = { 0x20, 0x03, 0x00, 0x01, 0x00 }; + MqttConnectAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_ConnectAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(ack.props); +} +#endif + #ifdef WOLFMQTT_BROKER /* [MQTT-3.2.2-4] Session Present must be zero when a connection is refused. * The encoder must not produce a CONNACK that the matching decoder rejects. */ @@ -3533,6 +3864,77 @@ TEST(encode_connect_v5_invalid_will_property_rejected) ASSERT_EQ(0xA5, buf[0]); } +/* [MQTT-3.1.3.2] Payload Format Indicator and Content Type are Will + * Properties, so a CONNECT carrying them in the Will must encode. */ +TEST(encode_connect_v5_will_publish_properties_accepted) +{ + byte buf[64]; + MqttConnect connect; + MqttMessage lwt; + MqttProp pfi; + MqttProp content_type; + int rc; + /* MQTT 5.0 section 3.1: CONNECT, Remaining Length 28 */ + static const byte expected[] = { + 0x10, 0x1C, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x00, /* v5, Will Flag, Keep Alive 0 */ + 0x00, /* CONNECT Properties length 0 */ + 0x00, 0x03, 'c', 'i', 'd', /* Client Identifier */ + 0x06, /* Will Properties length 6 */ + 0x01, 0x01, /* Payload Format Indicator 1 */ + 0x03, 0x00, 0x01, 't', /* Content Type "t" */ + 0x00, 0x01, 'w', /* Will Topic */ + 0x00, 0x00 /* empty Will Payload */ + }; + + XMEMSET(&connect, 0, sizeof(connect)); + XMEMSET(&lwt, 0, sizeof(lwt)); + XMEMSET(&pfi, 0, sizeof(pfi)); + XMEMSET(&content_type, 0, sizeof(content_type)); + connect.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + connect.client_id = "cid"; + connect.enable_lwt = 1; + connect.lwt_msg = &lwt; + lwt.topic_name = "w"; + pfi.type = MQTT_PROP_PAYLOAD_FORMAT_IND; + pfi.data_byte = 1; + pfi.next = &content_type; + content_type.type = MQTT_PROP_CONTENT_TYPE; + content_type.data_str.str = (char*)"t"; + content_type.data_str.len = 1; + lwt.props = &pfi; + + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ((int)sizeof(expected), rc); + ASSERT_EQ(0, XMEMCMP(expected, buf, sizeof(expected))); +} + +/* MQTT 5.0 section 3.1.2.11: Message Expiry Interval and Will Delay Interval + * are not CONNECT Properties; they belong only in the Will Properties. */ +TEST(encode_connect_v5_will_only_properties_in_connect_rejected) +{ + byte buf[64]; + MqttConnect connect; + MqttProp prop; + int rc; + + XMEMSET(&connect, 0, sizeof(connect)); + XMEMSET(&prop, 0, sizeof(prop)); + connect.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + connect.client_id = "cid"; + connect.props = ∝ + prop.data_int = 30; + + prop.type = MQTT_PROP_MSG_EXPIRY_INTERVAL; + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + + prop.type = MQTT_PROP_WILL_DELAY_INTERVAL; + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); +} + TEST(encode_connect_v5_cyclic_will_properties_rejected) { byte buf[256]; @@ -5288,6 +5690,25 @@ TEST(decode_suback_v5_props_freed_on_payload_overrun) ASSERT_NULL(ack.props); } +/* MQTT 5.0 section 3.9.2: the v5 SUBACK variable header is the Packet + * Identifier followed by the Property Length, which is mandatory even when + * zero (section 2.2.2.1). A packet carrying only the Packet Identifier is + * malformed and must not decode as a SUBACK with zero reason codes. */ +TEST(decode_suback_v5_missing_props_len_rejected) +{ + byte buf[] = { + 0x90, 0x02, /* SUBACK, remain_len = 2 */ + 0x00, 0x01 /* packet_id, nothing after it */ + }; + MqttSubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_SubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); +} + /* Pin v5's broadened set via the helper. */ TEST(suback_return_code_v5_allowed_set) { @@ -5870,6 +6291,24 @@ TEST(decode_unsuback_v5_props_past_remain_len_rejected) rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); } + +/* MQTT 5.0 section 3.11.2: the v5 UNSUBACK variable header is the Packet + * Identifier followed by the mandatory Property Length (section 2.2.2.1). A + * packet carrying only the Packet Identifier is malformed. */ +TEST(decode_unsuback_v5_missing_props_len_rejected) +{ + byte buf[] = { + 0xB0, 0x02, /* UNSUBACK, remain_len = 2 */ + 0x00, 0x01 /* packet_id, nothing after it */ + }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); +} #endif /* WOLFMQTT_V5 */ #ifdef WOLFMQTT_BROKER @@ -6591,6 +7030,39 @@ TEST(encode_props_boolean_byte_out_of_range_rejected) ASSERT_TRUE(rc > 0); } +/* MQTT 5.0 sections 3.1.2.11.3 and 3.2.2.3.3: a Receive Maximum of 0 is a + * Protocol Error in both CONNECT and CONNACK. The encoder must refuse it in + * the length pass (buf == NULL) and the output pass. */ +TEST(encode_props_receive_max_zero_rejected) +{ + MqttProp prop; + byte buf[8]; + /* Receive Maximum = 1: identifier 0x21, Two Byte Integer 0x0001 + * (MQTT 5.0 sections 2.2.2.2 and 1.5.2) */ + static const byte expect[] = { 0x21, 0x00, 0x01 }; + int rc; + + XMEMSET(&prop, 0, sizeof(prop)); + prop.type = MQTT_PROP_RECEIVE_MAX; + prop.data_short = 0; + prop.next = NULL; + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, NULL); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, buf); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT_ACK, &prop, NULL); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT_ACK, &prop, buf); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + + /* The smallest legal value still encodes. */ + prop.data_short = 1; + XMEMSET(buf, 0, sizeof(buf)); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, buf); + ASSERT_EQ((int)sizeof(expect), rc); + ASSERT_EQ(0, XMEMCMP(buf, expect, sizeof(expect))); +} + /* ============================================================================ * MqttEncode/Decode_Auth roundtrip * @@ -6717,6 +7189,33 @@ TEST(auth_v5_success_no_props_roundtrip) ASSERT_NULL(dec.props); } +/* MQTT 5.0 section 3.15.2.2.2: an AUTH that carries Properties must include + * the Authentication Method; omitting it is a Protocol Error. The short form + * of section 3.15.2.1 applies only to a Success AUTH with no Properties, so a + * Success AUTH with only a Reason String must be refused, not encoded. */ +TEST(encode_auth_v5_success_props_without_method_rejected) +{ + byte buf[32]; + MqttAuth enc; + MqttProp prop; + int rc; + char reason[] = "ok"; + + XMEMSET(&enc, 0, sizeof(enc)); + XMEMSET(&prop, 0, sizeof(prop)); + XMEMSET(buf, 0xA5, sizeof(buf)); + prop.type = MQTT_PROP_REASON_STR; + prop.data_str.str = reason; + prop.data_str.len = (word16)XSTRLEN(reason); + prop.next = NULL; + enc.reason_code = MQTT_REASON_SUCCESS; + enc.props = ∝ + + rc = MqttEncode_Auth(buf, (int)sizeof(buf), &enc); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + ASSERT_EQ(0xA5, buf[0]); +} + TEST(auth_v5_invalid_reason_code_rejected) { /* A reason code outside {SUCCESS, CONT_AUTH, REAUTH} must not decode @@ -6804,6 +7303,74 @@ TEST(decode_connect_v5_will_props_session_expiry_rejected) rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); } + +/* [MQTT-3.1.3.2] Payload Format Indicator and Content Type are valid Will + * Properties and must decode. */ +TEST(decode_connect_v5_will_publish_properties_accepted) +{ + /* Hand-built v5 CONNECT, same layout as + * encode_connect_v5_will_publish_properties_accepted. */ + byte buf[] = { + 0x10, 0x1C, /* CONNECT, Remaining Length 28 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', /* Protocol Name */ + 0x05, 0x04, 0x00, 0x00, /* v5, Will Flag, Keep Alive */ + 0x00, /* CONNECT Properties length */ + 0x00, 0x03, 'c', 'i', 'd', /* Client Identifier */ + 0x06, /* Will Properties length 6 */ + 0x01, 0x01, /* Payload Format Indicator 1 */ + 0x03, 0x00, 0x01, 't', /* Content Type "t" */ + 0x00, 0x01, 'w', /* Will Topic */ + 0x00, 0x00 /* empty Will Payload */ + }; + MqttConnect dec; + MqttMessage dec_lwt; + MqttProp* prop; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + XMEMSET(&dec_lwt, 0, sizeof(dec_lwt)); + dec.lwt_msg = &dec_lwt; + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ((int)sizeof(buf), rc); + prop = dec_lwt.props; + ASSERT_NOT_NULL(prop); + ASSERT_EQ(MQTT_PROP_PAYLOAD_FORMAT_IND, prop->type); + ASSERT_EQ(1, prop->data_byte); + prop = prop->next; + ASSERT_NOT_NULL(prop); + ASSERT_EQ(MQTT_PROP_CONTENT_TYPE, prop->type); + ASSERT_EQ(1, prop->data_str.len); + ASSERT_EQ('t', prop->data_str.str[0]); + ASSERT_NULL(prop->next); + MqttProps_Free(dec_lwt.props); + MqttProps_Free(dec.props); +} + +/* MQTT 5.0 section 3.1.2.11: Message Expiry Interval (0x02) and Will Delay + * Interval (0x18) are not CONNECT Properties. */ +TEST(decode_connect_v5_will_only_properties_in_connect_rejected) +{ + byte buf[] = { + 0x10, 0x15, /* CONNECT, Remaining Length 21 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', /* Protocol Name */ + 0x05, 0x02, 0x00, 0x00, /* v5, Clean Start, Keep Alive */ + 0x05, 0x02, 0x00, 0x00, 0x00, 0x1E, /* Property, four byte value 30 */ + 0x00, 0x03, 'c', 'i', 'd' /* Client Identifier */ + }; + MqttConnect dec; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY_MISMATCH, rc); + MqttProps_Free(dec.props); + + buf[13] = MQTT_PROP_WILL_DELAY_INTERVAL; + XMEMSET(&dec, 0, sizeof(dec)); + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY_MISMATCH, rc); + MqttProps_Free(dec.props); +} #endif /* WOLFMQTT_BROKER && WOLFMQTT_V5 */ #ifdef WOLFMQTT_V5 @@ -6984,6 +7551,24 @@ TEST(decode_props_auth_data_without_method_rejected) ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); ASSERT_NULL(props); } + +/* MQTT 5.0 section 2.2.2.1: the Property Length counts every byte of the + * properties, and section 2.2.2.2 encodes each Identifier as a Variable Byte + * Integer. An Identifier whose encoding runs past the declared Property + * Length is a Malformed Packet, and must be rejected before the remaining + * length is charged for it. Wire: Property Length 1, Identifier 0x80 0x01 + * (two bytes, value 128). */ +TEST(decode_props_identifier_past_prop_len_rejected) +{ + byte buf[] = { 0x80, 0x01 }; + MqttProp* props = NULL; + int rc; + + rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &props, buf, + (word32)sizeof(buf), 1); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(props); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -7032,6 +7617,11 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_vbi_overlong_2byte_127); RUN_TEST(encode_decode_vbi_roundtrip); RUN_TEST(packet_read_rejects_buffer_shorter_than_max_header); + RUN_TEST(packet_read_write_reject_negative_socket_pos); + RUN_TEST(packet_read_rejects_corrupt_resume_state); + RUN_TEST(packet_read_write_total_saturates); + RUN_TEST(packet_read_write_clamp_over_reported_length); + RUN_TEST(packet_read_reassembles_one_byte_reads); /* UTF-8 well-formedness validation [MQTT-1.5.3-1] */ #ifdef WOLFMQTT_BROKER @@ -7105,6 +7695,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_byte_property_out_of_range_rejected); RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); RUN_TEST(encode_publish_v5_response_topic_wildcard_rejected); + RUN_TEST(props_find_type); 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); @@ -7142,6 +7733,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_disconnect_v5_props_cannot_cross_packet_end); RUN_TEST(decode_auth_v5_rejects_bytes_after_property_block); RUN_TEST(decode_auth_v5_props_cannot_cross_packet_end); + RUN_TEST(decode_auth_v5_missing_auth_method_rejected); #endif RUN_TEST(decode_connack_flags_session_present_accepted); RUN_TEST(decode_connack_flags_no_session_accepted); @@ -7151,6 +7743,10 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_connack_flags_all_bits_rejected); RUN_TEST(decode_connack_refused_with_session_present_rejected); RUN_TEST(decode_connack_refused_without_session_present_accepted); + RUN_TEST(decode_connack_v311_reserved_return_code_rejected); +#ifdef WOLFMQTT_V5 + RUN_TEST(decode_connack_v5_unassigned_reason_code_rejected); +#endif #ifdef WOLFMQTT_BROKER RUN_TEST(encode_connack_refused_with_session_present_rejected); #endif @@ -7228,6 +7824,8 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_connect_lwt_topic_oversized_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(encode_connect_v5_invalid_will_property_rejected); + RUN_TEST(encode_connect_v5_will_publish_properties_accepted); + RUN_TEST(encode_connect_v5_will_only_properties_in_connect_rejected); RUN_TEST(encode_connect_v5_cyclic_will_properties_rejected); RUN_TEST(encode_connect_v5_excess_will_properties_rejected); #endif @@ -7265,6 +7863,8 @@ void run_mqtt_packet_tests(void) 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); + RUN_TEST(decode_connect_v5_will_publish_properties_accepted); + RUN_TEST(decode_connect_v5_will_only_properties_in_connect_rejected); RUN_TEST(decode_connect_v5_empty_will_topic_rejected); #endif @@ -7329,6 +7929,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_suback_v5_not_authorized_accepted); RUN_TEST(suback_return_code_v5_allowed_set); RUN_TEST(decode_suback_v5_props_freed_on_payload_overrun); + RUN_TEST(decode_suback_v5_missing_props_len_rejected); #endif /* MqttDecode_PublishResp */ @@ -7371,6 +7972,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_v5_longer_body_still_accepted); RUN_TEST(decode_unsuback_v5_reason_codes); RUN_TEST(decode_unsuback_v5_props_past_remain_len_rejected); + RUN_TEST(decode_unsuback_v5_missing_props_len_rejected); #endif #ifdef WOLFMQTT_BROKER RUN_TEST(encode_unsuback_packet_id_zero_rejected); @@ -7422,17 +8024,20 @@ void run_mqtt_packet_tests(void) 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); + RUN_TEST(decode_props_identifier_past_prop_len_rejected); /* MqttEncode/Decode_Auth */ RUN_TEST(encode_props_string_invalid_utf8_rejected); RUN_TEST(encode_props_user_prop_invalid_utf8_rejected); RUN_TEST(encode_props_duplicate_repeatability); RUN_TEST(encode_props_boolean_byte_out_of_range_rejected); + RUN_TEST(encode_props_receive_max_zero_rejected); RUN_TEST(auth_v5_cont_auth_roundtrip); RUN_TEST(auth_v5_reauth_roundtrip); RUN_TEST(auth_v5_reauth_decodes_without_error); RUN_TEST(auth_v5_success_remaining_length_zero); RUN_TEST(auth_v5_success_no_props_roundtrip); + RUN_TEST(encode_auth_v5_success_props_without_method_rejected); RUN_TEST(auth_v5_invalid_reason_code_rejected); RUN_TEST(decode_auth_v5_reason_code_past_buf_rejected); #endif diff --git a/tests/test_mqtt_props_init.c b/tests/test_mqtt_props_init.c index 5b08a10ce..f7705952e 100644 --- a/tests/test_mqtt_props_init.c +++ b/tests/test_mqtt_props_init.c @@ -25,6 +25,24 @@ static wm_Sem* sem_watch_recv_lock; static MqttMsgStat* sem_watch_ping_stat; static int sem_stale_ping_locks; static int sem_watched_recv_locks; +static wm_Sem* sem_fail_lock; +static wm_Sem* sem_fail_lock_while_held; +static int sem_fail_lock_skip; +static int quota_write_calls; +static MqttClient* sem_flag_client; +static word32 sem_flag_bits; +static MqttClient* io_lock_client; +static int io_locks_held_at_close; +/* Counts lockClient acquisitions on sem_track_client made while + * sem_track_while_held is owned, and records which one changed + * sem_track_bit in client->flags, so a later run can fail exactly that one. */ +static MqttClient* sem_track_client; +static wm_Sem* sem_track_while_held; +static word32 sem_track_bit; +static word32 sem_track_before; +static int sem_track_count; +static int sem_track_counted; +static int sem_track_toggle_at; #ifdef ENABLE_MQTT_CURL #define TEST_CLIENT_LOCK_COUNT 4 @@ -52,6 +70,19 @@ static void reset_sem_state(void) sem_watch_ping_stat = NULL; sem_stale_ping_locks = 0; sem_watched_recv_locks = 0; + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + sem_fail_lock_skip = 0; + quota_write_calls = 0; + sem_flag_client = NULL; + sem_flag_bits = 0; + sem_track_client = NULL; + sem_track_while_held = NULL; + sem_track_bit = 0; + sem_track_before = 0; + sem_track_count = 0; + sem_track_counted = 0; + sem_track_toggle_at = 0; } int wm_SemInit(wm_Sem* sem) @@ -83,7 +114,33 @@ int wm_SemFree(wm_Sem* sem) int wm_SemLock(wm_Sem* sem) { + /* Fail one acquisition of sem_fail_lock made while + * sem_fail_lock_while_held is owned (or any acquisition when it is NULL), + * after skipping sem_fail_lock_skip such acquisitions, so a test can + * target a single lock inside a larger operation. */ + if (sem == sem_fail_lock && (sem_fail_lock_while_held == NULL || + sem_fail_lock_while_held->lock_count > 0)) { + if (sem_fail_lock_skip > 0) { + sem_fail_lock_skip--; + } + else { + sem_fail_lock = NULL; + return MQTT_CODE_ERROR_SYSTEM; + } + } sem->lock_count++; + if (sem_track_client != NULL && sem == &sem_track_client->lockClient && + (sem_track_while_held == NULL || + sem_track_while_held->lock_count > 0)) { + sem_track_count++; + sem_track_counted = 1; + sem_track_before = sem_track_client->flags & sem_track_bit; + } + /* Model another thread that updated client->flags under lockClient and + * released it just as this acquisition succeeded. */ + if (sem_flag_client != NULL && sem == &sem_flag_client->lockClient) { + sem_flag_client->flags |= sem_flag_bits; + } if (sem == sem_watch_recv_lock) { sem_watched_recv_locks++; } @@ -105,6 +162,15 @@ int wm_SemLock(wm_Sem* sem) int wm_SemUnlock(wm_Sem* sem) { + if (sem_track_client != NULL && sem == &sem_track_client->lockClient && + sem_track_counted) { + sem_track_counted = 0; + if (sem_track_toggle_at == 0 && + (sem_track_client->flags & sem_track_bit) != + sem_track_before) { + sem_track_toggle_at = sem_track_count; + } + } if (sem->lock_count == 0) { sem_invalid_unlock_calls++; } @@ -479,6 +545,479 @@ static int test_failed_property_free_retried(void) return 0; } +static int quota_net_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; + (void)buf; + (void)timeout_ms; + quota_write_calls++; + return buf_len; +} + +/* [MQTT-3.3.4-9] The Client MUST NOT send more QoS 1/2 PUBLISH packets than + * the server's Receive Maximum allows. If lockClient cannot be taken to + * reserve a unit, the quota was never checked, so the PUBLISH must be refused + * rather than sent. The quota is exhausted here so any send exceeds it. */ +static int test_recv_quota_lock_failure_refuses_publish(void) +{ + int rc; + MqttClient client; + MqttNet net; + MqttPublish publish; + byte tx_buf[64]; + byte rx_buf[64]; + byte payload[1] = { 'x' }; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + XMEMSET(&publish, 0, sizeof(publish)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = quota_net_write; + net.disconnect = ping_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "quota client initialization returned %d\n", rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, + MQTT_CLIENT_FLAG_CONNECT_SENT | MQTT_CLIENT_FLAG_IS_CONNECTED); + client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + client.server_recv_max_negotiated = 1; + client.server_recv_max = 0; + + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "t"; + publish.buffer = payload; + publish.total_len = (word32)sizeof(payload); + + /* With lockSend held, MqttWriteStart first takes lockClient to mark the + * writer active; the next lockClient acquisition is the Receive Maximum + * reservation, which is the one that fails. */ + sem_fail_lock = &client.lockClient; + sem_fail_lock_while_held = &client.lockSend; + sem_fail_lock_skip = 1; + rc = MqttClient_Publish(&client, &publish); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + + if (rc != MQTT_CODE_ERROR_SYSTEM || quota_write_calls != 0 || + publish.stat.recvQuotaHeld != 0 || client.server_recv_max != 0) { + fprintf(stderr, + "quota lock failure returned %d, writes=%d held=%d quota=%u\n", + rc, quota_write_calls, publish.stat.recvQuotaHeld, + (unsigned)client.server_recv_max); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + +/* client->flags is shared with other threads and guarded by lockClient in + * multithread builds, so the keep-alive check of NO_AUTO_KEEPALIVE must read + * it under that lock. Here another thread disables automatic keep-alive under + * lockClient; a read that bypasses the lock misses that update and sends a + * PINGREQ the application has turned off. The ping is otherwise due: + * keep-alive is negotiated and nothing has been sent since time 0. */ +static int test_keepalive_flag_read_takes_client_lock(void) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = quota_net_write; + net.disconnect = ping_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "keep-alive client initialization returned %d\n", rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_CONNECT_SENT | + MQTT_CLIENT_FLAG_IS_CONNECTED); + client.keep_alive_sec = 60; + client.last_tx_time = 0; + + sem_flag_client = &client; + sem_flag_bits = MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE; + rc = MqttClient_WaitMessage_ex(&client, &client.msg, 1000); + sem_flag_client = NULL; + + if (quota_write_calls != 0) { + fprintf(stderr, + "keep-alive ignored a locked flag update: rc=%d writes=%d\n", + rc, quota_write_calls); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + +static int io_lock_disconnect_calls; + +static int io_lock_net_disconnect(void* context) +{ + (void)context; + io_lock_disconnect_calls++; + if (io_lock_client != NULL) { + io_locks_held_at_close = + (io_lock_client->lockRecv.lock_count > 0 && + io_lock_client->lockSend.lock_count > 0); + } + return MQTT_CODE_SUCCESS; +} + +/* MqttClient_NetDisconnect_Wait releases the transport only while holding + * lockRecv and lockSend, so no read or write on another thread can still be + * using socket or TLS state when it is freed. The transport's disconnect + * callback records whether both I/O locks were held when it ran. */ +static int test_net_disconnect_wait_holds_io_locks(void) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = ping_net_write; + net.disconnect = io_lock_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "disconnect-wait client initialization returned %d\n", + rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + io_lock_client = &client; + io_locks_held_at_close = 0; + + rc = MqttClient_NetDisconnect_Wait(&client); + io_lock_client = NULL; + + if (rc != MQTT_CODE_SUCCESS || !io_locks_held_at_close || + client.lockRecv.lock_count != 0 || + client.lockSend.lock_count != 0 || + (MqttClient_Flags(&client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED) != 0) { + fprintf(stderr, + "disconnect-wait rc=%d locks_held=%d recv=%d send=%d\n", + rc, io_locks_held_at_close, client.lockRecv.lock_count, + client.lockSend.lock_count); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + +/* If lockSend cannot be taken, MqttClient_NetDisconnect_Wait must release + * the lockRecv it already holds and leave the transport alone; if lockRecv + * cannot be taken, it must not touch the transport either. */ +static int run_net_disconnect_wait_lock_failure(int fail_send) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = ping_net_write; + net.disconnect = io_lock_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "lock-failure client initialization returned %d\n", + rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + io_lock_disconnect_calls = 0; + if (fail_send) { + sem_fail_lock = &client.lockSend; + sem_fail_lock_while_held = &client.lockRecv; + } + else { + sem_fail_lock = &client.lockRecv; + sem_fail_lock_while_held = NULL; + } + + rc = MqttClient_NetDisconnect_Wait(&client); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + + if (rc != MQTT_CODE_ERROR_SYSTEM || io_lock_disconnect_calls != 0 || + client.lockRecv.lock_count != 0 || + client.lockSend.lock_count != 0 || + (MqttClient_Flags(&client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED) == 0) { + fprintf(stderr, + "disconnect-wait fail_send=%d rc=%d closes=%d recv=%d send=%d\n", + fail_send, rc, io_lock_disconnect_calls, + client.lockRecv.lock_count, client.lockSend.lock_count); + fail = 1; + } + + MqttClient_DeInit(&client); + return fail; +} + +typedef struct AuthWindowNet { + int connack_pos; + int writes; +} AuthWindowNet; + +/* v5 CONNACK (MQTT 5.0 section 3.2): accepted, Session Present 0, Property + * Length 0. */ +static const byte auth_window_connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; + +static int auth_window_net_read(void* context, byte* buf, int buf_len, + int timeout_ms) +{ + AuthWindowNet* an = (AuthWindowNet*)context; + int n = (int)sizeof(auth_window_connack) - an->connack_pos; + + (void)timeout_ms; + if (n <= 0) { + return MQTT_CODE_ERROR_TIMEOUT; + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, auth_window_connack + an->connack_pos, (size_t)n); + an->connack_pos += n; + return n; +} + +static int auth_window_net_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + AuthWindowNet* an = (AuthWindowNet*)context; + + (void)buf; + (void)timeout_ms; + an->writes++; + return buf_len; +} + +/* Runs one v5 CONNECT, carrying an Authentication Method when with_method is + * set, against a CONNACK. When fail_at is non-zero, the fail_at'th lockClient + * acquisition made while while_held is owned fails. Otherwise the run records + * in sem_track_toggle_at which acquisition changed AUTH_PENDING. */ +static int run_auth_window_connect(MqttClient* client, AuthWindowNet* an, + int with_method, wm_Sem* (*while_held)(MqttClient*), int fail_at) +{ + MqttConnect connect; + MqttProp method_prop; + int rc; + + XMEMSET(an, 0, sizeof(*an)); + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "auth"; + if (with_method) { + XMEMSET(&method_prop, 0, sizeof(method_prop)); + method_prop.type = MQTT_PROP_AUTH_METHOD; + method_prop.data_str.str = (char*)"m"; + method_prop.data_str.len = 1; + connect.props = &method_prop; + } + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_CONNECT_SENT, + MQTT_CLIENT_FLAG_IS_CONNECTED); + + sem_track_client = client; + sem_track_while_held = while_held(client); + sem_track_bit = MQTT_CLIENT_FLAG_AUTH_PENDING; + sem_track_count = 0; + sem_track_counted = 0; + sem_track_toggle_at = 0; + if (fail_at > 0) { + sem_fail_lock = &client->lockClient; + sem_fail_lock_while_held = while_held(client); + sem_fail_lock_skip = fail_at - 1; + } + rc = MqttClient_Connect(client, &connect); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + sem_fail_lock_skip = 0; + sem_track_client = NULL; + return rc; +} + +static wm_Sem* auth_window_send_lock(MqttClient* client) +{ + return &client->lockSend; +} + +static wm_Sem* auth_window_recv_lock(MqttClient* client) +{ + return &client->lockRecv; +} + +static int auth_window_client_init(MqttClient* client, MqttNet* net, + AuthWindowNet* an, byte* tx_buf, byte* rx_buf, int buf_len) +{ + XMEMSET(net, 0, sizeof(*net)); + net->context = an; + net->connect = ping_net_connect; + net->read = auth_window_net_read; + net->write = auth_window_net_write; + net->disconnect = ping_net_disconnect; + return MqttClient_Init(client, net, NULL, tx_buf, buf_len, rx_buf, + buf_len, 1000); +} + +/* [MQTT-3.1.2-30] With an Authentication Method only AUTH or DISCONNECT may + * follow CONNECT until the CONNACK. If lockClient fails while raising that + * window, the CONNECT must not be sent, since nothing would hold back other + * packets, and no lock or pending response may be left behind. */ +static int test_auth_window_set_lock_failure(void) +{ + int rc; + int set_at; + MqttClient client; + MqttNet net; + AuthWindowNet an; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "auth-window client initialization returned %d\n", + rc); + return 1; + } + + /* Learn which acquisition under lockSend raises the window. */ + rc = run_auth_window_connect(&client, &an, 1, auth_window_send_lock, 0); + set_at = sem_track_toggle_at; + MqttClient_DeInit(&client); + if (rc != MQTT_CODE_SUCCESS || set_at == 0) { + fprintf(stderr, "auth-window dry run rc=%d set_at=%d\n", rc, set_at); + return 1; + } + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + return 1; + } + rc = run_auth_window_connect(&client, &an, 1, auth_window_send_lock, + set_at); + if (rc != MQTT_CODE_ERROR_SYSTEM || an.writes != 0 || + client.lockSend.lock_count != 0 || + client.lockClient.lock_count != 0 || + client.firstPendResp != NULL || + (client.flags & MQTT_CLIENT_FLAG_CONNECT_SENT) != 0) { + fprintf(stderr, + "auth-window set failure rc=%d writes=%d send=%d client=%d " + "pend=%p flags=0x%x\n", rc, an.writes, + client.lockSend.lock_count, client.lockClient.lock_count, + (void*)client.firstPendResp, (unsigned)client.flags); + fail = 1; + } + + MqttClient_DeInit(&client); + return fail; +} + +/* If lockClient fails while the CONNACK closes the authentication window, the + * CONNACK must fail rather than leave the window silently open-ended, and the + * next CONNECT must reset the stale flag. */ +static int test_auth_window_clear_lock_failure(void) +{ + int rc; + int clear_at; + MqttClient client; + MqttNet net; + AuthWindowNet an; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "auth-window client initialization returned %d\n", + rc); + return 1; + } + + /* Learn which acquisition under lockRecv closes the window. */ + rc = run_auth_window_connect(&client, &an, 1, auth_window_recv_lock, 0); + clear_at = sem_track_toggle_at; + MqttClient_DeInit(&client); + if (rc != MQTT_CODE_SUCCESS || clear_at == 0) { + fprintf(stderr, "auth-window dry run rc=%d clear_at=%d\n", rc, + clear_at); + return 1; + } + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + return 1; + } + rc = run_auth_window_connect(&client, &an, 1, auth_window_recv_lock, + clear_at); + if (rc != MQTT_CODE_ERROR_SYSTEM || + client.lockRecv.lock_count != 0 || + client.lockClient.lock_count != 0) { + fprintf(stderr, "auth-window clear failure rc=%d recv=%d client=%d\n", + rc, client.lockRecv.lock_count, client.lockClient.lock_count); + fail = 1; + } + + /* A later CONNECT without a method resets the window it left set. */ + if (!fail) { + rc = run_auth_window_connect(&client, &an, 0, auth_window_send_lock, + 0); + if (rc != MQTT_CODE_SUCCESS || + (client.flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { + fprintf(stderr, "auth-window reconnect rc=%d flags=0x%x\n", rc, + (unsigned)client.flags); + fail = 1; + } + } + + MqttClient_DeInit(&client); + return fail; +} + int main(void) { if (test_sn_null_ping_resets_terminal_read_state() != 0) { @@ -502,5 +1041,26 @@ int main(void) if (test_repeated_client_deinit_preserves_property_owner() != 0) { return 1; } + if (test_recv_quota_lock_failure_refuses_publish() != 0) { + return 1; + } + if (test_keepalive_flag_read_takes_client_lock() != 0) { + return 1; + } + if (test_net_disconnect_wait_holds_io_locks() != 0) { + return 1; + } + if (run_net_disconnect_wait_lock_failure(1) != 0) { + return 1; + } + if (run_net_disconnect_wait_lock_failure(0) != 0) { + return 1; + } + if (test_auth_window_set_lock_failure() != 0) { + return 1; + } + if (test_auth_window_clear_lock_failure() != 0) { + return 1; + } return 0; } diff --git a/tests/test_mqtt_sem_config.h b/tests/test_mqtt_sem_config.h new file mode 100644 index 000000000..b5e876a20 --- /dev/null +++ b/tests/test_mqtt_sem_config.h @@ -0,0 +1,23 @@ +/* Force the multithreaded client and rename the POSIX primitives behind + * wm_SemInit/wm_SemFree so the dedicated semaphore test can make their + * initialization fail deterministically. */ +#include "wolfmqtt/options.h" + +#ifndef WOLFMQTT_MULTITHREAD + #define WOLFMQTT_MULTITHREAD +#endif + +#if defined(__FreeBSD__) || defined(__linux__) || defined(__QNX__) + /* Declare the real primitives before renaming them. */ + #include + int wolfmqtt_test_pthread_mutex_init(pthread_mutex_t* m, + const pthread_mutexattr_t* a); + int wolfmqtt_test_pthread_mutex_destroy(pthread_mutex_t* m); + int wolfmqtt_test_pthread_cond_init(pthread_cond_t* c, + const pthread_condattr_t* a); + int wolfmqtt_test_pthread_cond_destroy(pthread_cond_t* c); + #define pthread_mutex_init wolfmqtt_test_pthread_mutex_init + #define pthread_mutex_destroy wolfmqtt_test_pthread_mutex_destroy + #define pthread_cond_init wolfmqtt_test_pthread_cond_init + #define pthread_cond_destroy wolfmqtt_test_pthread_cond_destroy +#endif diff --git a/tests/test_mqtt_sem_init.c b/tests/test_mqtt_sem_init.c new file mode 100644 index 000000000..7a62a89b9 --- /dev/null +++ b/tests/test_mqtt_sem_init.c @@ -0,0 +1,165 @@ +/* Native semaphore initialization failure tests with deterministic POSIX + * primitive injection. */ + +#include +#include + +#include "wolfmqtt/mqtt_client.h" + +#ifdef WOLFMQTT_POSIX_SEMAPHORES + +/* The config header renames these in every translation unit; call the real + * primitives from the hooks below. */ +#undef pthread_mutex_init +#undef pthread_mutex_destroy +#undef pthread_cond_init +#undef pthread_cond_destroy + +static int mutex_fail_on; /* fail the Nth mutex init, 0 = never */ +static int cond_fail_on; /* fail the Nth cond init, 0 = never */ +static int mutex_inits; +static int cond_inits; +static int mutex_live; +static int cond_live; + +static void reset_counts(void) +{ + mutex_fail_on = 0; + cond_fail_on = 0; + mutex_inits = 0; + cond_inits = 0; + mutex_live = 0; + cond_live = 0; +} + +int wolfmqtt_test_pthread_mutex_init(pthread_mutex_t* m, + const pthread_mutexattr_t* a) +{ + int rc; + if (++mutex_inits == mutex_fail_on) { + return EAGAIN; + } + rc = pthread_mutex_init(m, a); + if (rc == 0) { + mutex_live++; + } + return rc; +} + +int wolfmqtt_test_pthread_mutex_destroy(pthread_mutex_t* m) +{ + mutex_live--; + return pthread_mutex_destroy(m); +} + +int wolfmqtt_test_pthread_cond_init(pthread_cond_t* c, + const pthread_condattr_t* a) +{ + int rc; + if (++cond_inits == cond_fail_on) { + return ENOMEM; + } + rc = pthread_cond_init(c, a); + if (rc == 0) { + cond_live++; + } + return rc; +} + +int wolfmqtt_test_pthread_cond_destroy(pthread_cond_t* c) +{ + cond_live--; + return pthread_cond_destroy(c); +} + +static int test_connect(void* context, const char* host, word16 port, + int timeout_ms) +{ + (void)context; + (void)host; + (void)port; + (void)timeout_ms; + return MQTT_CODE_SUCCESS; +} + +static int test_io(void* context, byte* buf, int buf_len, int timeout_ms) +{ + (void)context; + (void)buf; + (void)buf_len; + (void)timeout_ms; + return MQTT_CODE_ERROR_TIMEOUT; +} + +static int test_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; + (void)buf; + (void)timeout_ms; + return buf_len; +} + +static int test_disconnect(void* context) +{ + (void)context; + return MQTT_CODE_SUCCESS; +} + +/* A failed native mutex or condition-variable initialization must fail + * MqttClient_Init rather than mark the lock initialized, and must leave no + * native object behind once the failed init has cleaned up. */ +static int run_init_failure(const char* name, int fail_mutex, int fail_cond) +{ + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + int rc; + + reset_counts(); + mutex_fail_on = fail_mutex; + cond_fail_on = fail_cond; + XMEMSET(&net, 0, sizeof(net)); + net.connect = test_connect; + net.read = test_io; + net.write = test_write; + net.disconnect = test_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc == MQTT_CODE_SUCCESS) { + MqttClient_DeInit(&client); + fprintf(stderr, "%s: MqttClient_Init reported success\n", name); + return 1; + } + if (mutex_live != 0 || cond_live != 0) { + fprintf(stderr, "%s: rc=%d leaked mutexes=%d conds=%d\n", + name, rc, mutex_live, cond_live); + return 1; + } + return 0; +} + +int main(void) +{ + int fail = 0; + + /* First and a later lock: the first fails before anything else is set + * up, the later one after earlier locks were initialized. */ + fail |= run_init_failure("first mutex", 1, 0); + fail |= run_init_failure("second mutex", 2, 0); + fail |= run_init_failure("first cond", 0, 1); + fail |= run_init_failure("second cond", 0, 2); + return fail; +} + +#else + +int main(void) +{ + /* Only the POSIX semaphore backend is exercised here. */ + return 77; +} + +#endif /* WOLFMQTT_POSIX_SEMAPHORES */ diff --git a/tests/test_mqtt_sn.c b/tests/test_mqtt_sn.c index 112e13cc5..3b78f042b 100644 --- a/tests/test_mqtt_sn.c +++ b/tests/test_mqtt_sn.c @@ -1055,6 +1055,32 @@ TEST(sn_encode_publish_short_topic_valid) ASSERT_EQ('i', tx_buf[8]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_publish_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + byte payload[2] = { 'h', 'i' }; + SN_Publish publish; + int rc; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.topic_type = 3; + publish.topic_name = topic; + publish.packet_id = 0x1234; + publish.buffer = payload; + publish.total_len = sizeof(payload); + + rc = SN_Encode_Publish(tx_buf, (int)sizeof(tx_buf), &publish); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + publish.topic_type = 4; + rc = SN_Encode_Publish(tx_buf, (int)sizeof(tx_buf), &publish); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + TEST(sn_encode_publish_ind_form_valid) { /* A 300-byte payload pushes the packet past SN_PACKET_MAX_SMALL_SIZE, so the @@ -1525,6 +1551,28 @@ TEST(sn_encode_subscribe_short_topic_valid) ASSERT_EQ('p', tx_buf[6]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_subscribe_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + SN_Subscribe subscribe; + int rc; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + subscribe.topic_type = 3; + subscribe.topicNameId = topic; + subscribe.packet_id = 0x1234; + + rc = SN_Encode_Subscribe(tx_buf, (int)sizeof(tx_buf), &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + subscribe.topic_type = 4; + rc = SN_Encode_Subscribe(tx_buf, (int)sizeof(tx_buf), &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + /* ============================================================================ * SN_Encode_Unsubscribe * @@ -1633,6 +1681,28 @@ TEST(sn_encode_unsubscribe_short_topic_valid) ASSERT_EQ('p', tx_buf[6]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_unsubscribe_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + SN_Unsubscribe unsubscribe; + int rc; + + XMEMSET(&unsubscribe, 0, sizeof(unsubscribe)); + unsubscribe.topic_type = 3; + unsubscribe.topicNameId = topic; + unsubscribe.packet_id = 0x1234; + + rc = SN_Encode_Unsubscribe(tx_buf, (int)sizeof(tx_buf), &unsubscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + unsubscribe.topic_type = 4; + rc = SN_Encode_Unsubscribe(tx_buf, (int)sizeof(tx_buf), &unsubscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + /* ============================================================================ * SN_Encode_WillTopic * @@ -1971,6 +2041,7 @@ int main(int argc, char** argv) /* SN_Encode_Publish */ RUN_TEST(sn_encode_publish_short_topic_valid); + RUN_TEST(sn_encode_publish_reserved_topic_type_rejected); RUN_TEST(sn_encode_publish_ind_form_valid); RUN_TEST(sn_encode_publish_oversized_total_len_rejected); RUN_TEST(sn_encode_publish_total_len_int_max_rejected); @@ -1999,6 +2070,7 @@ int main(int argc, char** argv) RUN_TEST(sn_encode_subscribe_null_args_rejected); RUN_TEST(sn_encode_subscribe_normal_topic_valid); RUN_TEST(sn_encode_subscribe_short_topic_valid); + RUN_TEST(sn_encode_subscribe_reserved_topic_type_rejected); /* SN_Encode_Unsubscribe */ RUN_TEST(sn_encode_unsubscribe_null_topic_normal_rejected); @@ -2006,6 +2078,7 @@ int main(int argc, char** argv) RUN_TEST(sn_encode_unsubscribe_null_args_rejected); RUN_TEST(sn_encode_unsubscribe_normal_topic_valid); RUN_TEST(sn_encode_unsubscribe_short_topic_valid); + RUN_TEST(sn_encode_unsubscribe_reserved_topic_type_rejected); /* SN_Encode_WillTopic */ RUN_TEST(sn_encode_willtopic_null_topic_string_rejected); diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index c0cec1809..5bea2b320 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -101,6 +101,8 @@ typedef struct MockNet { int write_fail_rc; /* if nonzero, write() returns this instead of * accepting the buffer (simulate short/failed * write) */ + int write_fail_type; /* fail only this MQTT-SN packet type, 0 = + * every write */ int write_chunk; /* maximum bytes accepted per write */ int write_zero_count; /* zero-progress writes before accepting */ int read_zero_count; /* zero-progress reads before delivering */ @@ -145,6 +147,19 @@ typedef struct SnPingRaceCtx { } SnPingRaceCtx; static SnMtOnlyRaceCtx* g_mt_only_race; + +/* Stands in for a reader thread that claimed an early CONNACK and is still + * decoding into the SN_Connect when the Will exchange fails. */ +typedef struct SnConnackClaimCtx { + SN_Connect* connect; + pthread_t thread; + int started; + int released; + int sync_error; +} SnConnackClaimCtx; + +static SnConnackClaimCtx* g_connack_claim; +static void sn_connack_claim_start(void); static SnPingRaceCtx* g_ping_race; static int g_mt_only_reentrant_send_rc; static int g_mt_only_reentrant_wait_rc; @@ -233,7 +248,13 @@ static int mock_write(void *ctx, const byte* buf, int buf_len, int timeout_ms) #endif net->write_calls++; - if (net->write_fail_rc != 0) { + if (net->write_fail_rc != 0 && (net->write_fail_type == 0 || + (buf_len > 1 && buf[1] == net->write_fail_type))) { +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS + if (g_connack_claim != NULL) { + sn_connack_claim_start(); + } +#endif /* Simulate a short/failed write so the caller's rc != xfer error path is * exercised. The buffer is left as the caller encoded it (not captured), * matching a real transport that errored mid-send. */ @@ -480,6 +501,52 @@ static const byte SN_PUBACK_FRAME[] = { * ============================================================================ */ static MqttClient g_client; + +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS +static void* sn_connack_claim_thread(void* arg) +{ + SnConnackClaimCtx* claim = (SnConnackClaimCtx*)arg; + struct timespec delay; + + /* Long enough that a reset not waiting for the claim returns first. */ + delay.tv_sec = 0; + delay.tv_nsec = 100 * 1000 * 1000; + (void)nanosleep(&delay, NULL); + if (wm_SemLock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + return NULL; + } + claim->released = 1; + MqttClient_RespList_EndProcessing(&g_client, &claim->connect->pendResp); + if (wm_SemUnlock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + } + return NULL; +} + +/* Called once from the failing Will write, on the connecting thread. */ +static void sn_connack_claim_start(void) +{ + SnConnackClaimCtx* claim = g_connack_claim; + + g_connack_claim = NULL; + if (wm_SemLock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + return; + } + claim->connect->pendResp.packetProcessing = 1; + if (wm_SemUnlock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + } + if (pthread_create(&claim->thread, NULL, sn_connack_claim_thread, + claim) != 0) { + claim->sync_error = 1; + return; + } + claim->started = 1; +} +#endif + static MqttNet g_net; static MockNet g_mock; static byte g_tx[512]; @@ -861,6 +928,87 @@ TEST(sn_connect_lwt_no_continue) * sn_subscribe_rejected. Without the guard this connect would report * MQTT_CODE_SUCCESS over a link the gateway actually refused. Runs in every SN * build. */ +/* SN_Client_Connect is resumable and keyed off mc_connect->stat.write. When + * the Will exchange after CONNECT fails, the call must leave the object ready + * to start over: write state back at BEGIN and, in multithread builds, the + * CONNACK pending response unlinked. A retry with the same SN_Connect then + * sends a new CONNECT; the gateway only issues WILLTOPICREQ in reply to one + * (MQTT-SN 1.2 section 6.3). The first attempt fails because the WILLTOPIC + * write fails at the transport. */ +TEST(sn_connect_will_error_allows_retry) +{ + SN_Connect mc; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + g_mock.write_fail_rc = MQTT_CODE_ERROR_NETWORK; + g_mock.write_fail_type = SN_MSG_TYPE_WILLTOPIC; + + sn_will_setup_connect(&mc); + rc = sn_connect_pump(&mc, NULL); + + ASSERT_TRUE(rc < 0); + ASSERT_TRUE(rc != MQTT_CODE_CONTINUE); + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_NO_PENDRESP(); + + /* Retry with the same object: CONNECT goes out again first. */ + g_mock.write_fail_rc = 0; + g_mock.write_fail_type = 0; + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + g_mock.out_len = 0; + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(SN_RC_ACCEPTED, mc.ack.return_code); + ASSERT_TRUE(g_mock.out_len >= 2); + ASSERT_EQ(SN_MSG_TYPE_CONNECT, g_mock.out[1]); + ASSERT_NO_PENDRESP(); +} + +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS +/* A reader can claim an early CONNACK and be decoding into the SN_Connect + * when the Will exchange fails. The reset must wait for that claim to end + * before unlinking the entry and returning, since the caller may then free or + * reuse the object. */ +TEST(sn_connect_will_error_waits_for_connack_reader) +{ + SN_Connect mc; + SnConnackClaimCtx claim; + int released_at_return; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + g_mock.write_fail_rc = MQTT_CODE_ERROR_NETWORK; + g_mock.write_fail_type = SN_MSG_TYPE_WILLTOPIC; + XMEMSET(&claim, 0, sizeof(claim)); + claim.connect = &mc; + g_connack_claim = &claim; + + sn_will_setup_connect(&mc); + rc = sn_connect_pump(&mc, NULL); + ASSERT_EQ(0, wm_SemLock(&g_client.lockClient)); + released_at_return = claim.released; + ASSERT_EQ(0, wm_SemUnlock(&g_client.lockClient)); + g_connack_claim = NULL; + if (claim.started) { + ASSERT_EQ(0, pthread_join(claim.thread, NULL)); + } + + ASSERT_EQ(0, claim.sync_error); + ASSERT_EQ(1, claim.started); + ASSERT_EQ(1, released_at_return); + ASSERT_TRUE(rc < 0); + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_NO_PENDRESP(); +} +#endif + TEST(sn_connect_refused) { SN_Connect mc; @@ -907,6 +1055,57 @@ TEST(sn_will_payload_scrubbed_after_send) sn_will_scrub_check(0 /* no CONTINUE */); } +/* When the WILLMSG cannot be encoded (here the Will Message is larger than + * tx_buf), SN_WillMessage releases the writer after wiping tx_buf, like the + * other encode-failure paths. The wipe must cover the whole buffer: tx_buf is + * pre-filled to stand in for earlier traffic, and none of it may remain. */ +TEST(sn_willmsg_encode_failure_scrubs_tx_buf) +{ + SN_Connect mc; + int rc, i; + static byte big_msg[600]; /* larger than the 512-byte test tx_buf */ + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + XMEMSET(g_client.tx_buf, 0x5A, (size_t)g_client.tx_buf_len); + XMEMSET(big_msg, 'w', sizeof(big_msg)); + + sn_will_setup_connect(&mc); + mc.will.willMsg = big_msg; + mc.will.willMsgLen = (word16)sizeof(big_msg); + ASSERT_TRUE((int)sizeof(big_msg) > g_client.tx_buf_len); + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_TRUE(rc < 0); + ASSERT_TRUE(rc != MQTT_CODE_CONTINUE); + for (i = 0; i < g_client.tx_buf_len; i++) { + if (g_client.tx_buf[i] != 0) { + FAIL("tx_buf not wiped after WILLMSG encode failure"); + } + } + ASSERT_NO_PENDRESP(); + + /* The connect state is rewound, so a retry with the same SN_Connect and + * a Will Message that fits sends a new CONNECT and completes. */ + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_EQ(0, mc.will_done); /* SN_WILL_DONE_NONE, private to the client */ + mc.will.willMsg = (byte*)"offline"; + mc.will.willMsgLen = 7; + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + g_mock.out_len = 0; + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_mock.out_len >= 2); + ASSERT_EQ(SN_MSG_TYPE_CONNECT, g_mock.out[1]); + ASSERT_NO_PENDRESP(); +} + /* ============================================================================ * SN will-message-update scrub test (#3138) * @@ -924,6 +1123,38 @@ TEST(sn_willmsgupd_payload_scrubbed_after_send) sn_will_msg_update_scrub_check(0 /* no CONTINUE */); } +/* MQTT-SN 1.2 section 5.4.19: an empty WILLTOPICUPD (no Flags, no WillTopic) + * asks the gateway to delete the stored Will topic and Will message. + * SN_Client_WillTopicUpdate must send that form when the SN_Will carries no + * topic, then complete on the WILLTOPICRESP (section 5.4.20). */ +TEST(sn_willtopicupd_null_topic_sends_delete) +{ + SN_Will will; + int rc = MQTT_CODE_CONTINUE; + int i; + /* WILLTOPICUPD: Length=2, MsgType=0x1A, nothing else. */ + static const byte expect[] = { 0x02, SN_MSG_TYPE_WILLTOPICUPD }; + /* WILLTOPICRESP: Length=3, MsgType=0x1B, ReturnCode=accepted. */ + static const byte resp[] = { 0x03, SN_MSG_TYPE_WILLTOPICRESP, + SN_RC_ACCEPTED }; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + mock_net_push(&g_mock, resp, (int)sizeof(resp)); + + XMEMSET(&will, 0, sizeof(will)); + will.willTopic = NULL; + + for (i = 0; i < 50 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WillTopicUpdate(&g_client, &will); + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(SN_RC_ACCEPTED, will.resp.topicResp.return_code); + ASSERT_EQ((int)sizeof(expect), g_mock.out_len); + ASSERT_EQ(0, XMEMCMP(expect, g_mock.out, sizeof(expect))); + ASSERT_NO_PENDRESP(); +} + /* #3138 error-path coverage. The success-path tests above never reach the * rc != xfer branch because mock_write() always accepts the whole buffer, so * the error-path CLIENT_FORCE_ZERO (and the pendResp removal beside it) would go @@ -1043,6 +1274,38 @@ TEST(sn_will_response_reset_clears_return_code) } } } + +/* A reader that completes another thread's SN response must release its claim + * on the entry. MqttClient_CancelMessage waits while an entry is claimed, so a + * claim left set after the response completed would read as a reader still + * decoding into the caller's object. */ +TEST(sn_reader_releases_completed_pendresp) +{ + SN_Subscribe sub; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + XMEMSET(&sub, 0, sizeof(sub)); + + ASSERT_EQ(0, wm_SemLock(&g_client.lockClient)); + rc = MqttClient_RespList_Add(&g_client, + (MqttPacketType)SN_MSG_TYPE_SUBACK, SN_TEST_SUB_PACKET_ID, + &sub.pendResp, &sub.subAck); + ASSERT_EQ(0, wm_SemUnlock(&g_client.lockClient)); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + /* A generic reader routes the SUBACK into the subscriber's object. */ + mock_net_push(&g_mock, SUBACK_FRAME, (int)sizeof(SUBACK_FRAME)); + (void)SN_Client_WaitMessage(&g_client, 1000); + + ASSERT_EQ(1, sub.pendResp.packetDone); + ASSERT_EQ(0, sub.pendResp.packetProcessing); + ASSERT_EQ(SN_TEST_SUB_TOPIC_ID, sub.subAck.topicId); + + rc = MqttClient_CancelMessage(&g_client, (MqttObject*)&sub); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_NO_PENDRESP(); +} #endif /* WOLFMQTT_MULTITHREAD */ /* ============================================================================ @@ -1276,6 +1539,209 @@ TEST(sn_publish_incoming_null_msg_cb_errors_no_ack) ASSERT_EQ(0, g_mock.write_calls); } +static int g_sn_wait_deliveries; +static byte g_sn_wait_last_byte; + +static int sn_record_delivery(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg_new; + (void)msg_done; + g_sn_wait_deliveries++; + if (msg->buffer != NULL && msg->buffer_len > 0) { + g_sn_wait_last_byte = msg->buffer[0]; + } + return MQTT_CODE_SUCCESS; +} + +/* SN_Client_WaitMessage waits on the client's shared msgSN object. Once a + * packet has been handled the next call must read a new one, not handle the + * previous packet again from its leftover payload state. Two QoS 0 PUBLISH + * frames (MQTT-SN 1.2 section 5.4.12: Length, MsgType, Flags, TopicId, + * MsgId, Data) with different data, waited for back to back. */ +TEST(sn_wait_message_reads_each_packet_once) +{ + static const byte pub1[] = { 0x08, SN_MSG_TYPE_PUBLISH, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x11 }; + static const byte pub2[] = { 0x08, SN_MSG_TYPE_PUBLISH, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x22 }; + int rc; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_record_delivery; + g_sn_wait_deliveries = 0; + g_sn_wait_last_byte = 0; + mock_net_push(&g_mock, pub1, (int)sizeof(pub1)); + mock_net_push(&g_mock, pub2, (int)sizeof(pub2)); + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(1, g_sn_wait_deliveries); + ASSERT_EQ(0x11, g_sn_wait_last_byte); + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(2, g_mock.in_idx); + ASSERT_EQ(2, g_sn_wait_deliveries); + ASSERT_EQ(0x22, g_sn_wait_last_byte); +} + +#if WOLFMQTT_MAX_QOS >= 2 +static int g_sn_qos2_deliveries; + +static int sn_count_qos2_delivery(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg; + (void)msg_new; + (void)msg_done; + g_sn_qos2_deliveries++; + return MQTT_CODE_SUCCESS; +} + +/* Count short-form (1-byte length) MQTT-SN frames of a type in the capture. */ +static int sn_out_count(const MockNet* net, byte packet_type) +{ + int count = 0; + int pos = 0; + + while (pos + 1 < net->out_len && net->out[pos] >= 2) { + if (net->out[pos + 1] == packet_type) { + count++; + } + pos += net->out[pos]; + } + return count; +} + +/* MQTT-SN 1.2 section 6.7: a QoS 2 PUBLISH is answered with PUBREC and its + * delivery completes with PUBREL/PUBCOMP. A retransmitted PUBLISH (DUP set) + * for an id still awaiting PUBREL must be acknowledged again but not handed + * to the application a second time. Once PUBREL has released the id, a new + * PUBLISH may reuse it and is delivered. Frames: PUBLISH QoS 2 id 7 (flags + * 0x40), its DUP retransmit (0xC0), PUBREL id 7, a new PUBLISH id 7. */ +TEST(sn_qos2_retransmit_not_delivered_twice) +{ + static const byte pub[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x00, 0x07, 0x01, 0x02 }; + static const byte dup[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0xC0, 0x00, 0x0A, + 0x00, 0x07, 0x01, 0x02 }; + static const byte rel[] = { 0x04, SN_MSG_TYPE_PUBREL, 0x00, 0x07 }; + static const byte pub2[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x00, 0x07, 0x03, 0x04 }; + int rc = MQTT_CODE_SUCCESS; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_count_qos2_delivery; + g_sn_qos2_deliveries = 0; + mock_net_push(&g_mock, pub, (int)sizeof(pub)); + mock_net_push(&g_mock, dup, (int)sizeof(dup)); + mock_net_push(&g_mock, rel, (int)sizeof(rel)); + mock_net_push(&g_mock, pub2, (int)sizeof(pub2)); + + for (i = 0; i < 40 && g_mock.in_idx < g_mock.in_count; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + if (rc != MQTT_CODE_SUCCESS && rc != MQTT_CODE_CONTINUE) { + FAIL("SN_Client_WaitMessage failed"); + } + } + /* Let a pending response from the last frame finish. */ + for (i = 0; i < 4 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + + ASSERT_EQ(g_mock.in_count, g_mock.in_idx); + ASSERT_EQ(2, g_sn_qos2_deliveries); + ASSERT_EQ(3, sn_out_count(&g_mock, SN_MSG_TYPE_PUBREC)); + ASSERT_EQ(1, sn_out_count(&g_mock, SN_MSG_TYPE_PUBCOMP)); +} + +/* A new QoS 2 id that does not fit the de-duplication table cannot be + * tracked, so it is refused with MQTT_CODE_ERROR_PACKET_ID rather than + * delivered: delivered untracked, its retransmit would reach the application + * again (MQTT-SN 1.2 section 6.7). No PUBREC is sent. */ +TEST(sn_qos2_full_table_refuses_new_id) +{ + static const byte pub[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x01, 0x00, 0x01, 0x02 }; /* id 0x0100 */ + int rc = MQTT_CODE_CONTINUE; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_count_qos2_delivery; + g_sn_qos2_deliveries = 0; + for (i = 0; i < MQTT_MAX_RECV_QOS2; i++) { + g_client.recv_qos2_pending[i] = (word16)(i + 1); + } + mock_net_push(&g_mock, pub, (int)sizeof(pub)); + + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + + ASSERT_EQ(MQTT_CODE_ERROR_PACKET_ID, rc); + ASSERT_EQ(0, g_sn_qos2_deliveries); + ASSERT_EQ(0, sn_out_count(&g_mock, SN_MSG_TYPE_PUBREC)); +} + +/* A clean-session CONNECT starts with no inbound QoS 2 flows, so the + * de-duplication table is cleared once the gateway accepts it; a + * non-clean-session CONNECT resumes and keeps it, but only for the ClientId + * whose Session established it. The first CONNECT always uses the default + * ClientId; the reconnect uses client_id. */ +static int sn_connect_keeps_qos2_id(int clean_session, const char* client_id) +{ + SN_Connect mc; + int rc; + + if (sn_client_init(0) != MQTT_CODE_SUCCESS) { + return -1; + } + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + sn_will_setup_connect(&mc); + mc.enable_lwt = 0; + rc = sn_connect_pump(&mc, NULL); + if (rc != MQTT_CODE_SUCCESS) { + return -1; + } + g_client.recv_qos2_pending[0] = 0x0042; + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + sn_will_setup_connect(&mc); + mc.enable_lwt = 0; + mc.clean_session = (byte)clean_session; + mc.client_id = client_id; + rc = sn_connect_pump(&mc, NULL); + if (rc != MQTT_CODE_SUCCESS) { + return -1; + } + return g_client.recv_qos2_pending[0] == 0x0042; +} + +TEST(sn_qos2_table_cleared_by_clean_session_connect) +{ + ASSERT_EQ(0, sn_connect_keeps_qos2_id(1, "wolfMQTT-sn-test")); + ASSERT_EQ(1, sn_connect_keeps_qos2_id(0, "wolfMQTT-sn-test")); +} + +/* A non-clean CONNECT under a different ClientId is a different Session, so + * a pending id from the previous one must not suppress a new QoS 2 PUBLISH + * that reuses it. */ +TEST(sn_qos2_table_cleared_by_client_id_change) +{ + ASSERT_EQ(0, sn_connect_keeps_qos2_id(0, "wolfMQTT-sn-other")); +} +#endif /* WOLFMQTT_MAX_QOS >= 2 */ + #ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS static int sn_mt_only_packet_count(const MockNet* net, byte packet_type) { @@ -2707,6 +3173,42 @@ TEST(sn_publish_qos1_zero_write_returns_network_error) ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); ASSERT_NO_PENDRESP(); } + +static int g_zero_puback_publish_calls; + +static int sn_count_incoming_publish(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg; + (void)msg_new; + (void)msg_done; + g_zero_puback_publish_calls++; + return MQTT_CODE_SUCCESS; +} + +/* MQTT-SN 1.2 section 5.4.4: a QoS 1 PUBLISH is acknowledged with PUBACK. A + * blocking PUBACK write that sends zero bytes did not acknowledge it, so it + * must be reported as a network error instead of success. */ +TEST(sn_publish_auto_reply_zero_write_returns_network_error) +{ + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + g_client.msg_cb = sn_count_incoming_publish; + g_zero_puback_publish_calls = 0; + g_mock.write_zero_count = 1; + g_mock.write_zero_type = SN_MSG_TYPE_PUBACK; + mock_net_push(&g_mock, SN_PUBLISH_QOS1_FRAME, + (int)sizeof(SN_PUBLISH_QOS1_FRAME)); + + rc = SN_Client_WaitMessage(&g_client, 1000); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(1, g_zero_puback_publish_calls); + ASSERT_EQ(0, g_mock.write_zero_count); /* the PUBACK write was tried */ + ASSERT_EQ(0, g_mock.out_len); +} #endif /* !WOLFMQTT_NONBLOCK */ /* ============================================================================ @@ -3259,12 +3761,19 @@ int main(int argc, char** argv) /* Happy path runs in every SN build (blocking and non-blocking). */ RUN_TEST(sn_connect_lwt_no_continue); RUN_TEST(sn_connect_refused); + RUN_TEST(sn_connect_will_error_allows_retry); +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS + RUN_TEST(sn_connect_will_error_waits_for_connack_reader); +#endif RUN_TEST(sn_will_payload_scrubbed_after_send); + RUN_TEST(sn_willmsg_encode_failure_scrubs_tx_buf); RUN_TEST(sn_willmsgupd_payload_scrubbed_after_send); + RUN_TEST(sn_willtopicupd_null_topic_sends_delete); RUN_TEST(sn_willmsgupd_payload_scrubbed_on_write_error); #ifdef WOLFMQTT_MULTITHREAD RUN_TEST(sn_willmsgupd_scrubbed_on_response_registration_error); RUN_TEST(sn_will_response_reset_clears_return_code); + RUN_TEST(sn_reader_releases_completed_pendresp); #endif RUN_TEST(sn_subscribe_no_continue); RUN_TEST(sn_subscribe_rejected); @@ -3278,9 +3787,17 @@ int main(int argc, char** argv) RUN_TEST(sn_connect_willmsg_zero_write_returns_network_error); RUN_TEST(sn_willmsgupd_zero_write_returns_network_error); RUN_TEST(sn_publish_qos1_zero_write_returns_network_error); + RUN_TEST(sn_publish_auto_reply_zero_write_returns_network_error); #endif RUN_TEST(sn_unsubscribe_no_continue); RUN_TEST(sn_publish_incoming_null_msg_cb_errors_no_ack); + RUN_TEST(sn_wait_message_reads_each_packet_once); +#if WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(sn_qos2_retransmit_not_delivered_twice); + RUN_TEST(sn_qos2_full_table_refuses_new_id); + RUN_TEST(sn_qos2_table_cleared_by_clean_session_connect); + RUN_TEST(sn_qos2_table_cleared_by_client_id_change); +#endif RUN_TEST(sn_ping_no_continue); RUN_TEST(sn_ping_null_no_continue); RUN_TEST(sn_ping_null_consecutive_success); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index b42a2efdc..4c9742519 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -132,6 +132,27 @@ #ifndef BROKER_MAX_WILL_PAYLOAD_LEN #define BROKER_MAX_WILL_PAYLOAD_LEN 256 #endif +/* Static-memory room for a v5 Will's encoded publishable properties. */ +#ifndef BROKER_MAX_WILL_PROPS_LEN + #define BROKER_MAX_WILL_PROPS_LEN 128 +#endif +/* Will Properties rebuilt on the stack when the Will is published, so that + * cannot fail on the shared property pool. In a static build this bounds + * them and a Will with more is refused at CONNECT; the default fits any + * Will within BROKER_MAX_WILL_PROPS_LEN, since every publishable property + * other than User Property appears at most once and a User Property takes at + * least 5 bytes. A dynamic build puts a larger Will on the heap. */ +#ifndef BROKER_WILL_PROPS_STACK_ENTRIES + #ifdef WOLFMQTT_STATIC_MEMORY + #define BROKER_WILL_PROPS_STACK_ENTRIES \ + (5 + BROKER_MAX_WILL_PROPS_LEN / 5) + #else + #define BROKER_WILL_PROPS_STACK_ENTRIES 8 + #endif +#endif +#if BROKER_WILL_PROPS_STACK_ENTRIES < 1 + #error BROKER_WILL_PROPS_STACK_ENTRIES must be at least 1 +#endif #ifndef BROKER_MAX_PENDING_WILLS #define BROKER_MAX_PENDING_WILLS 4 #endif @@ -149,8 +170,8 @@ #endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers - * any reasonable client; a misbehaving client that exceeds this gets a - * malformed-packet rejection. */ + * any reasonable client; a misbehaving client that exceeds this is + * disconnected (v5 reason Quota Exceeded 0x97). */ #ifndef BROKER_MAX_INBOUND_QOS2 #define BROKER_MAX_INBOUND_QOS2 16 #endif @@ -262,8 +283,9 @@ * wrap_mode byte out of the schema-version field. Bumped 2 -> 3 when * the NS_SESSION record gained an orphan_since timestamp. Bumped 3 -> 4 * when NS_OUTQ gained an explicit enqueue sequence, then 4 -> 5 when it - * gained retransmission state. */ - #define WOLFMQTT_BROKER_PERSIST_SCHEMA_VER 5 + * gained retransmission state, then 5 -> 6 when it gained v5 Properties + * and an explicit Message Expiry presence flag. */ + #define WOLFMQTT_BROKER_PERSIST_SCHEMA_VER 6 #endif /* Header wrap_mode byte values (record body framing on disk). */ @@ -357,8 +379,9 @@ typedef struct MqttBrokerNet { /* The persistence layer is intentionally hook-based so the broker can run * on top of POSIX files, embedded flash, an external KV store, or an * in-RAM stub used by tests. Each hook returns 0 on success or a negative - * error code. Shadow-write failures are logged while in-memory state remains - * authoritative. Iterator failures during restore abort MqttBroker_Start. + * error code. Most shadow-write failures are not logged by the broker, so a + * hook should report its own errors; in-memory state remains authoritative. + * Iterator failures during restore abort MqttBroker_Start. * * The broker currently uses only the key/value API. At minimum kv_put, * kv_get, and kv_iter must be installed for sessions, subscriptions, @@ -380,13 +403,23 @@ typedef struct MqttBrokerPersistHooks { * implementations must honor key_len. Keys are derived from * peer-supplied client_ids and topics, which dynamic-memory builds do * not cap. A backend with a maximum key length (the POSIX backend - * accepts up to 256 bytes) must reject a longer key with an error - * rather than truncate it. */ + * accepts up to (NAME_MAX - 8) / 2 bytes, 123 with the usual 255-byte + * NAME_MAX) must reject a longer key with an error rather than + * truncate it. */ int (*kv_put)(void* ctx, byte ns, const byte* key, word16 key_len, const byte* blob, word32 blob_len); + /* On input *inout_len is the capacity of out; on success it is set to + * the record length. Return MQTT_CODE_ERROR_NOT_FOUND for a missing key + * and MQTT_CODE_ERROR_OUT_OF_BUFFER when the record is larger than the + * buffer - never a truncated success, since restore treats an oversized + * META record as a schema mismatch. */ int (*kv_get)(void* ctx, byte ns, const byte* key, word16 key_len, byte* out, word32* inout_len); int (*kv_del)(void* ctx, byte ns, const byte* key, word16 key_len); + /* Calls cb for each record in namespace ns. Every key passed to cb must + * be accepted unchanged by kv_get and kv_del: the broker deletes records + * it finds while iterating and repeats until none match, so a key that + * kv_del cannot remove makes that loop spin. */ int (*kv_iter)(void* ctx, byte ns, MqttBrokerPersist_IterCb cb, void* cb_ctx); @@ -525,18 +558,17 @@ typedef struct BrokerOutPub { * MQTT-4.4.0-1, then clears the flag. */ byte retransmit_dup; /* 0 or 1 */ byte partial_send; /* QoS 0 PUBLISH started on the old socket */ + byte has_expiry; /* v5 Message Expiry Interval present */ WOLFMQTT_BROKER_TIME_T enq_time; word64 enqueue_seq; /* persistent FIFO order within this session */ - word32 expiry_sec; /* v5 Message Expiry Interval, 0 = no expiry */ + word32 expiry_sec; /* v5 Message Expiry Interval when has_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. */ + * via BrokerProps_FreeClone - never via MqttProps_Free(). */ MqttProp* props; #endif struct BrokerOutPub* next; @@ -646,7 +678,7 @@ typedef struct BrokerClient { * not yet PUBREL'd. A duplicate PUBLISH carrying one of these IDs is * acked again (PUBREC) but NOT re-fanned-out to subscribers. The * BROKER_MAX_INBOUND_QOS2 cap is enforced in both memory modes; a - * client that exceeds it is disconnected with malformed-packet error. + * client that exceeds it is disconnected with Quota Exceeded (0x97). * Compiled out for capped-QoS builds (WOLFMQTT_MAX_QOS < 2). */ #if WOLFMQTT_MAX_QOS >= 2 #ifdef WOLFMQTT_STATIC_MEMORY @@ -685,10 +717,10 @@ typedef struct BrokerClient { int out_q_pending_len; /* Encoded length of a direct broker response still being written. */ int direct_pending_len; - /* v5 Receive Maximum advertised by this client in CONNECT, or 65535 - * (per MQTT v5 sec 3.1.2.11.3) when the client did not include the - * property. For v3.1.1 clients this is left at 65535 - the cap - * comes from BROKER_MAX_INFLIGHT_PER_SUB alone. */ + /* v5 Receive Maximum advertised by this client in CONNECT, or 0 when + * the client did not include the property or is a v3.1.1 client. 0 + * means no per-client cap (the MQTT v5 section 3.1.2.11.3 default of + * 65535), so the cap comes from BROKER_MAX_INFLIGHT_PER_SUB alone. */ word16 client_receive_max; #endif /* !WOLFMQTT_STATIC_MEMORY */ word32 session_expiry_sec; @@ -710,6 +742,19 @@ typedef struct BrokerClient { * direct_pending_len. Once that write finishes or fails, the client is * closed instead of reading another packet. */ unsigned int close_after_direct : 1; + /* The connection is being taken over and closes once its own Will has + * been published, so nothing new may start writing to it. */ + unsigned int takeover_closing : 1; +#endif +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_V5) + /* Encoded Will Properties carried onto the Will PUBLISH, i.e. all but + * the Will Delay Interval (MQTT 5.0 section 3.1.3.2). */ +#ifdef WOLFMQTT_STATIC_MEMORY + byte will_props[BROKER_MAX_WILL_PROPS_LEN]; +#else + byte* will_props; +#endif + word16 will_props_len; #endif } BrokerClient; @@ -772,6 +817,15 @@ typedef struct BrokerPendingWill { MqttQoS qos; byte retain; WOLFMQTT_BROKER_TIME_T publish_time; /* absolute time to publish */ +#ifdef WOLFMQTT_V5 + /* Encoded Will Properties, as in BrokerClient.will_props. */ +#ifdef WOLFMQTT_STATIC_MEMORY + byte props[BROKER_MAX_WILL_PROPS_LEN]; +#else + byte* props; +#endif + word16 props_len; +#endif } BrokerPendingWill; #endif /* WOLFMQTT_BROKER_WILL */ @@ -864,10 +918,11 @@ typedef struct MqttBroker { #ifndef WOLFMQTT_STATIC_MEMORY /* Linked list of disconnected persistent sessions. Each entry holds * its own outbound queue + identity so messages published while the - * owning client is offline are retained until reconnect (or - * BROKER_MAX_PERSIST_SESSIONS forces drop-oldest eviction). Subs - * pointing at orphaned sessions keep sub->client=NULL; fan-out - * branches on that to look up the orphan by client_id. */ + * owning client is offline are retained until reconnect. CONNECT refuses + * a new persistent Session once BROKER_MAX_PERSIST_SESSIONS are held, so + * existing ones are not evicted to make room. Subs pointing at orphaned + * sessions keep sub->client=NULL; fan-out branches on that to look up + * the orphan by client_id. */ BrokerOrphanSession* orphan_sessions; int orphan_session_count; #endif @@ -884,13 +939,20 @@ typedef struct MqttBroker { * failure is reported once rather than on every step. */ int accept_log_err; int accept_log_err_tls; +#ifdef WOLFMQTT_V5 + /* Holds one MqttProps_Init reference from MqttBroker_Init to + * MqttBroker_Free, so restored v5 queue properties can be decoded when + * no client is connected. */ + byte props_ref; +#endif } MqttBroker; /* -------------------------------------------------------------------------- */ /* Public API */ /* -------------------------------------------------------------------------- */ -/* Initialize the broker context with network callbacks */ +/* Initialize the broker context with network callbacks. Every successful + * call must be paired with MqttBroker_Free, which releases what it holds. */ WOLFMQTT_API int MqttBroker_Init(MqttBroker* broker, MqttBrokerNet* net); /* Run the broker main loop (blocking) */ @@ -994,8 +1056,9 @@ WOLFMQTT_LOCAL int BrokerPersist_DelOutPub(MqttBroker* broker, WOLFMQTT_LOCAL int BrokerPersist_DelOutQueue(MqttBroker* broker, const char* client_id); /* Startup-time restore on a freshly initialized, empty broker. Rebuilds the - * in-memory tables before the first MqttBroker_Start accepts clients. Wipes - * and re-stamps META when the persisted schema version does not match. */ + * in-memory tables before the first MqttBroker_Start accepts clients. When the + * persisted schema version does not match it re-stamps META, first deleting + * every record in dynamic memory builds (static builds skip the wipe). */ WOLFMQTT_LOCAL int BrokerPersist_Restore(MqttBroker* broker); /* Discard state loaded by a failed startup restore. Defined in * mqtt_broker.c because it uses the broker's shared teardown helpers. */ @@ -1013,6 +1076,18 @@ WOLFMQTT_LOCAL void BrokerStaticOrphan_DropFull(MqttBroker* broker, * and restore-time expiry sweep so the two paths can't drift. */ WOLFMQTT_LOCAL void BrokerOrphan_DropFull(MqttBroker* broker, BrokerOrphanSession* o); +#ifdef WOLFMQTT_V5 +/* v5 Properties of a queued PUBLISH, for the persistence layer. */ +WOLFMQTT_LOCAL int BrokerOutPub_EncodeProps(const BrokerOutPub* e, byte* buf); +WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, + word32 len); +#endif +#endif + +#ifndef WOLFMQTT_BROKER_NO_LOG +/* Escape control bytes in a peer-controlled string before it is logged. + * Defined in mqtt_broker.c; shared with the persistence layer. */ +WOLFMQTT_LOCAL const char* BrokerLog_Sanitize(const char* src); #endif /* CLI wrapper interface */ diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index f4b04a67f..c6be1d77c 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -197,7 +197,13 @@ enum MqttClientFlags { * section 3.2.2.3.2). Managed by the library - set or cleared by * MqttClient_Connect and updated from the CONNACK. Applications must * not set or clear it through MqttClient_Flags. */ - MQTT_CLIENT_FLAG_PERSISTENT_SESSION = 0x01 << 10 + MQTT_CLIENT_FLAG_PERSISTENT_SESSION = 0x01 << 10, + /* A v5 CONNECT carrying an Authentication Method has been written and no + * CONNACK has arrived yet. [MQTT-3.1.2-30] allows only AUTH or DISCONNECT + * in that window, so the other send APIs refuse. Managed by the library - + * set by MqttClient_Connect, cleared when the CONNACK is processed and by + * MqttSocket_Connect and MqttSocket_Disconnect. */ + MQTT_CLIENT_FLAG_AUTH_PENDING = 0x01 << 11 }; /*! \brief Sets flags in the MqttClient structure. To be used from the application before calling MqttClient_NetConnect. @@ -419,7 +425,7 @@ typedef struct _MqttClient { #endif MqttPkRead packet; /* publish packet state - protected by read lock */ - MqttPublishResp packetAck; /* publish ACK - protected by write lock */ + MqttPublishResp packetAck; /* publish ACK - protected by lockClient */ MqttSk read; /* read socket state - protected by read lock */ MqttSk write; /* write socket state - protected by write lock */ @@ -461,7 +467,9 @@ typedef struct _MqttClient { byte init_flags; /* Internal resource-initialization flags. */ #ifdef WOLFMQTT_THREAD_ID_T byte write_owner_valid; /* lockSend owner is known. */ + byte read_owner_valid; /* lockRecv owner is known. */ WOLFMQTT_THREAD_ID_T write_owner; + WOLFMQTT_THREAD_ID_T read_owner; #endif wm_Sem lockSend; wm_Sem lockRecv; @@ -516,8 +524,8 @@ typedef struct _MqttClient { /* Outbound Packet Identifiers written on the current Network Connection * and not yet released by their acknowledgement. Cleared when a connection - * starts or ends, since the client keeps no outbound session state across - * one. */ + * starts or ends; messages kept in replay[] have their identifiers + * reserved again when a resumed Session re-sends them. */ MqttSendId send_inflight[MQTT_MAX_SEND_INFLIGHT]; #ifndef WOLFMQTT_NO_SESSION_REPLAY @@ -537,6 +545,13 @@ typedef struct _MqttClient { word16 auth_method_len; byte auth_method[MQTT_AUTH_METHOD_MAX]; #endif +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_SEM_SIGNAL) + /* Posted when a reader stops decoding into a pending response, so a + * thread unlinking that entry need not wait on lockRecv, which a + * dedicated reader retakes for its next blocking read. */ + wm_Sem decodeDone; + word16 decodeWaiters; /* threads waiting on decodeDone, under lockClient */ +#endif } MqttClient; #ifdef WOLFMQTT_SN @@ -570,6 +585,10 @@ WOLFMQTT_API int MqttClient_Init( int cmd_timeout_ms); /*! \brief Cleans up resources allocated to the MqttClient structure + * \note Does not access the tx_buf and rx_buf passed to + MqttClient_Init, so the caller may free them before or after + this call. MqttClient_NetDisconnect scrubs both buffers when + the transport is closed. * \param client Pointer to MqttClient structure */ WOLFMQTT_API void MqttClient_DeInit(MqttClient *client); @@ -649,7 +668,10 @@ WOLFMQTT_API int MqttClient_Connect( Maximum QoS, Retain unavailable, Topic Alias above the server maximum, or Receive Maximum quota exhausted), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -717,7 +739,10 @@ WOLFMQTT_API int MqttClient_Publish_ex( broker-rejection detection for QoS>0 is required. * \return MQTT_CODE_SUCCESS, MQTT_CODE_CONTINUE (for non-blocking), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -744,7 +769,10 @@ WOLFMQTT_API int MqttClient_Publish_WriteOnly( v3.1.1 rejection is 0x80, v5 rejection is any reason_code >= 0x80), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or another MQTT_CODE_ERROR_* for transport/protocol @@ -762,7 +790,10 @@ WOLFMQTT_API int MqttClient_Subscribe( with topic list. * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -777,7 +808,10 @@ WOLFMQTT_API int MqttClient_Unsubscribe( * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_Ping( @@ -791,7 +825,10 @@ WOLFMQTT_API int MqttClient_Ping( * \param ping Pointer to MqttPing structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping); @@ -811,10 +848,12 @@ WOLFMQTT_API int MqttClient_Auth( /*! \brief Add a new property. - * Allocate a property structure and add it to the head of the list - pointed to by head. To be used prior to calling packet command. + * Allocate a property structure and append it to the tail of the list + pointed to by head; *head is set only when the list is empty. To be + used prior to calling packet command. * \param head Pointer-pointer to a property structure - * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_BAD_ARG + * \return Pointer to the new property, or NULL if head is NULL or no + property could be allocated */ WOLFMQTT_API MqttProp* MqttClient_PropsAdd( MqttProp **head); @@ -831,8 +870,9 @@ WOLFMQTT_API int MqttClient_PropsFree( /*! \brief Encodes and sends the MQTT Disconnect packet (no response) - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this @@ -844,8 +884,9 @@ WOLFMQTT_API int MqttClient_Disconnect( /*! \brief Encodes and sends the MQTT Disconnect packet (no response) - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \param disconnect Pointer to MqttDisconnect structure. NULL is valid. * \return MQTT_CODE_SUCCESS, @@ -866,6 +907,13 @@ WOLFMQTT_API int MqttClient_Disconnect_ex( connection disconnected (clears the connected flag); the application should call MqttClient_NetDisconnect to tear down the transport. + * \note In builds without WOLFMQTT_NONBLOCK a partly received packet + cannot be resumed, so a read that times out after part of a + packet arrived returns MQTT_CODE_ERROR_NETWORK and marks the + connection disconnected, rather than MQTT_CODE_ERROR_TIMEOUT. + Use a timeout_ms long enough for one packet to arrive on the + link. A timeout before any byte of a packet still returns + MQTT_CODE_ERROR_TIMEOUT. * \param client Pointer to MqttClient structure * \param timeout_ms Milliseconds until read timeout * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -897,7 +945,16 @@ WOLFMQTT_API int MqttClient_WaitMessage_ex( #if defined(WOLFMQTT_MULTITHREAD) || defined(WOLFMQTT_NONBLOCK) /*! \brief In a multi-threaded and non-blocking mode this allows you to cancel an MQTT object that was previously submitted. - * \note This is a blocking function that will wait for MqttNet.read + * \note This function performs no network I/O. In multi-threaded + builds it may wait to take the client locks, and waits for + another thread that is decoding a response into msg to + finish, so msg may be freed on return. Do not call it from + a callback (message, property or publish payload) while a + response is being decoded into msg, such as a property + callback for msg's own ack. Where the platform provides + thread identities (WOLFMQTT_THREAD_ID_T) it detects this and + returns MQTT_CODE_ERROR_STAT without cancelling; otherwise it + deadlocks. * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -914,8 +971,8 @@ WOLFMQTT_API int MqttClient_CancelMessage( * \note This function assumes caller owns the object * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure - * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* - (see enum MqttPacketResponseCodes) + * \return 1 if a read or write is pending, 0 if not, or a negative + MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_IsMessageActive( MqttClient *client, @@ -955,6 +1012,13 @@ WOLFMQTT_API int MqttClient_NetConnect( WOLFMQTT_MULTITHREAD builds a buffer whose read or write is still active is left to its owner; release an abandoned operation with MqttClient_CancelMessage, which scrubs it. + * \note In WOLFMQTT_MULTITHREAD builds this does not wait for other + threads: no other thread may be inside a client call that + reads or writes the network (for example blocked in + MqttClient_WaitMessage), or it could use transport or TLS + state while it is freed. Stop those threads first, or use + MqttClient_NetDisconnect_Wait. This function may be called + from a message callback. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -962,6 +1026,26 @@ WOLFMQTT_API int MqttClient_NetConnect( WOLFMQTT_API int MqttClient_NetDisconnect( MqttClient *client); +/*! \brief Performs a network disconnect after other threads' network I/O + on this client has finished + * \note In WOLFMQTT_MULTITHREAD builds this takes the receive lock and + then the send lock before releasing the transport, so a read + or write in progress on another thread completes (or times + out) first and nothing can use transport or TLS state while it + is freed. It must not be called from a callback (message, + property or publish payload) or other code already holding + those locks; use MqttClient_NetDisconnect there. Where the + platform provides thread identities (WOLFMQTT_THREAD_ID_T) it + detects this and returns MQTT_CODE_ERROR_STAT instead of + deadlocking. In other builds it is the same as + MqttClient_NetDisconnect. + * \param client Pointer to MqttClient structure + * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* + (see enum MqttPacketResponseCodes) + */ +WOLFMQTT_API int MqttClient_NetDisconnect_Wait( + MqttClient *client); + /*! \brief Gets number version of connected protocol version * \param client Pointer to MqttClient structure * \return 4 (v3.1.1) or 5 (v5) @@ -1001,8 +1085,31 @@ WOLFMQTT_LOCAL int MqttClient_RespList_Add(MqttClient *client, void *packet_obj); WOLFMQTT_LOCAL int MqttClient_CheckPendResp(MqttClient *client, byte wait_type, word16 wait_packet_id); +WOLFMQTT_LOCAL void MqttClient_RespList_EndProcessing(MqttClient *client, + MqttPendResp *resp); +WOLFMQTT_LOCAL int MqttClient_RespList_RemoveIdle(MqttClient *client, + MqttPendResp *rmResp); #endif WOLFMQTT_LOCAL int MqttPacket_HandleNetError(MqttClient *client, int rc); +#if WOLFMQTT_MAX_QOS >= 2 +/* Inbound QoS 2 de-duplication table (recv_qos2_pending), shared by the MQTT + * and MQTT-SN clients. */ +WOLFMQTT_LOCAL int MqttClient_RecvQos2_Contains(const MqttClient* client, + word16 packet_id); +WOLFMQTT_LOCAL int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client); +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Add(MqttClient* client, + word16 packet_id); +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Remove(MqttClient* client, + word16 packet_id); +#endif +#ifdef WOLFMQTT_SESSION_ID_TRACK +/* ClientId of the Session whose state the client holds, shared by the MQTT + * and MQTT-SN clients. */ +WOLFMQTT_LOCAL int MqttClient_SessionIdMatches(const MqttClient* client, + const char* client_id); +WOLFMQTT_LOCAL void MqttClient_SessionIdRecord(MqttClient* client, + const char* client_id); +#endif /* Securely zero `len` bytes of `mem` (e.g. client->tx_buf) to scrub plaintext * such as credentials or will payloads before releasing the send lock. Uses a diff --git a/wolfmqtt/mqtt_packet.h b/wolfmqtt/mqtt_packet.h index 25efb2b0f..c1899cbec 100644 --- a/wolfmqtt/mqtt_packet.h +++ b/wolfmqtt/mqtt_packet.h @@ -204,9 +204,10 @@ typedef enum _MqttQoS { } MqttQoS; /* Maximum QoS supported by this build. Legal values: 0, 1, 2. Default 2. - * Define in user_settings.h to compile a QoS-capped client/broker. Wired - * into broker CONNACK Maximum QoS property emission today; broader code - * gating lives on the mqtt_qos_max branch. */ + * Define in user_settings.h to compile a QoS-capped client/broker. Sets the + * client's max_qos and the broker's CONNACK Maximum QoS, clamps granted + * subscription and Will QoS, rejects inbound PUBLISH above the cap, and + * compiles out the QoS 2 state when below 2. */ #ifndef WOLFMQTT_MAX_QOS #define WOLFMQTT_MAX_QOS 2 #endif @@ -844,6 +845,11 @@ WOLFMQTT_API int MqttEncode_Vbi(byte *buf, word32 x); /* Packet Encoders/Decoders */ WOLFMQTT_API int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *connect); +/* Decode a CONNACK. In v5 builds set connect_ack->protocol_level first: it + * selects the v5 form (Properties after the return code) and which return + * codes are valid. A code outside the MQTT 3.1.1 section 3.2.2.3 table, or + * the MQTT 5.0 section 3.2.2.2 table at level 5, is rejected with + * MQTT_CODE_ERROR_MALFORMED_DATA. */ WOLFMQTT_API int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, MqttConnectAck *connect_ack); WOLFMQTT_API int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, @@ -899,19 +905,32 @@ WOLFMQTT_API int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth); WOLFMQTT_API int MqttEncode_Auth(byte *tx_buf, int tx_buf_len, MqttAuth *auth); +/* Encode or decode a v5 property list. packet selects the allow-list the + * properties are checked against (MQTT 5.0 section 2.2.2.2). Will Properties + * have their own list (section 3.1.3.2), selected with + * MQTT_PACKET_TYPE_RESERVED; MQTT_PACKET_TYPE_CONNECT covers only the CONNECT + * Properties, so Will Delay Interval and Message Expiry Interval are rejected + * there. */ WOLFMQTT_API int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf); WOLFMQTT_API int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* buf, word32 buf_len, word32 prop_len); -/* Must be called once from a single thread before any concurrent access - * to MQTTv5 property functions. Not thread-safe if called concurrently. */ +/* Reference counted: each call must be paired with one MqttProps_ShutDown + * call. MqttClient_Init and MqttClient_DeInit already make one pair per v5 + * client. Calls must not run concurrently with each other. */ WOLFMQTT_API int MqttProps_Init(void); -/* Must be called once from a single thread after all concurrent access - * to MQTTv5 property functions has ceased. Not thread-safe if called - * concurrently. */ +/* Releases one MqttProps_Init reference. The shared property lock is freed + * when the last reference is released, so call it only after property + * functions are no longer in use through that reference. Calls must not run + * concurrently with each other. */ WOLFMQTT_API int MqttProps_ShutDown(void); +/* Append a cleared property to the end of the list at *head and return it, + * or NULL if no property can be allocated. */ WOLFMQTT_API MqttProp* MqttProps_Add(MqttProp **head); +/* Release every property in the list starting at head. */ WOLFMQTT_API int MqttProps_Free(MqttProp *head); +/* Return the first property in the list starting at head whose type is + * type, or NULL if the list is empty or holds none. */ WOLFMQTT_API MqttProp* MqttProps_FindType(MqttProp *head, MqttPropertyType type); #endif diff --git a/wolfmqtt/mqtt_sn_client.h b/wolfmqtt/mqtt_sn_client.h index 985d0889b..99a19ba74 100644 --- a/wolfmqtt/mqtt_sn_client.h +++ b/wolfmqtt/mqtt_sn_client.h @@ -59,9 +59,9 @@ WOLFMQTT_API int SN_Client_SearchGW( /*! \brief Encodes and sends the Connect packet and waits for the Connect Acknowledgment packet. If Will is enabled, the gateway - prompts for LWT Topic and Message. Sending an empty will topic - indicates that the client wishes to delete the Will topic and - the Will message stored in the server. + prompts for LWT Topic and Message. The Will is always sent + with its flags, so an empty Will topic here does not use the + delete form (MQTT-SN 1.2 section 5.4.7). * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param connect Pointer to SN_Connect structure initialized @@ -80,13 +80,15 @@ WOLFMQTT_API int SN_Client_Connect( MqttClient *client, SN_Connect *connect); -/*! \brief Encodes and sends the MQTT-SN Will Topic Update packet. Sending - a NULL 'will' indicates that the client wishes to delete the - Will topic and the Will message stored in the server. +/*! \brief Encodes and sends the MQTT-SN Will Topic Update packet. Setting + will->willTopic to NULL sends an empty Will Topic Update, + which asks the server to delete the stored Will topic and + Will message. * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param will Pointer to SN_Will structure initialized - with topic and message parameters. NULL is valid. + with topic and message parameters. Must not be + NULL; it also holds the request state. * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ @@ -96,7 +98,8 @@ WOLFMQTT_API int SN_Client_WillTopicUpdate(MqttClient *client, SN_Will *will); * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param will Pointer to SN_Will structure initialized - with topic and message parameters. NULL is valid. + with topic and message parameters. Must not be + NULL. * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ @@ -185,8 +188,9 @@ WOLFMQTT_API int SN_Client_Unsubscribe( /*! \brief Encodes and sends the MQTT-SN Disconnect packet. Client may send the disconnect with a duration to indicate the client is entering the "asleep" state. - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) diff --git a/wolfmqtt/mqtt_sn_packet.h b/wolfmqtt/mqtt_sn_packet.h index b74246525..f248892ea 100644 --- a/wolfmqtt/mqtt_sn_packet.h +++ b/wolfmqtt/mqtt_sn_packet.h @@ -44,10 +44,10 @@ #define WOLFMQTT_SN_MAXPACKET_SIZE 1024 #endif -/* The SN_GwAddr field has a variable length and contains the address of a GW. - Its depends on the network over which MQTT-SN operates and is indicated in - the first octet of this field. For example, in a ZigBee network the network - address is 2-octet long. */ +/* Gateway address. MQTT-SN 1.2 section 5.4.3 defines GwAdd as variable + length and network dependent, but this type holds only a fixed 2-octet + address (for example a ZigBee network address). SN_Decode_GWInfo copies at + most sizeof(SN_GwAddr) bytes. */ typedef word16 SN_GwAddr ; /* RETURN CODE values */ diff --git a/wolfmqtt/mqtt_types.h b/wolfmqtt/mqtt_types.h index f46eb78b5..862155e40 100644 --- a/wolfmqtt/mqtt_types.h +++ b/wolfmqtt/mqtt_types.h @@ -92,7 +92,9 @@ /* Multi-threading uses binary semaphores */ #if defined(WOLFMQTT_USER_THREADING) /* User provides API's and wm_Sem type. - * Add your wc_Sem into user_settings.h */ + * Add your wc_Sem into user_settings.h. Define WOLFMQTT_SEM_SIGNAL + * if wm_SemUnlock may be called by a thread other than the one that + * locked the semaphore. */ #elif defined(__MACH__) /* Apple Style Dispatch Semaphore */ @@ -101,6 +103,7 @@ typedef struct { dispatch_semaphore_t sem; } wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T pthread_t #define WOLFMQTT_THREAD_SELF() pthread_self() #define WOLFMQTT_THREAD_EQUAL(a, b) pthread_equal((a), (b)) @@ -116,6 +119,9 @@ #endif pthread_mutex_t mutex; } wm_Sem; + #ifndef WOLFMQTT_NO_COND_SIGNAL + #define WOLFMQTT_SEM_SIGNAL + #endif #define WOLFMQTT_THREAD_ID_T pthread_t #define WOLFMQTT_THREAD_SELF() pthread_self() #define WOLFMQTT_THREAD_EQUAL(a, b) pthread_equal((a), (b)) @@ -125,6 +131,7 @@ #include #include typedef SemaphoreHandle_t wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #if defined(INCLUDE_xTaskGetCurrentTaskHandle) && \ (INCLUDE_xTaskGetCurrentTaskHandle == 1) #define WOLFMQTT_THREAD_ID_T TaskHandle_t @@ -138,6 +145,7 @@ #include #include typedef HANDLE wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T DWORD #define WOLFMQTT_THREAD_SELF() GetCurrentThreadId() #define WOLFMQTT_THREAD_EQUAL(a, b) ((a) == (b)) @@ -145,6 +153,7 @@ #elif defined(THREADX) #include typedef TX_SEMAPHORE wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T TX_THREAD* #define WOLFMQTT_THREAD_SELF() tx_thread_identify() #define WOLFMQTT_THREAD_EQUAL(a, b) ((a) == (b))