From 6502d6e5f455fa865dd61440c054c262382fe984 Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Thu, 10 Jun 2021 15:33:01 +0100 Subject: [PATCH] Add mosquitto_topic_matches_sub_with_pattern() And use it in the default security checks. --- ChangeLog.txt | 3 + include/mosquitto.h | 38 ++- lib/linker.version | 1 + lib/util_topic.c | 66 +++++- src/security_default.c | 39 +--- test/broker/09-acl-access-variants.py | 106 ++++----- test/unit/util_topic_test.c | 319 ++++++++++++++++++++++++++ 7 files changed, 459 insertions(+), 113 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index c3f49f9c..35ce3dd5 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -35,6 +35,9 @@ Client library: - Callbacks no longer block other callbacks, and can be set from within a callback. Closes #2127. - Add support for MQTT v5 broker to client topic aliases. +- Add `mosquitto_topic_matches_sub_with_pattern()`, which can match against + subscriptions with `%c` and `%u` patterns for client id / username + substitution. Clients: - Add `-o` option for all clients loading options from a specific file. diff --git a/include/mosquitto.h b/include/mosquitto.h index 64b681d9..19339e0d 100644 --- a/include/mosquitto.h +++ b/include/mosquitto.h @@ -2445,35 +2445,55 @@ libmosq_EXPORT int mosquitto_sub_topic_tokens_free(char ***topics, int count); * Returns: * MOSQ_ERR_SUCCESS - on success * MOSQ_ERR_INVAL - if the input parameters were invalid. - * MOSQ_ERR_NOMEM - if an out of memory condition occurred. */ libmosq_EXPORT int mosquitto_topic_matches_sub(const char *sub, const char *topic, bool *result); - /* * Function: mosquitto_topic_matches_sub2 * - * Check whether a topic matches a subscription. + * Identical to . The sublen and topiclen + * parameters are *IGNORED*. + */ +libmosq_EXPORT int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *topic, size_t topiclen, bool *result); + + +/* + * Function: mosquitto_topic_matches_sub_with_pattern + * + * Check whether a topic matches a subscription, with client id/username + * pattern substitution. + * + * Any instances of a topic hierarchy that are exactly %c or %u will be + * replaced with the client id or username respectively. * * For example: * - * foo/bar would match the subscription foo/# or +/bar - * non/matching would not match the subscription non/+/+ + * mosquitto_topic_matches_sub_with_pattern("sensors/%c/temperature", "sensors/kitchen/temperature", "kitchen", NULL, &result) + * -> this will match + * + * mosquitto_topic_matches_sub_with_pattern("sensors/%c/temperature", "sensors/bathroom/temperature", "kitchen", NULL, &result) + * -> this will not match + * + * mosquitto_topic_matches_sub_with_pattern("sensors/%count/temperature", "sensors/kitchen/temperature", "kitchen", NULL, &result) + * -> this will not match - the `%count` is not treated as a pattern + * + * mosquitto_topic_matches_sub_with_pattern("%c/%c/%u/%u", "kitchen/kitchen/bathroom/bathroom", "kitchen", "bathroom", &result) + * -> this will match * * Parameters: * sub - subscription string to check topic against. - * sublen - length in bytes of sub string * topic - topic to check. - * topiclen - length in bytes of topic string + * clientid - client id to substitute in patterns. If NULL, then any %c patterns will not match. + * username - username to substitute in patterns. If NULL, then any %u patterns will not match. * result - bool pointer to hold result. Will be set to true if the topic * matches the subscription. * * Returns: * MOSQ_ERR_SUCCESS - on success * MOSQ_ERR_INVAL - if the input parameters were invalid. - * MOSQ_ERR_NOMEM - if an out of memory condition occurred. */ -libmosq_EXPORT int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *topic, size_t topiclen, bool *result); +libmosq_EXPORT int mosquitto_topic_matches_sub_with_pattern(const char *sub, const char *topic, const char *clientid, const char *username, bool *result); + /* * Function: mosquitto_pub_topic_check diff --git a/lib/linker.version b/lib/linker.version index 6fe20feb..fb5e27e0 100644 --- a/lib/linker.version +++ b/lib/linker.version @@ -145,4 +145,5 @@ MOSQ_1.7 { MOSQ_2.1 { global: mosquitto_pre_connect_callback_set; + mosquitto_topic_matches_sub_with_pattern; } MOSQ_1.7; diff --git a/lib/util_topic.c b/lib/util_topic.c index 62b53112..2454f0cb 100644 --- a/lib/util_topic.c +++ b/lib/util_topic.c @@ -189,18 +189,10 @@ int mosquitto_sub_topic_check2(const char *str, size_t len) return MOSQ_ERR_SUCCESS; } -int mosquitto_topic_matches_sub(const char *sub, const char *topic, bool *result) -{ - return mosquitto_topic_matches_sub2(sub, 0, topic, 0, result); -} - -/* Does a topic match a subscription? */ -int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *topic, size_t topiclen, bool *result) +static int topic_matches_sub(const char *sub, const char *topic, const char *clientid, const char *username, bool match_patterns, bool *result) { size_t spos; - - UNUSED(sublen); - UNUSED(topiclen); + const char *pattern_check; if(!result) return MOSQ_ERR_INVAL; *result = false; @@ -221,6 +213,39 @@ int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *top if(topic[0] == '+' || topic[0] == '#'){ return MOSQ_ERR_INVAL; } + if(match_patterns && + sub[0] == '%' && + (sub[1] == 'c' || sub[1] == 'u') && + (sub[2] == '/' || sub[2] == '\0') + ){ + + if(sub[1] == 'c'){ + pattern_check = clientid; + }else{ + pattern_check = username; + } + if(pattern_check == NULL || pattern_check[0] == '\0'){ + return MOSQ_ERR_SUCCESS; + } + spos += 2; + sub += 2; + + while(pattern_check[0] != 0 && topic[0] != 0 && topic[0] != '/'){ + if(pattern_check[0] != topic[0]){ + /* Valid input, but no match */ + return MOSQ_ERR_SUCCESS; + } + pattern_check++; + topic++; + } + if((sub[0] == '\0' && topic[0] == '\0') || + (sub[0] == '/' && sub[1] == '#' && sub[2] == '\0' && topic[0] == '\0') + ){ + + *result = true; + return MOSQ_ERR_SUCCESS; + } + } if(sub[0] != topic[0] || topic[0] == 0){ /* Check for wildcard matches */ if(sub[0] == '+'){ /* Check for bad "+foo" or "a/+foo" subscription */ @@ -325,3 +350,24 @@ int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *top return MOSQ_ERR_SUCCESS; } + + +int mosquitto_topic_matches_sub(const char *sub, const char *topic, bool *result) +{ + return topic_matches_sub(sub, topic, NULL, NULL, false, result); +} + +/* Does a topic match a subscription? */ +int mosquitto_topic_matches_sub2(const char *sub, size_t sublen, const char *topic, size_t topiclen, bool *result) +{ + UNUSED(sublen); + UNUSED(topiclen); + + return topic_matches_sub(sub, topic, NULL, NULL, false, result); +} + + +int mosquitto_topic_matches_sub_with_pattern(const char *sub, const char *topic, const char *clientid, const char *username, bool *result) +{ + return topic_matches_sub(sub, topic, clientid, username, true, result); +} diff --git a/src/security_default.c b/src/security_default.c index e659e7bf..90c4c7c8 100644 --- a/src/security_default.c +++ b/src/security_default.c @@ -368,12 +368,8 @@ static int add__acl_pattern(struct mosquitto__security_options *security_opts, c static int mosquitto_acl_check_default(int event, void *event_data, void *userdata) { struct mosquitto_evt_acl_check *ed = event_data; - char *local_acl; struct mosquitto__acl *acl_root; bool result; - size_t i; - size_t len, tlen, clen, ulen; - char *s; struct mosquitto__security_options *security_opts = NULL; UNUSED(event); @@ -446,47 +442,14 @@ static int mosquitto_acl_check_default(int event, void *event_data, void *userda /* Loop through all pattern ACLs. ACL denial patterns are iterated over first. */ if(!ed->client->id) return MOSQ_ERR_ACL_DENIED; - clen = strlen(ed->client->id); while(acl_root){ - tlen = strlen(acl_root->topic); - if(acl_root->ucount && !ed->client->username){ acl_root = acl_root->next; continue; } - if(ed->client->username){ - ulen = strlen(ed->client->username); - len = tlen + (size_t)acl_root->ccount*(clen-2) + (size_t)acl_root->ucount*(ulen-2); - }else{ - ulen = 0; - len = tlen + (size_t)acl_root->ccount*(clen-2); - } - local_acl = mosquitto__malloc(len+1); - if(!local_acl) return MOSQ_ERR_NOMEM; - s = local_acl; - for(i=0; itopic[i] == '%'){ - if(acl_root->topic[i+1] == 'c'){ - i++; - strncpy(s, ed->client->id, clen); - s+=clen; - continue; - }else if(ed->client->username && acl_root->topic[i+1] == 'u'){ - i++; - strncpy(s, ed->client->username, ulen); - s+=ulen; - continue; - } - } - s[0] = acl_root->topic[i]; - s++; - } - local_acl[len] = '\0'; - - mosquitto_topic_matches_sub(local_acl, ed->topic, &result); - mosquitto__free(local_acl); + mosquitto_topic_matches_sub_with_pattern(acl_root->topic, ed->topic, ed->client->id, ed->client->username, &result); if(result){ if(acl_root->access == MOSQ_ACL_NONE){ /* Access was explicitly denied for this topic pattern. */ diff --git a/test/broker/09-acl-access-variants.py b/test/broker/09-acl-access-variants.py index 30f85150..5cb71e48 100755 --- a/test/broker/09-acl-access-variants.py +++ b/test/broker/09-acl-access-variants.py @@ -27,44 +27,60 @@ def write_acl(filename, global_en, user_en, pattern_en): def single_test(port, per_listener, username, topic, expect_deny): - rc = 1 + keepalive = 60 + connect_packet = mosq_test.gen_connect("acl-check", keepalive=keepalive, username=username) + connack_packet = mosq_test.gen_connack(rc=0) + mid = 1 + subscribe_packet = mosq_test.gen_subscribe(mid=mid, topic=topic, qos=1) + suback_packet = mosq_test.gen_suback(mid=mid, qos=1) + + mid = 2 + publish1s_packet = mosq_test.gen_publish(topic=topic, mid=mid, qos=1, payload="message") + puback1s_packet = mosq_test.gen_puback(mid) + + mid=1 + publish1r_packet = mosq_test.gen_publish(topic=topic, mid=mid, qos=1, payload="message") + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + sock.send(publish1s_packet) + if expect_deny: + mosq_test.expect_packet(sock, "puback", puback1s_packet) + mosq_test.do_ping(sock) + else: + mosq_test.receive_unordered(sock, puback1s_packet, publish1r_packet, "puback / publish1r") + sock.close() + + +def acl_test(port, per_listener, global_en, user_en, pattern_en): + acl_file = os.path.basename(__file__).replace('.py', '.acl') conf_file = os.path.basename(__file__).replace('.py', '.conf') + + write_acl(acl_file, global_en=global_en, user_en=user_en, pattern_en=pattern_en) write_config(conf_file, port, per_listener) broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + rc = 0 try: - keepalive = 60 - connect_packet = mosq_test.gen_connect("acl-check", keepalive=keepalive, username=username) - connack_packet = mosq_test.gen_connack(rc=0) - - mid = 1 - subscribe_packet = mosq_test.gen_subscribe(mid=mid, topic=topic, qos=1) - suback_packet = mosq_test.gen_suback(mid=mid, qos=1) - - mid = 2 - publish1s_packet = mosq_test.gen_publish(topic=topic, mid=mid, qos=1, payload="message") - puback1s_packet = mosq_test.gen_puback(mid) - - mid=1 - publish1r_packet = mosq_test.gen_publish(topic=topic, mid=mid, qos=1, payload="message") - - sock = mosq_test.do_client_connect(connect_packet, connack_packet, port=port) - mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") - sock.send(publish1s_packet) - if expect_deny: - mosq_test.expect_packet(sock, "puback", puback1s_packet) - mosq_test.do_ping(sock) - else: - mosq_test.receive_unordered(sock, puback1s_packet, publish1r_packet, "puback / publish1r") - sock.close() - - rc = 0 + if global_en: + single_test(port, per_listener, username=None, topic="topic/global", expect_deny=False) + single_test(port, per_listener, username="username", topic="topic/global", expect_deny=True) + single_test(port, per_listener, username=None, topic="topic/global/except", expect_deny=True) + if user_en: + single_test(port, per_listener, username=None, topic="topic/username", expect_deny=True) + single_test(port, per_listener, username="username", topic="topic/username", expect_deny=False) + single_test(port, per_listener, username="username", topic="topic/username/except", expect_deny=True) + if pattern_en: + single_test(port, per_listener, username=None, topic="pattern/username", expect_deny=True) + single_test(port, per_listener, username="username", topic="pattern/username", expect_deny=False) + single_test(port, per_listener, username="username", topic="pattern/username/except", expect_deny=True) except mosq_test.TestError: - pass + rc = 1 finally: os.remove(conf_file) + os.remove(acl_file) broker.terminate() broker.wait() (stdo, stde) = broker.communicate() @@ -72,35 +88,13 @@ def single_test(port, per_listener, username, topic, expect_deny): print(stde.decode('utf-8')) exit(rc) -def acl_test(port, per_listener, global_en, user_en, pattern_en): - acl_file = os.path.basename(__file__).replace('.py', '.acl') - - write_acl(acl_file, global_en=global_en, user_en=user_en, pattern_en=pattern_en) - - if global_en: - single_test(port, per_listener, username=None, topic="topic/global", expect_deny=False) - single_test(port, per_listener, username="username", topic="topic/global", expect_deny=True) - single_test(port, per_listener, username=None, topic="topic/global/except", expect_deny=True) - if user_en: - single_test(port, per_listener, username=None, topic="topic/username", expect_deny=True) - single_test(port, per_listener, username="username", topic="topic/username", expect_deny=False) - single_test(port, per_listener, username="username", topic="topic/username/except", expect_deny=True) - if pattern_en: - single_test(port, per_listener, username=None, topic="pattern/username", expect_deny=True) - single_test(port, per_listener, username="username", topic="pattern/username", expect_deny=False) - single_test(port, per_listener, username="username", topic="pattern/username/except", expect_deny=True) - def do_test(port, per_listener): - try: - acl_test(port, per_listener, global_en=False, user_en=False, pattern_en=True) - acl_test(port, per_listener, global_en=False, user_en=True, pattern_en=False) - acl_test(port, per_listener, global_en=True, user_en=False, pattern_en=False) - acl_test(port, per_listener, global_en=False, user_en=True, pattern_en=True) - acl_test(port, per_listener, global_en=True, user_en=False, pattern_en=True) - acl_test(port, per_listener, global_en=True, user_en=True, pattern_en=True) - finally: - acl_file = os.path.basename(__file__).replace('.py', '.acl') - os.remove(acl_file) + acl_test(port, per_listener, global_en=False, user_en=False, pattern_en=True) + acl_test(port, per_listener, global_en=False, user_en=True, pattern_en=False) + acl_test(port, per_listener, global_en=True, user_en=False, pattern_en=False) + acl_test(port, per_listener, global_en=False, user_en=True, pattern_en=True) + acl_test(port, per_listener, global_en=True, user_en=False, pattern_en=True) + acl_test(port, per_listener, global_en=True, user_en=True, pattern_en=True) port = mosq_test.get_port() diff --git a/test/unit/util_topic_test.c b/test/unit/util_topic_test.c index b669fcc9..a104af87 100644 --- a/test/unit/util_topic_test.c +++ b/test/unit/util_topic_test.c @@ -84,6 +84,72 @@ static void TEST_empty_input(void) CU_ASSERT_EQUAL(match, false); } +static void TEST_pattern_empty_input(void) +{ + int rc; + bool match; + + rc = mosquitto_topic_matches_sub_with_pattern(NULL, NULL, NULL, NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("sub", NULL, NULL, NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern(NULL, "topic", NULL, NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern(NULL, NULL, "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern(NULL, NULL, NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("sub", "", "", "", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("", "topic", "", "", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("", "", "clientid", "", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("", "", "", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%c", "topic", NULL, NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%u", "topic", NULL, NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%c", "", "", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%u", "", NULL, "", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_INVAL); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/test", "test//test", "", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/test", "test//test", NULL, "", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); +} + /* ======================================================================== * VALID MATCHING AND NON-MATCHING * ======================================================================== */ @@ -181,6 +247,254 @@ static void TEST_invalid(void) no_match_helper(MOSQ_ERR_INVAL, "/#a", "foo/bar"); } +/* ======================================================================== + * PATTERNS + * ======================================================================== */ + +static void TEST_pattern_clientid(void) +{ + int rc; + bool match; + + /* Sole pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("%c", "clientid", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("%c", "clientid", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern at beginning */ + rc = mosquitto_topic_matches_sub_with_pattern("%c/test", "clientid/test", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("%c/test", "clientid/test", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern at end */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%c", "test/clientid", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c", "test/clientid", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern in middle */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/test", "test/clientid/test", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/test", "test/clientid/test", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Repeated pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%c/test", "test/clientid/clientid/test", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%c/test", "test/clientid/clientid/test", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Not a pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%count", "test/clientid", "clientid", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); +} + +static void TEST_pattern_username(void) +{ + int rc; + bool match; + + /* Sole pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("%u", "username", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("%u", "username", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern at beginning */ + rc = mosquitto_topic_matches_sub_with_pattern("%u/test", "username/test", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("%u/test", "username/test", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern at end */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%u", "test/username", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%u", "test/username", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern in middle */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/test", "test/username/test", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/test", "test/username/test", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Repeated pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/%u/test", "test/username/username/test", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/%u/test", "test/username/username/test", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Not a pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%username", "test/username", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); +} + +static void TEST_pattern_both(void) +{ + int rc; + bool match; + + /* Sole pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("%u/%c", "username/clientid", "clientid", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("%u/%c", "username/clientid", "clientid", "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%u/%c", "username/clientid", "nomatch", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("%u/%c", "username/clientid", "nomatch", "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Pattern in middle */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%u/test", "test/clientid/username/test", "clientid", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%u/test", "test/clientid/username/test", "clientid", "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%u/test", "test/clientid/username/test", "nomatch", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("test/%c/%u/test", "test/clientid/username/test", "nomatch", "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Repeated pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%u/%c/%c/%u/test", "test/username/clientid/clientid/username/test", "clientid", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + /* Not a pattern */ + rc = mosquitto_topic_matches_sub_with_pattern("test/%username/%client", "test/username/clientid", "clientid", "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); +} + +static void TEST_pattern_wildcard(void) +{ + int rc; + bool match; + + /* Malicious */ + /* ========= */ + + /* / in client id */ + rc = mosquitto_topic_matches_sub_with_pattern("%c", "clientid/test", "clientid/test", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* / in username */ + rc = mosquitto_topic_matches_sub_with_pattern("%u", "username/test", NULL, "username/test", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* + in client id */ + rc = mosquitto_topic_matches_sub_with_pattern("%c", "clientid", "+", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* + in username */ + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/+", "username/test/+", NULL, "+", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Valid */ + /* ========= */ + + /* Ends in + */ + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/+", "clientid/test/topic", "test", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/+", "clientid/test/topic", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/+", "username/test/topic", NULL, "test", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/+", "username/test/topic", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + /* Ends in # */ + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/#", "clientid/test/topic", "test", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/#", "clientid/test/topic", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/#", "username/test/topic", NULL, "test", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/#", "username/test/topic", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/#", "clientid/test", "test", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("clientid/%c/#", "clientid/test", "nomatch", NULL, &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); + + rc = mosquitto_topic_matches_sub_with_pattern("pattern/%u/#", "pattern/username", NULL, "username", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, true); + + rc = mosquitto_topic_matches_sub_with_pattern("username/%u/#", "username/test", NULL, "nomatch", &match); + CU_ASSERT_EQUAL(rc, MOSQ_ERR_SUCCESS); + CU_ASSERT_EQUAL(match, false); +} + /* ======================================================================== * PUB TOPIC CHECK * ======================================================================== */ @@ -287,6 +601,11 @@ int init_util_topic_tests(void) || !CU_add_test(test_suite, "Pub topic: Invalid", TEST_pub_topic_invalid) || !CU_add_test(test_suite, "Sub topic: Valid", TEST_sub_topic_valid) || !CU_add_test(test_suite, "Sub topic: Invalid", TEST_sub_topic_invalid) + || !CU_add_test(test_suite, "Pattern topic: Empty input", TEST_pattern_empty_input) + || !CU_add_test(test_suite, "Pattern topic: clientid", TEST_pattern_clientid) + || !CU_add_test(test_suite, "Pattern topic: username", TEST_pattern_username) + || !CU_add_test(test_suite, "Pattern topic: both", TEST_pattern_both) + || !CU_add_test(test_suite, "Pattern topic: wildcard", TEST_pattern_wildcard) ){ printf("Error adding util topic CUnit tests.\n");