134 lines
5.6 KiB
C++
134 lines
5.6 KiB
C++
#pragma once
|
|
|
|
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
#include <helpers/IdentityStore.h>
|
|
#include <new> // std::nothrow (heap-allocated client table)
|
|
|
|
#define PERM_ACL_ROLE_MASK 7 // lower 3 bits
|
|
#define PERM_ACL_GUEST 0
|
|
#define PERM_ACL_READ_ONLY 1
|
|
#define PERM_ACL_READ_WRITE 2
|
|
#define PERM_ACL_ADMIN 3
|
|
#define PERM_ACL_REGION_MGR 4
|
|
#define PERM_ACL_FILTER_MGR 5
|
|
|
|
#define OUT_PATH_FORCE_FLOOD 0xFE
|
|
#define OUT_PATH_UNKNOWN 0xFF
|
|
|
|
struct ClientInfo {
|
|
mesh::Identity id;
|
|
uint8_t permissions;
|
|
uint8_t out_path_len;
|
|
uint8_t out_path[MAX_PATH_SIZE];
|
|
bool out_path_is_persistable; // live route may replace, but not erase, durable route
|
|
uint8_t alt_path_len;
|
|
uint8_t alt_path[MAX_PATH_SIZE];
|
|
uint8_t observed_path_len; // reciprocal login PATH (transient)
|
|
uint8_t observed_path[MAX_PATH_SIZE];
|
|
bool observed_path_pending;
|
|
uint32_t observed_path_expiry;
|
|
uint8_t shared_secret[PUB_KEY_SIZE];
|
|
uint32_t last_timestamp; // by THEIR clock (exact live floor; reserved ceiling after load)
|
|
uint32_t last_activity; // by OUR clock (transient)
|
|
union {
|
|
struct {
|
|
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
|
|
uint32_t last_post_timestamp; // sender timestamp for room posts only (transient)
|
|
uint32_t pending_ack;
|
|
uint32_t push_post_timestamp;
|
|
unsigned long ack_timeout;
|
|
uint8_t push_failures;
|
|
} room;
|
|
struct {
|
|
uint32_t expiry_timestamp; // epoch seconds
|
|
uint32_t push_tag;
|
|
uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber
|
|
uint8_t min_deltas_len;
|
|
uint8_t min_deltas[14]; // LPP encoded
|
|
uint8_t prev_telem[14]; // LPP encoded
|
|
} sensor;
|
|
} extra;
|
|
|
|
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
|
bool isRegionMgr() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_REGION_MGR; }
|
|
bool isFilterMgr() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_FILTER_MGR; }
|
|
bool isProtectedManager() const { return isAdmin() || isRegionMgr() || isFilterMgr(); }
|
|
};
|
|
|
|
#ifndef MAX_CLIENTS
|
|
#define MAX_CLIENTS 32
|
|
#endif
|
|
|
|
static_assert(sizeof(void*) != 4 || sizeof(ClientInfo) <= 320,
|
|
"Update the client-table runtime RAM budget in check_firmware_ram.py");
|
|
|
|
struct ClientLoginReplayClampResult {
|
|
uint16_t stored_matched;
|
|
uint16_t stored_changed;
|
|
uint16_t live_matched;
|
|
uint16_t live_changed;
|
|
};
|
|
|
|
class ClientACL {
|
|
FILESYSTEM* _fs;
|
|
ClientInfo* clients;
|
|
int capacity; // 0 when the table could not be allocated
|
|
int num_clients;
|
|
bool login_replay_store_available;
|
|
bool acl_load_complete;
|
|
|
|
public:
|
|
// MAX_CLIENTS entries run to several kilobytes. Classic ESP32's link-time
|
|
// static DRAM window is much smaller than its runtime heap, so the table is
|
|
// allocated here instead of living in .bss. This constructor runs before
|
|
// setup(), while the heap is still unfragmented. A failed allocation leaves
|
|
// a zero-capacity ACL that refuses new clients rather than writing through
|
|
// a null table; putClient() returns NULL and callers already handle that.
|
|
ClientACL() {
|
|
_fs = NULL;
|
|
clients = new (std::nothrow) ClientInfo[MAX_CLIENTS];
|
|
capacity = clients ? MAX_CLIENTS : 0;
|
|
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
|
|
num_clients = 0;
|
|
login_replay_store_available = false;
|
|
acl_load_complete = false;
|
|
}
|
|
~ClientACL() { delete[] clients; }
|
|
ClientACL(const ClientACL&) = delete;
|
|
ClientACL& operator=(const ClientACL&) = delete;
|
|
void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id);
|
|
bool save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
|
|
bool clear();
|
|
|
|
// Authenticate the sender/password first, then call this before allocating
|
|
// or mutating a client. A true result means any required replay high-water
|
|
// reservation was durably published. runtime_last_timestamp is zero when
|
|
// this boot has no live state for the identity (including after eviction or
|
|
// revocation), in which case the durable tombstone is authoritative.
|
|
// login_permissions selects whether a missing identity may allocate durable
|
|
// state. Guest/read-only sessions still enforce and advance an existing
|
|
// tombstone, but otherwise use only their exact in-boot floor.
|
|
bool authorizeLoginTimestamp(const uint8_t* pubkey,
|
|
uint32_t sender_timestamp,
|
|
uint32_t runtime_last_timestamp,
|
|
uint8_t login_permissions);
|
|
|
|
// Explicitly authorized recovery only: the caller validates its transport,
|
|
// permissions and clock. A non-null selector is a complete PUB_KEY_SIZE key;
|
|
// null selects all records, including historical identities outside the ACL.
|
|
// Only lower existing values to now. Never insert/delete records or raise a
|
|
// floor. Publish durable changes before live changes; false returns zero
|
|
// counts and leaves live state untouched. Counts include duplicate records.
|
|
bool clampLoginReplayTimestamps(const uint8_t* pubkey, uint32_t now,
|
|
ClientLoginReplayClampResult& result);
|
|
|
|
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
|
|
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);
|
|
bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms);
|
|
|
|
int getNumClients() const { return num_clients; }
|
|
ClientInfo* getClientByIdx(int idx) { return &clients[idx]; }
|
|
const ClientInfo* getClientByIdx(int idx) const { return &clients[idx]; }
|
|
};
|