mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 10:57:27 +00:00
227 lines
8.3 KiB
C++
227 lines
8.3 KiB
C++
#pragma once
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
// Pure per-broker packet-type allowlist helpers. MeshCore payload types occupy
|
|
// the low four bits of the packet header, so a uint16_t stores the complete
|
|
// 0..15 allowlist without dynamic allocation.
|
|
namespace MQTTPacketFilter {
|
|
|
|
static const uint8_t kMinPacketType = 0;
|
|
static const uint8_t kMaxPacketType = 15;
|
|
static const uint16_t kAllPacketTypes = 0xFFFFu;
|
|
// "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" plus the terminator.
|
|
static const size_t kFilterTextSize = 38;
|
|
|
|
inline bool isAsciiSpace(char c) {
|
|
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
|
}
|
|
|
|
inline bool tokenEquals(const char* begin, size_t len, const char* token) {
|
|
return token != nullptr && strlen(token) == len && memcmp(begin, token, len) == 0;
|
|
}
|
|
|
|
// Payload-type spellings accepted alongside the decimal form, mirroring the
|
|
// PAYLOAD_TYPE_* names in src/Packet.h. Types 12-14 are reserved upstream and
|
|
// have no name, so they stay reachable only by number.
|
|
struct NamedPacketType {
|
|
const char* name;
|
|
uint8_t type;
|
|
};
|
|
|
|
inline const NamedPacketType* namedPacketTypes(size_t* count_out) {
|
|
static const NamedPacketType kNames[] = {
|
|
{"req", 0}, {"response", 1}, {"txt_msg", 2}, {"ack", 3},
|
|
{"advert", 4}, {"grp_txt", 5}, {"grp_data", 6}, {"anon_req", 7},
|
|
{"path", 8}, {"trace", 9}, {"multipart", 10}, {"control", 11},
|
|
{"raw_custom", 15},
|
|
};
|
|
if (count_out != nullptr) *count_out = sizeof(kNames) / sizeof(kNames[0]);
|
|
return kNames;
|
|
}
|
|
|
|
// Resolve one already-trimmed list entry to a payload type. Decimal and named
|
|
// spellings are interchangeable within a list; "all"/"none" are whole-value
|
|
// keywords and deliberately do not resolve here, so "all,2" stays invalid.
|
|
inline bool resolveToken(const char* begin, size_t len, uint8_t* type_out) {
|
|
if (begin == nullptr || type_out == nullptr || len == 0) return false;
|
|
|
|
if (*begin >= '0' && *begin <= '9') {
|
|
unsigned value = 0;
|
|
for (size_t i = 0; i < len; ++i) {
|
|
if (begin[i] < '0' || begin[i] > '9') return false;
|
|
value = value * 10u + static_cast<unsigned>(begin[i] - '0');
|
|
if (value > kMaxPacketType) return false;
|
|
}
|
|
*type_out = static_cast<uint8_t>(value);
|
|
return true;
|
|
}
|
|
|
|
size_t name_count = 0;
|
|
const NamedPacketType* names = namedPacketTypes(&name_count);
|
|
for (size_t i = 0; i < name_count; ++i) {
|
|
if (tokenEquals(begin, len, names[i].name)) {
|
|
*type_out = names[i].type;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Parse an allowlist value. Empty input means "all" so WebConfig can clear a
|
|
// field and retain the same backwards-compatible default as an older
|
|
// /mqtt_prefs file. Keywords and type names are deliberately lowercase; "all"
|
|
// and "none" cannot be mixed into a list. Entries may be decimal (0..15) or
|
|
// named, may carry surrounding ASCII whitespace, and may be repeated.
|
|
inline bool parse(const char* input, uint16_t* mask_out) {
|
|
if (input == nullptr || mask_out == nullptr) return false;
|
|
|
|
const char* begin = input;
|
|
while (*begin && isAsciiSpace(*begin)) begin++;
|
|
const char* end = begin + strlen(begin);
|
|
while (end > begin && isAsciiSpace(end[-1])) end--;
|
|
|
|
const size_t len = static_cast<size_t>(end - begin);
|
|
if (len == 0 || tokenEquals(begin, len, "all")) {
|
|
*mask_out = kAllPacketTypes;
|
|
return true;
|
|
}
|
|
if (tokenEquals(begin, len, "none")) {
|
|
*mask_out = 0;
|
|
return true;
|
|
}
|
|
|
|
uint16_t parsed = 0;
|
|
const char* cursor = begin;
|
|
for (;;) {
|
|
const char* comma = cursor;
|
|
while (comma < end && *comma != ',') comma++;
|
|
|
|
const char* token_begin = cursor;
|
|
const char* token_end = comma;
|
|
while (token_begin < token_end && isAsciiSpace(*token_begin)) token_begin++;
|
|
while (token_end > token_begin && isAsciiSpace(token_end[-1])) token_end--;
|
|
|
|
uint8_t type = 0;
|
|
if (!resolveToken(token_begin, static_cast<size_t>(token_end - token_begin), &type)) {
|
|
return false;
|
|
}
|
|
parsed |= static_cast<uint16_t>(1u << type);
|
|
|
|
if (comma >= end) break;
|
|
cursor = comma + 1;
|
|
}
|
|
|
|
*mask_out = parsed;
|
|
return true;
|
|
}
|
|
|
|
// Format masks deterministically for CLI/API output. Subsets are emitted in
|
|
// ascending order; the two useful extremes use concise keywords.
|
|
inline bool format(uint16_t mask, char* output, size_t output_size) {
|
|
if (output == nullptr || output_size == 0) return false;
|
|
output[0] = '\0';
|
|
|
|
const char* keyword = nullptr;
|
|
if (mask == kAllPacketTypes) keyword = "all";
|
|
else if (mask == 0) keyword = "none";
|
|
if (keyword != nullptr) {
|
|
const size_t len = strlen(keyword);
|
|
if (output_size <= len) return false;
|
|
memcpy(output, keyword, len + 1);
|
|
return true;
|
|
}
|
|
|
|
char formatted[kFilterTextSize];
|
|
size_t pos = 0;
|
|
bool first = true;
|
|
for (uint8_t type = kMinPacketType; type <= kMaxPacketType; ++type) {
|
|
if ((mask & static_cast<uint16_t>(1u << type)) == 0) continue;
|
|
if (!first) formatted[pos++] = ',';
|
|
if (type >= 10) formatted[pos++] = '1';
|
|
formatted[pos++] = static_cast<char>('0' + (type % 10));
|
|
first = false;
|
|
}
|
|
formatted[pos] = '\0';
|
|
|
|
if (output_size <= pos) return false;
|
|
memcpy(output, formatted, pos + 1);
|
|
return true;
|
|
}
|
|
|
|
// The keyword shortcut above means format() never actually emits the whole
|
|
// 0..15 list, but the emit loop is written for every type, so the internal
|
|
// buffer must still hold it: 22 digits + 15 separators + NUL.
|
|
static_assert(kFilterTextSize >= 38, "filter text buffer must hold the full 0..15 list");
|
|
|
|
inline bool allows(uint16_t mask, uint8_t packet_type) {
|
|
return packet_type <= kMaxPacketType &&
|
|
(mask & static_cast<uint16_t>(1u << packet_type)) != 0;
|
|
}
|
|
|
|
// How many types a mask allows. Lets a length-constrained reply fall back to
|
|
// "N/16" instead of a clipped list, which would read as a different allowlist.
|
|
inline uint8_t countTypes(uint16_t mask) {
|
|
uint8_t total = 0;
|
|
for (uint8_t type = kMinPacketType; type <= kMaxPacketType; ++type) {
|
|
if ((mask & static_cast<uint16_t>(1u << type)) != 0) total++;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// Conservative "could any configured broker want this type?" mask, used to
|
|
// reject a packet before it is copied into the publish queue. Disabled slots
|
|
// contribute nothing; a slot whose topic style later turns out not to support
|
|
// the publication is still counted here, so the gate never drops a packet the
|
|
// per-slot pass would have published.
|
|
inline uint16_t enabledUnion(const uint16_t* masks, const bool* enabled, size_t count) {
|
|
if (masks == nullptr) return 0;
|
|
uint16_t combined = 0;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
if (enabled != nullptr && !enabled[i]) continue;
|
|
combined = static_cast<uint16_t>(combined | masks[i]);
|
|
}
|
|
return combined;
|
|
}
|
|
|
|
// True when every slot still carries the default all-types mask, i.e. nothing
|
|
// depends on the packet-filter tail of /mqtt_prefs being written.
|
|
inline bool allMasksDefault(const uint16_t* masks, size_t count) {
|
|
if (masks == nullptr) return true;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
if (masks[i] != kAllPacketTypes) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Stage one of the publish gate: everything decidable before a packet is
|
|
// serialised or a topic is built. Cheap enough to run per packet per slot.
|
|
inline bool slotCandidate(bool slot_enabled, uint16_t mask, uint8_t packet_type) {
|
|
return slot_enabled && allows(mask, packet_type);
|
|
}
|
|
|
|
// The complete gate, once the slot's topic support is known. Eligibility
|
|
// deliberately excludes connection state: a temporarily disconnected broker
|
|
// that is configured for this type still requires the shared queue's existing
|
|
// bounded retry policy.
|
|
//
|
|
// eligiblePacketSlots() calls this directly, using slotCandidate() first only
|
|
// to skip the topic build for slots the mask already rejects. A slot that
|
|
// passes both therefore has its topic built again in the publish loop; that
|
|
// second build is the accepted cost of not carrying six 128-byte topics on the
|
|
// MQTT task stack.
|
|
inline bool slotEligible(bool slot_enabled, bool topic_supported,
|
|
uint16_t mask, uint8_t packet_type) {
|
|
return slotCandidate(slot_enabled, mask, packet_type) && topic_supported;
|
|
}
|
|
|
|
// A fully filtered/topic-incompatible packet is intentionally complete. Once
|
|
// any eligible target exists, at least one actual publish must succeed.
|
|
inline bool publishComplete(bool has_eligible_target, bool any_publish_succeeded) {
|
|
return any_publish_succeeded || !has_eligible_target;
|
|
}
|
|
|
|
} // namespace MQTTPacketFilter
|