Files
MeshCore/test/test_repeater_guest_access.py
T

540 lines
25 KiB
Python

#!/usr/bin/env python3
"""Execute repeater guest authorization, saved toggles, and receive admission."""
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/guest_login"
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <helpers/GuestLoginSettings.h>
#include <helpers/ClientLoginPersistence.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/ClientPathPersistence.h>
#include <helpers/LazyPersistence.h>
#include <helpers/FloodCommandAliases.h>
#include <helpers/LoginHistory.h>
#define PUB_KEY_SIZE 32
#define MAX_PATH_SIZE 64
#define MAX_PACKET_PAYLOAD 184
#define PAYLOAD_TYPE_ANON_REQ 0x07
#define MAX_CLIENTS 16
#define MESH_CLIENT_REPEATER_ONLY 1
#define MESH_ENABLE_FLOOD_RULE_ENGINE 1
#define MESH_DEBUG_PRINTLN(...) ((void)0)
@CONSTANTS@
namespace mesh {
struct Identity {
uint8_t pub_key[PUB_KEY_SIZE];
bool matches(const Identity& other) const {
return memcmp(pub_key, other.pub_key, sizeof(pub_key)) == 0;
}
bool isHashMatch(const uint8_t* hash) const { return pub_key[0] == hash[0]; }
};
struct Packet {
bool direct = true;
uint8_t type = PAYLOAD_TYPE_ANON_REQ;
bool isRouteDirect() const { return direct; }
bool isRouteFlood() const { return !direct; }
uint8_t getPayloadType() const { return type; }
};
}
@CLIENT_INFO@;
struct ClientACL {
ClientInfo clients[MAX_CLIENTS]{};
int num_clients = 0;
unsigned authorizations = 0, allocations = 0;
bool authorize_ok = true, allocate_ok = true;
uint8_t authorized_role = 255;
ClientInfo* getClient(const uint8_t* key, int length) {
for (int i = 0; i < num_clients; ++i)
if (memcmp(clients[i].id.pub_key, key, size_t(length)) == 0) return &clients[i];
return nullptr;
}
int getNumClients() const { return num_clients; }
ClientInfo* getClientByIdx(int i) { return &clients[i]; }
const ClientInfo* getClientByIdx(int i) const { return &clients[i]; }
ClientInfo* putClient(const mesh::Identity& id, uint8_t permissions) {
++allocations;
if (auto* old = getClient(id.pub_key, PUB_KEY_SIZE)) return old;
if (!allocate_ok || num_clients == MAX_CLIENTS) return nullptr;
auto* client = &clients[num_clients++];
*client = ClientInfo{};
client->id = id;
client->permissions = permissions;
client->out_path_len = OUT_PATH_UNKNOWN;
client->alt_path_len = OUT_PATH_UNKNOWN;
client->observed_path_len = OUT_PATH_UNKNOWN;
client->out_path_is_persistable = true;
return client;
}
bool authorizeLoginTimestamp(const uint8_t*, uint32_t timestamp,
uint32_t floor, uint8_t role) {
++authorizations;
authorized_role = role;
return authorize_ok && mesh::planClientLoginReplay(floor, 0, timestamp).fresh;
}
int removeGuestClients();
};
@REMOVE_GUESTS@
struct Clock {
uint32_t now = 1234567890;
uint32_t getCurrentTime() const { return now; }
uint32_t getCurrentTimeUnique() { return now + ++unique; }
uint32_t unique = 0;
};
struct RNG { void random(uint8_t* data, size_t len) { memset(data, 0xB5, len); } };
@MANAGER_HELPERS@
struct MyMesh {
GuestMemoryFS fs;
mesh::GuestLoginSettings guest_login;
mesh::LoginHistory login_history;
ClientACL acl;
Clock clock;
RNG rng;
struct Prefs { char password[16] = "admin"; char guest_password[16] = ""; } _prefs;
unsigned long dirty_contacts_expiry = 0;
uint8_t contacts_save_failures = 0, reply_data[160]{};
int matching_peer_indexes[MAX_CLIENTS]{};
unsigned received = 0;
unsigned anonymous_requests = 0;
uint8_t reply_path_len = 0;
MyMesh() { assert(guest_login.begin(&fs)); }
Clock* getRTCClock() { return &clock; }
RNG* getRNG() { return &rng; }
unsigned long futureMillis(unsigned long delay) { return 10000 + delay; }
uint8_t handleLoginReq(const mesh::Identity&, const uint8_t*, uint32_t, const uint8_t*, bool);
void onAnonDataRecv(mesh::Packet*, const uint8_t*, const mesh::Identity&, uint8_t*, size_t);
uint8_t handleAnonRegionsReq(const mesh::Identity&, uint32_t, const uint8_t*, size_t) { ++anonymous_requests; return 0; }
uint8_t handleAnonOwnerReq(const mesh::Identity&, uint32_t, const uint8_t*, size_t) { ++anonymous_requests; return 0; }
uint8_t handleAnonClockReq(const mesh::Identity&, uint32_t, const uint8_t*, size_t) { ++anonymous_requests; return 0; }
bool handleGuestCommand(const char*, char*, size_t);
int searchPeersByHash(const uint8_t*);
void onPeerDataRecv(mesh::Packet*, uint8_t, int, const uint8_t*, uint8_t*, size_t);
bool onPeerPathRecv(mesh::Packet*, int, const uint8_t*, uint8_t*, uint8_t, uint8_t, uint8_t*, uint8_t);
int handleRequest(ClientInfo*, uint32_t, uint8_t*, size_t, size_t);
void dispatch(ClientInfo*, const char*, char*);
};
@LOGIN@
@ANONYMOUS@
@COMMAND@
@SEARCH@
@PATH@
@DATA_ADMISSION@
@REQUEST_ADMISSION@
@DISPATCH@
static mesh::Identity identity(unsigned number) {
mesh::Identity id;
memset(id.pub_key, 0xAA, PUB_KEY_SIZE);
id.pub_key[PUB_KEY_SIZE - 1] = uint8_t(number);
return id;
}
static const uint8_t secret[PUB_KEY_SIZE] = {0xB7};
using LoginResult = mesh::LoginHistory::Result;
static void history(const MyMesh& mesh, unsigned id, uint32_t attempts, LoginResult result) {
const auto* entry = mesh.login_history.get(0);
assert(entry && memcmp(entry->key, identity(id).pub_key, PUB_KEY_SIZE) == 0);
assert(entry->timestamp == mesh.clock.now && entry->attempts == attempts && entry->result == result);
}
static uint8_t login(MyMesh& mesh, unsigned id, const char* password,
uint32_t timestamp = 100, bool flood = true) {
guest_metadata_fs = &mesh.fs;
return mesh.handleLoginReq(identity(id), secret, timestamp,
reinterpret_cast<const uint8_t*>(password), flood);
}
static ClientInfo* add(MyMesh& mesh, unsigned number, uint8_t role) {
auto* client = mesh.acl.putClient(identity(number), role);
assert(client);
memcpy(client->shared_secret, secret, sizeof(secret));
client->last_activity = 7;
client->last_timestamp = 10;
client->out_path_len = 1;
client->out_path[0] = 0x42;
return client;
}
static void command(MyMesh& mesh, const char* text, const char* expected) {
guest_metadata_fs = &mesh.fs;
char reply[160];
memset(reply, '#', sizeof(reply));
assert(mesh.handleGuestCommand(text, reply, 157));
assert(strcmp(reply, expected) == 0);
assert(reply[157] == '#' && reply[158] == '#' && reply[159] == '#');
}
static void defaultsAndPasswords() {
MyMesh mesh;
assert(!mesh.guest_login.enabled() && mesh.fs.files.empty());
assert(login(mesh, 1, "") == 0 && mesh.acl.authorizations == 0);
assert(login(mesh, 1, "wrong") == 0 && mesh.acl.num_clients == 0);
command(mesh, "get guest", "> off");
command(mesh, "set guest on", "OK - guest on");
assert(login(mesh, 1, "") == 13);
assert(mesh.reply_data[6] == 0 && mesh.reply_data[7] == PERM_ACL_GUEST);
assert(mesh.acl.clients[0].last_activity == mesh.clock.now);
assert(mesh.dirty_contacts_expiry == 0); // Guest sessions stay out of the ACL file.
strcpy(mesh._prefs.guest_password, "guest-code");
const ClientInfo before = mesh.acl.clients[0];
assert(login(mesh, 1, "", 101) == 0); // Cached identity cannot bypass changed password.
assert(login(mesh, 1, "wrong", 102) == 0);
assert(memcmp(&before, &mesh.acl.clients[0], sizeof(before)) == 0);
assert(login(mesh, 1, "guest-code", 103) == 13);
assert(login(mesh, 1, "guest-code", 103) == 0); // Existing replay floor still applies.
MyMesh reboot;
reboot.fs.files = mesh.fs.files;
guest_metadata_fs = &reboot.fs;
assert(reboot.guest_login.begin(&reboot.fs) && reboot.guest_login.enabled());
assert(reboot.acl.num_clients == 0);
command(reboot, "set guest off", "OK - guest off");
MyMesh off_reboot;
off_reboot.fs.files = reboot.fs.files;
guest_metadata_fs = &off_reboot.fs;
assert(off_reboot.guest_login.begin(&off_reboot.fs) && !off_reboot.guest_login.enabled());
assert(login(off_reboot, 2, "") == 0);
MyMesh blank_admin;
blank_admin._prefs.password[0] = 0;
assert(login(blank_admin, 1, "") == 0);
command(blank_admin, "set guest on", "OK - guest on");
assert(login(blank_admin, 1, "") == 13 && blank_admin.reply_data[7] == PERM_ACL_GUEST);
}
static void assignedRoles() {
for (unsigned permissions = 0; permissions < 256; ++permissions) {
if ((permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) continue;
MyMesh mesh;
auto* client = add(mesh, 1, uint8_t(permissions));
assert(login(mesh, 1, "wrong", 100, false) == 13);
assert(client->permissions == permissions && client->out_path_len == 1);
assert(client->last_timestamp == 100 && client->last_activity == mesh.clock.now);
assert(mesh.reply_data[7] == permissions);
assert(login(mesh, 1, "admin", 101) == 13);
assert(client->permissions == ((permissions & ~PERM_ACL_ROLE_MASK) | PERM_ACL_ADMIN));
}
MyMesh guest;
add(guest, 1, 0xA0);
const ClientInfo before = guest.acl.clients[0];
assert(login(guest, 1, "", 100) == 0 && guest.acl.authorizations == 0);
assert(memcmp(&before, &guest.acl.clients[0], sizeof(before)) == 0);
assert(login(guest, 1, "admin", 101) == 13);
assert(guest.acl.clients[0].permissions == (0xA0 | PERM_ACL_ADMIN));
MyMesh failure;
failure.acl.authorize_ok = false;
assert(login(failure, 1, "admin") == 0 && failure.acl.allocations == 0);
failure.acl.authorize_ok = true;
failure.acl.allocate_ok = false;
assert(login(failure, 1, "admin") == 0 && failure.acl.num_clients == 0);
}
static void revocationAndFailures() {
MyMesh mesh;
command(mesh, "set guest on", "OK - guest on");
add(mesh, 1, PERM_ACL_GUEST);
const ClientInfo admin = *add(mesh, 2, PERM_ACL_ADMIN);
add(mesh, 3, 0xF8);
const ClientInfo manager = *add(mesh, 4, PERM_ACL_FILTER_MGR);
command(mesh, "set guest off", "OK - guest off");
assert(mesh.acl.num_clients == 2);
assert(memcmp(&admin, &mesh.acl.clients[0], sizeof(admin)) == 0);
assert(memcmp(&manager, &mesh.acl.clients[1], sizeof(manager)) == 0);
ClientInfo zero{};
assert(memcmp(&zero, &mesh.acl.clients[2], sizeof(zero)) == 0);
assert(memcmp(&zero, &mesh.acl.clients[3], sizeof(zero)) == 0);
command(mesh, "set guest on", "OK - guest on");
assert(mesh.acl.getClient(identity(1).pub_key, PUB_KEY_SIZE) == nullptr);
assert(login(mesh, 1, "", 200) == 13);
mesh.fs.fail_open_write = true;
command(mesh, "set guest off", "Err - guest setting save unconfirmed; reboot may differ");
assert(mesh.guest_login.healthy() && mesh.guest_login.enabled());
assert(mesh.acl.num_clients == 3); // Confirmed old setting remains live.
mesh.fs.fail_open_write = false;
mesh.fs.fail_public_read = true;
command(mesh, "set guest off", "Err - guest setting save unconfirmed; reboot may differ");
assert(!mesh.guest_login.healthy() && !mesh.guest_login.enabled());
assert(mesh.acl.num_clients == 2); // Uncertain storage fails closed and revokes.
assert(login(mesh, 1, "", 201) == 0);
command(mesh, "get guest", "> off (settings unavailable)");
assert(login(mesh, 2, "wrong", 300) == 13); // Assigned admin survives fail-closed guest storage.
}
static void commandPermissions() {
MyMesh mesh;
for (const char* invalid : {"get guest on", "set guest", "set guest 1", "set guest ON",
"set guest on extra", "set guest off extra"}) {
command(mesh, invalid, "Err - use get guest or set guest on|off");
assert(!mesh.guest_login.enabled() && mesh.fs.writes == 0);
}
for (const char* unrelated : {"get guests", "get guest.password", "set guest.password secret",
"get guestx", "set guestx on"}) {
char reply[160] = "untouched";
assert(!mesh.handleGuestCommand(unrelated, reply, sizeof(reply)));
assert(strcmp(reply, "untouched") == 0);
}
for (uint8_t role : {PERM_ACL_GUEST, PERM_ACL_READ_ONLY, PERM_ACL_READ_WRITE,
PERM_ACL_REGION_MGR, PERM_ACL_FILTER_MGR, 6, 7}) {
ClientInfo caller{};
caller.permissions = role;
char reply[160]{};
mesh.dispatch(&caller, "set guest on", reply);
assert(strcmp(reply, "Err - not permitted") == 0);
assert(!mesh.guest_login.enabled());
mesh.dispatch(&caller, "get guest", reply);
assert(strcmp(reply, "Err - not permitted") == 0);
}
ClientInfo admin{};
admin.permissions = PERM_ACL_ADMIN;
char reply[160]{};
mesh.dispatch(&admin, "set guest on", reply);
assert(strcmp(reply, "OK - guest on") == 0 && mesh.guest_login.enabled());
mesh.dispatch(nullptr, "set guest off", reply);
assert(strcmp(reply, "OK - guest off") == 0 && !mesh.guest_login.enabled());
}
static void receiveAdmission() {
MyMesh mesh;
// Deliberately leave stale/in-flight guest state to exercise receive gates.
auto* guest = add(mesh, 1, 0xA0);
auto* admin = add(mesh, 2, PERM_ACL_ADMIN);
const uint8_t hash[] = {0xAA};
assert(mesh.searchPeersByHash(hash) == 1 && mesh.matching_peer_indexes[0] == 1);
mesh::Packet packet;
uint8_t data[8] = {1}, path[] = {0x55, 0x66};
mesh.matching_peer_indexes[0] = 0;
const ClientInfo before = *guest;
mesh.onPeerDataRecv(&packet, 0, 0, secret, data, sizeof(data));
assert(mesh.received == 0);
assert(mesh.handleRequest(guest, 100, data, sizeof(data), 160) == 0);
assert(!mesh.onPeerPathRecv(&packet, 0, secret, path, 2, 0x0F, nullptr, 0));
assert(memcmp(&before, guest, sizeof(before)) == 0);
mesh.matching_peer_indexes[0] = 1;
mesh.onPeerDataRecv(&packet, 0, 0, secret, data, sizeof(data));
assert(mesh.received == 1);
assert(mesh.handleRequest(admin, 100, data, sizeof(data), 160) == 1);
assert(!mesh.onPeerPathRecv(&packet, 0, secret, path, 2, 0x0F, nullptr, 0));
assert(admin->out_path_len == 2 && admin->last_activity == mesh.clock.now);
command(mesh, "set guest on", "OK - guest on");
assert(mesh.searchPeersByHash(hash) == 2);
mesh.matching_peer_indexes[0] = 0;
mesh.onPeerDataRecv(&packet, 0, 0, secret, data, sizeof(data));
assert(mesh.received == 3);
assert(mesh.handleRequest(guest, 100, data, sizeof(data), 160) == 1);
assert(!mesh.onPeerPathRecv(&packet, 0, secret, path, 2, 0x0F, nullptr, 0));
assert(guest->out_path_len == 2 && guest->last_activity == mesh.clock.now);
// A flood login still clears the old selected path and transient observation.
guest->observed_path_pending = true;
assert(login(mesh, 1, "", 200, true) == 13);
assert(guest->out_path_len == OUT_PATH_UNKNOWN && !guest->observed_path_pending);
}
static void loginHistoryDecisions() {
MyMesh mesh;
assert(mesh.login_history.size() == 0);
assert(login(mesh, 1, "") == 0);
assert(mesh.login_history.size() == 1);
history(mesh, 1, 1, LoginResult::GuestsDisabled);
assert(login(mesh, 1, "wrong") == 0);
history(mesh, 1, 2, LoginResult::GuestsDisabled);
command(mesh, "set guest on", "OK - guest on");
strcpy(mesh._prefs.guest_password, "private-guest");
assert(login(mesh, 1, "wrong") == 0);
history(mesh, 1, 3, LoginResult::WrongPassword);
assert(login(mesh, 1, "private-guest") == 13);
history(mesh, 1, 4, LoginResult::Accepted);
assert(login(mesh, 1, "private-guest") == 0);
history(mesh, 1, 5, LoginResult::ReplayOrStateRejected);
mesh.acl.authorize_ok = false;
assert(login(mesh, 2, "admin") == 0);
history(mesh, 2, 1, LoginResult::ReplayOrStateRejected);
mesh.acl.authorize_ok = true;
mesh.acl.allocate_ok = false;
assert(login(mesh, 2, "admin") == 0);
history(mesh, 2, 2, LoginResult::ACLUnavailable);
mesh.acl.allocate_ok = true;
assert(login(mesh, 2, "admin") == 13);
history(mesh, 2, 3, LoginResult::Accepted);
command(mesh, "set guest off", "OK - guest off");
assert(mesh.login_history.size() == 2); // Revoking sessions preserves attempts.
assert(login(mesh, 2, "wrong", 101) == 13); // Authorized ACL identity.
history(mesh, 2, 4, LoginResult::Accepted);
auto* manager = add(mesh, 3, PERM_ACL_FILTER_MGR);
manager->last_timestamp = 10;
assert(login(mesh, 3, "wrong", 101) == 13);
history(mesh, 3, 1, LoginResult::Accepted);
assert(login(mesh, 1, "private-guest", 102) == 0);
history(mesh, 1, 6, LoginResult::GuestsDisabled);
const auto before = mesh.fs.files;
const auto writes = mesh.fs.writes;
const uint32_t before_attempts = mesh.login_history.get(0)->attempts;
char reply[160];
assert(mesh::cli::handleLoginsGet(mesh.login_history, "get logins", reply, sizeof(reply)));
assert(!strstr(reply, "private-guest") && !strstr(reply, "admin") && !strstr(reply, "wrong"));
assert(mesh.fs.files == before && mesh.fs.writes == writes);
assert(mesh.login_history.get(0)->attempts == before_attempts);
MyMesh reboot;
assert(reboot.login_history.size() == 0); // History is deliberately volatile.
}
static void anonymousRequestsAreNotLogins() {
MyMesh mesh;
mesh::Packet packet;
uint8_t data[MAX_PACKET_PAYLOAD + 1]{};
const auto id = identity(1);
guest_metadata_fs = &mesh.fs;
for (uint8_t type : {ANON_REQ_TYPE_REGIONS, ANON_REQ_TYPE_OWNER, ANON_REQ_TYPE_BASIC}) {
data[4] = type;
mesh.onAnonDataRecv(&packet, secret, id, data, 6);
assert(mesh.login_history.size() == 0);
}
assert(mesh.anonymous_requests == 3);
data[4] = 0x04; // Unknown anonymous query.
mesh.onAnonDataRecv(&packet, secret, id, data, 6);
packet.direct = false;
data[4] = ANON_REQ_TYPE_REGIONS;
mesh.onAnonDataRecv(&packet, secret, id, data, 6);
mesh.onAnonDataRecv(nullptr, secret, id, data, 6);
mesh.onAnonDataRecv(&packet, secret, id, nullptr, 6);
for (size_t length : {size_t(0), size_t(3), size_t(MAX_PACKET_PAYLOAD)})
mesh.onAnonDataRecv(&packet, secret, id, data, length);
packet.type = 0;
data[4] = 0;
mesh.onAnonDataRecv(&packet, secret, id, data, 4);
assert(mesh.login_history.size() == 0);
packet.type = PAYLOAD_TYPE_ANON_REQ;
// A timestamp-only anonymous packet is the valid empty-password login form.
mesh.onAnonDataRecv(&packet, secret, id, data, 4);
history(mesh, 1, 1, LoginResult::GuestsDisabled);
assert(mesh.anonymous_requests == 3);
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "passwords")) defaultsAndPasswords();
else if (!strcmp(argv[1], "roles")) assignedRoles();
else if (!strcmp(argv[1], "revoke")) revocationAndFailures();
else if (!strcmp(argv[1], "commands")) commandPermissions();
else if (!strcmp(argv[1], "receive")) receiveAdmission();
else if (!strcmp(argv[1], "history")) loginHistoryDecisions();
else if (!strcmp(argv[1], "anonymous")) anonymousRequestsAreNotLogins();
else assert(false);
printf("repeater guest %s passed\n", argv[1]);
}
'''
class RepeaterGuestAccessTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
raise unittest.SkipTest("host C++17 compiler required")
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
acl_header = (ROOT / "src/helpers/ClientACL.h").read_text()
acl_source = (ROOT / "src/helpers/ClientACL.cpp").read_text()
constants = "\n".join(line for line in acl_header.splitlines()
if line.startswith(("#define PERM_ACL_", "#define OUT_PATH_")))
for name in ("FIRMWARE_VER_LEVEL", "RESP_SERVER_LOGIN_OK", "LAZY_CONTACTS_WRITE_DELAY",
"ANON_REQ_TYPE_REGIONS", "ANON_REQ_TYPE_OWNER", "ANON_REQ_TYPE_BASIC"):
constants += "\n" + re.search(r"^#define\s+" + name + r"\s+.*$", source, re.MULTILINE).group()
manager_helpers = "\n".join(extract_braced(source, signature) for signature in (
"static bool commandFamilyMatches(", "static bool commandListMatches(",
"static bool isCommonManagerReadOnlyAllowed(", "static bool isRegionMgrAllowed(",
"static bool isFilterMgrAllowed(",
))
dispatch = extract_braced(source, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
call = dispatch.index("if (handleGuestCommand(command, reply, 157)")
guard_start = dispatch.rindex("if (sender && !sender->isAdmin())", 0, call)
guard = extract_braced(dispatch[guard_start:], "if (sender && !sender->isAdmin())")
# Exercise the actual authorization branch and setter, excluding unrelated
# radio commands and the independently tested guest-list formatter.
command_dispatch = "void MyMesh::dispatch(ClientInfo* sender, const char* command, char* reply) {\n" + guard + "\nif (handleGuestCommand(command, reply, 157)) return;\nstrcpy(reply, \"unhandled\");\n}"
peer = extract_braced(source, "void MyMesh::onPeerDataRecv(")
gate_end = peer.index("#if defined(WITH_MQTT_NEIGHBORS)", peer.index("ClientInfo* client = acl.getClientByIdx(i);"))
# Compile the real receive admission through the guest gate. Radio request
# execution is replaced by a counter only after authorization succeeds.
data_admission = peer[:gate_end] + "++received;\n}"
request = extract_braced(source, "int MyMesh::handleRequest(")
request_admission = request[:request.index(" if (reply_capacity >")] + "++received; return 1;\n}"
anonymous = extract_braced(source, "void MyMesh::onAnonDataRecv(")
anonymous = anonymous[:anonymous.index(" if (reply_len == 0) return;")] + " (void)reply_len;\n }\n}"
cls.source_text = source
replacements = {
"@CONSTANTS@": constants,
"@CLIENT_INFO@": extract_braced(acl_header, "struct ClientInfo {"),
"@REMOVE_GUESTS@": extract_braced(acl_source, "int ClientACL::removeGuestClients("),
"@MANAGER_HELPERS@": manager_helpers,
"@LOGIN@": extract_braced(source, "uint8_t MyMesh::handleLoginReq("),
"@ANONYMOUS@": anonymous,
"@COMMAND@": extract_braced(source, "bool MyMesh::handleGuestCommand("),
"@SEARCH@": extract_braced(source, "int MyMesh::searchPeersByHash("),
"@PATH@": extract_braced(source, "bool MyMesh::onPeerPathRecv("),
"@DATA_ADMISSION@": data_admission,
"@REQUEST_ADMISSION@": request_admission,
"@DISPATCH@": command_dispatch,
}
generated = HARNESS
for marker, text in replacements.items():
generated = generated.replace(marker, text)
cls.work = tempfile.TemporaryDirectory(prefix="mesh-repeater-guest-")
cls.addClassCleanup(cls.work.cleanup)
work = Path(cls.work.name)
cpp = work / "test.cpp"
cpp.write_text(generated)
cls.binary = work / ("guest.exe" if sys.platform == "win32" else "guest")
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
result = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-sign-compare", "-Wno-misleading-indentation",
"-DESP32_PLATFORM", "-DESP32", *sanitizer,
"-I", str(FIXTURE / "mocks"), "-I", str(ROOT / "src"),
str(cpp), str(ROOT / "src/helpers/GuestLoginSettings.cpp"), "-o", str(cls.binary),
], capture_output=True, text=True, timeout=60)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
def scenario(self, name):
result = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("repeater guest " + name + " passed", result.stdout)
def test_default_password_change_and_reboot(self):
self.scenario("passwords")
def test_assigned_roles_and_admin_upgrade(self):
self.scenario("roles")
def test_disable_revokes_only_guests_and_storage_failure_fails_closed(self):
self.scenario("revoke")
def test_command_validation_and_admin_authorization(self):
self.scenario("commands")
def test_peer_request_and_path_admission(self):
self.scenario("receive")
def test_login_history_records_actual_authorization_results(self):
self.scenario("history")
def test_only_login_requests_enter_history(self):
self.scenario("anonymous")
def test_password_bytes_are_not_logged(self):
login = extract_braced(self.source_text, "uint8_t MyMesh::handleLoginReq(")
debug_calls = re.findall(r"MESH_DEBUG_PRINTLN\([\s\S]*?\);", login)
self.assertFalse(any(re.search(r"\bdata\b", call) for call in debug_calls), debug_calls)
def test_guest_controls_precede_portable_role_handoff(self):
# Current FULL builds and legacy PORTABLE_MQTT_OBSERVER builds both need
# the same saved policy and permissions, rather than a generic CLI fallback.
body = extract_braced(self.source_text, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
self.assertEqual(body.count("handleGuestCommand(command, reply, 157)"), 2)
self.assertEqual(body.count("mesh::cli::handleGuestsGet(acl, command, reply, 157)"), 2)
self.assertEqual(body.count("mesh::cli::handleLoginsGet(login_history, command, reply, 157)"), 2)
if __name__ == "__main__":
unittest.main()