Files
MeshCore/test/fixtures/room_web_service/service.cpp
T

931 lines
61 KiB
C++

#include "filesystem.h"
#include <cstdlib>
#include <new>
#include <array>
#include <ArduinoJson.h>
static bool deny_json_allocation = false;
static unsigned json_allocations = 0;
static void* trackedMalloc(size_t size) {
if (deny_json_allocation) return nullptr;
void* result = std::malloc(size); if (result) ++json_allocations; return result;
}
static void trackedFree(void* ptr) {
if (ptr) { assert(json_allocations != 0); --json_allocations; } std::free(ptr);
}
static void* trackedRealloc(void* ptr, size_t size) {
if (deny_json_allocation) return nullptr;
return std::realloc(ptr, size);
}
#define malloc trackedMalloc
#define free trackedFree
#define realloc trackedRealloc
#include <helpers/RoomWebJson.h>
#undef malloc
#undef free
#undef realloc
#include <helpers/RoomBoardStore.h>
#include <helpers/RoomMailWeb.h>
#include <helpers/RoomHistoryStore.h>
#include <helpers/RoomAccessPolicy.h>
#include <helpers/RoomLoginAuthorization.h>
#include <helpers/ClientPathPersistence.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/RoomClientPathCommand.h>
#include <helpers/LazyPersistence.h>
#include <helpers/RoomTopicStore.h>
#include <helpers/RoomCatchUp.h>
#define WITH_WEBCONFIG 1
#define PUB_KEY_SIZE 32
#define MAX_PATH_SIZE 64
#define MAX_HASH_SIZE 8
#define MESH_CLIENT_REPEATER_ONLY 0
#define MAX_POST_TEXT_LEN 151
#define MAX_UNSYNCED_POSTS 32
#define POST_SYNC_DELAY_SECS 2
#define PUSH_NOTIFY_DELAY_MILLIS 200
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#include "roles.inc"
static uint32_t ticks = 100;
static uint32_t millis() { return ticks; }
static uint32_t futureMillis(uint32_t duration) { return millis() + duration; }
namespace mesh {
struct Packet { static bool isValidPathLen(uint8_t length); };
struct Identity {
uint8_t pub_key[32];
Identity() = default;
explicit Identity(const uint8_t* key) { memcpy(pub_key, key, PUB_KEY_SIZE); }
bool matches(const Identity& other) const { return memcmp(pub_key, other.pub_key, 32) == 0; }
};
struct LocalIdentity : Identity {
void calcSharedSecret(uint8_t* output, const uint8_t* key) const {
for (size_t i = 0; i < PUB_KEY_SIZE; ++i) output[i] = key[i] ^ pub_key[i];
}
};
struct Utils {
static bool isHexChar(char);
static bool fromHex(uint8_t*, int, const char*);
// Cryptography is a boundary mock: every fixture token has a distinct key.
static void sha256(uint8_t* out, size_t length, const uint8_t* token, size_t token_length,
const uint8_t* server, size_t server_length) {
assert(length == 32 && token_length == 32 && server_length == 32);
for (size_t i = 0; i < length; ++i) out[i] = token[i] ^ server[i] ^ 0x5a;
}
static void toHex(char* output, const uint8_t* key, size_t length) {
static const char digits[] = "0123456789abcdef";
for (size_t i = 0; i < length; ++i) { output[2 * i] = digits[key[i] >> 4]; output[2 * i + 1] = digits[key[i] & 15]; }
output[length * 2] = 0;
}
static void sha256(uint8_t* output, size_t size, const uint8_t* bytes, size_t length) {
memset(output, 0, size);
for (size_t i = 0; i < length; ++i) output[i % size] ^= bytes[i];
}
};
}
struct StrHelper {
static void strncpy(char* out, const char* text, size_t capacity) { std::snprintf(out, capacity, "%s", text); }
};
struct Preferences {
static bool value, fail_open, fail_write;
bool begin(const char* name, bool readonly) { assert(strcmp(name, "mesh-room") == 0 && !readonly); return !fail_open; }
size_t putBool(const char* key, bool next) { assert(strcmp(key, "web") == 0); if (fail_write) return 0; value = next; return sizeof(bool); }
bool getBool(const char* key, bool fallback) { (void)fallback; assert(strcmp(key, "web") == 0); return value; }
void end() {}
};
bool Preferences::value = false, Preferences::fail_open = false, Preferences::fail_write = false;
#include "client.inc"
struct ACL {
ClientInfo clients[8]{};
ClientInfo durable[8]{};
bool retained[8]{};
ClientInfo& radio_admin = clients[0];
unsigned lookups = 0, saves = 0;
int count = 1;
int& num_clients = count;
static constexpr int capacity = 8;
bool acl_load_complete = true, protect_explicit_permissions = true;
bool save_ok = true;
int getNumClients() const { return count; }
ClientInfo* getClientByIdx(int i) { assert(i >= 0 && i < count); return &clients[i]; }
ClientInfo* getClient(const uint8_t* key, int length) {
++lookups; assert(length == PUB_KEY_SIZE);
for (int i = 0; i < count; ++i) {
if (memcmp(clients[i].id.pub_key, key, PUB_KEY_SIZE) == 0) return &clients[i];
}
return nullptr;
}
bool save(MemoryFS* fs, bool (*filter)(ClientInfo*)) {
assert(fs && filter); ++saves;
if (!save_ok) return false;
for (int i = 0; i < count; ++i) {
retained[i] = (clients[i].permissions & PERM_ACL_ROLE_MASK) != PERM_ACL_GUEST && filter(&clients[i]);
if (retained[i]) durable[i] = clients[i];
}
return true;
}
ClientInfo* putClient(const mesh::Identity& id, uint8_t permissions);
bool applyPermissions(const mesh::LocalIdentity& self, const uint8_t* key, int length, uint8_t permissions);
};
struct Clock {
uint32_t now = 1000;
uint32_t getCurrentTime() const { return now; }
uint32_t getCurrentTimeUnique() { return now++; }
};
#include "post.inc"
struct MyMesh {
MemoryFS storage; MemoryFS* _fs = &storage;
mesh::RoomAccessPolicy room_access;
mesh::LocalIdentity self_id{};
struct { char password[65] = "admin-secret", guest_password[65] = "write-secret", node_name[64] = "Test room"; bool allow_read_only = true; } _prefs;
bool room_web_enabled = false;
bool room_web_request_authenticated = false;
uint32_t room_web_boot_id = 71;
#include "web_user.inc"
ACL acl;
Clock clock;
PostInfo posts[MAX_UNSYNCED_POSTS] = {};
int next_post_idx = 0;
uint32_t last_room_timestamp = 0, post_ready_at[MAX_UNSYNCED_POSTS] = {}, post_ready_mask = UINT32_MAX, next_push = 0;
uint16_t _num_posted = 0;
mesh::RoomHistoryState room_history_state;
bool room_history_enabled = false, room_history_available = true;
char room_topic[152] = "Topic <img src=x onerror=attack()>";
unsigned long dirty_contacts_expiry = 0;
uint8_t contacts_save_failures = 0;
uint32_t room_topic_revision = 0, room_topic_timestamp = 0, room_topic_ready_at = 0;
bool room_topic_ready = false;
struct { unsigned clears = 0; void clear() { ++clears; } } recent_room_posts;
unsigned retry_cancellations = 0;
uint32_t cancelled_retry_timestamp = 0;
MyMesh() {
self_id.pub_key[0] = 4; metadata_filesystem = &storage; assert(room_access.load(_fs));
acl.radio_admin.permissions = PERM_ACL_ADMIN;
for (auto& client : acl.clients) {
client.out_path_len = client.alt_path_len = client.observed_path_len = OUT_PATH_UNKNOWN;
client.out_path_is_persistable = true;
}
}
Clock* getRTCClock() { return &clock; }
bool handleRoomWebCommand(const char*, char*);
void processRoomRequest(const uint8_t*, uint32_t, char*, char*, size_t);
void serviceRoomQuotas();
bool storePost(const mesh::Identity&, const char*);
bool snapshotRoomHistory();
bool executeClientPathCommand(ClientInfo*, mesh::RoomClientPathCommand, const char*, char*);
bool setRoomClientPath(ClientInfo*, const char*, const char*, char*);
bool applyRoomCatchUpCommand(ClientInfo*, const char*, uint32_t, char*);
static bool saveFilter(ClientInfo*);
uint8_t getUnsyncedCount(ClientInfo*);
bool roomClientChatEnabled(ClientInfo*);
bool handleRoomHistoryCommand(const char*, char*);
bool handleRoomTopicCommand(const char*, char*);
void activateRoomTopic();
void writeRoomClientJson(mesh::RoomJsonWriter&, ClientInfo*, bool);
void cancelActiveMessageRetries(const uint8_t*, uint32_t timestamp) {
++retry_cancellations; cancelled_retry_timestamp = timestamp;
}
};
#include "production.inc"
static std::array<uint8_t, 32> token(uint8_t id = 1) { std::array<uint8_t, 32> value = {}; value[0] = id; return value; }
static std::string execute(MyMesh& m, const std::string& input, uint32_t sequence,
uint8_t owner = 1, size_t capacity = 2048) {
metadata_filesystem = &m.storage;
std::vector<char> request(input.begin(), input.end()); request.push_back(0);
std::vector<char> reply(capacity + 2, 0x7f);
const auto key = token(owner);
m.processRoomRequest(key.data(), sequence, request.data(), reply.data() + 1, capacity);
assert(reply.front() == 0x7f && reply.back() == 0x7f && json_allocations == 0);
if (!capacity) return "";
assert(memchr(reply.data() + 1, 0, capacity)); return reply.data() + 1;
}
static JsonDocument result(MyMesh& m, const std::string& input, uint32_t sequence, uint8_t owner = 1) {
const auto text = execute(m, input, sequence, owner);
JsonDocument json; assert(deserializeJson(json, text) == DeserializationError::Ok); return json;
}
static std::string request(const char* op, const char* password = "write-secret", const std::string& fields = "") {
return std::string("{\"op\":\"") + op + "\",\"password\":\"" + password + "\"" + fields + "}";
}
static void error(MyMesh& m, const std::string& input, uint32_t sequence, const char* fragment, uint8_t owner = 1) {
const auto json = result(m, input, sequence, owner); const char* text = json["error"];
if (!text || !strstr(text, fragment)) fprintf(stderr, "Expected %s, got %s for %.120s\n", fragment, text ? text : "no error", input.c_str());
assert(text && strstr(text, fragment));
}
static std::string encoded(const std::string& body) {
std::vector<char> out(4 * ((body.size() + 2) / 3) + 1);
mesh::roomEncodeBase64(reinterpret_cast<const uint8_t*>(body.data()), body.size(), out.data()); return out.data();
}
static std::string saveRequest(uint8_t id, uint32_t version, const std::string& title, const std::string& body, const char* password = "admin-secret") {
return request("board.save", password, ",\"boot\":71,\"id\":" + std::to_string(id) + ",\"version\":" + std::to_string(version)
+ ",\"title\":\"" + title + "\",\"body64\":\"" + encoded(body) + "\"");
}
static void enable(MyMesh& m) { char reply[160]; assert(m.handleRoomWebCommand("set room.web on", reply)); assert(m.room_web_enabled); }
static unsigned floors(const MyMesh& m) { unsigned total = 0; for (const auto& user : m.room_web_users) total += user.last_sequence; return total; }
static std::string clientKey(const ClientInfo& client) {
char key[65]; mesh::Utils::toHex(key, client.id.pub_key, PUB_KEY_SIZE); return key;
}
static void seedClients(MyMesh& m, unsigned count = 5) {
assert(count <= 8); m.acl.count = count;
for (unsigned i = 0; i < count; ++i) {
auto& client = m.acl.clients[i];
memset(client.id.pub_key, i + 1, PUB_KEY_SIZE);
client.permissions = i == 0 ? PERM_ACL_ADMIN : PERM_ACL_READ_WRITE;
client.permissions_are_explicit = i < 2;
client.last_activity = 900 + i;
client.out_path_len = 1; client.out_path[0] = 0xA1 + i;
client.out_path_is_persistable = true;
client.alt_path_len = OUT_PATH_UNKNOWN;
client.extra.room.sync_since = 100 + i;
}
}
static void adminAuthorizationCases() {
const char* operations[] = {"admin.users", "admin.user", "admin.route", "admin.catchup",
"admin.access", "admin.ban", "admin.settings", "admin.topic", "admin.history", "admin.rates",
"admin.mail.list", "admin.mail.settings", "admin.mail.policy", "admin.mail.delete"};
unsigned denied = 0;
for (const char* password : {"", "wrong-password", "write-secret"}) {
for (const char* operation : operations) {
MyMesh m; enable(m); seedClients(m);
const std::vector<ClientInfo> before(m.acl.clients, m.acl.clients + m.acl.count);
const unsigned writes = m.storage.write_opens;
const auto reply = result(m, request(operation, password,
",\"boot\":71,\"cursor\":0,\"key\":\"" + clientKey(m.acl.clients[1])
+ "\",\"which\":\"outpath\",\"value\":\"B1\",\"expected\":\"> A2\","
"\"mode\":\"keep\",\"count\":1,\"expected_sync\":101,"
"\"role\":3,\"expected_role\":2,\"banned\":true"), 999);
assert(reply["error"].is<const char*>() && strstr(reply["error"].as<const char*>(), "admin"));
assert(reply.as<JsonObjectConst>().size() == 1);
assert(floors(m) == 0 && m.acl.lookups == 0 && m.acl.saves == 0);
assert(m.storage.write_opens == writes && m.room_access.banCount() == 0);
assert(memcmp(before.data(), m.acl.clients, before.size() * sizeof(ClientInfo)) == 0);
assert(m._num_posted == 0 && m.last_room_timestamp == 0);
++denied;
}
}
MyMesh closed; enable(closed); closed._prefs.allow_read_only = false;
error(closed, request("admin.users", "wrong-password", ",\"cursor\":0"), 100, "incorrect");
assert(floors(closed) == 0 && !closed.room_web_request_authenticated && closed.acl.lookups == 0);
MyMesh empty; enable(empty); empty._prefs.password[0] = 0;
error(empty, request("admin.users", "admin-secret", ",\"cursor\":0"), 100, "admin");
assert(floors(empty) == 0 && empty.acl.lookups == 0);
printf("ADMIN_AUTH: %u rejected low-trust operations leave state and identity data unchanged\n", denied + 2);
}
static std::string adminClientRequest(const char* operation, const ClientInfo& client,
const std::string& fields = "", uint32_t boot = 71) {
return request(operation, "admin-secret", ",\"boot\":" + std::to_string(boot)
+ ",\"key\":\"" + clientKey(client) + "\"" + fields);
}
static std::string routeFields(const char* which, const char* value, const char* expected) {
if (strncmp(expected, "> ", 2) == 0) expected += 2;
return std::string(",\"which\":\"") + which + "\",\"value\":\"" + value
+ "\",\"expected\":\"" + expected + "\"";
}
static void adminRouteCases() {
MyMesh m; enable(m); seedClients(m);
auto& retained = m.acl.clients[1];
const ClientInfo other = m.acl.clients[0];
uint32_t sequence = 1;
error(m, adminClientRequest("admin.route", retained,
routeFields("outpath", "B1", "> A2"), 70), sequence++, "restarted");
assert(m.acl.saves == 0 && floors(m) == 0);
assert(result(m, adminClientRequest("admin.route", retained,
routeFields("outpath", "B1", "> A2")), sequence++)["ok"] == true);
assert(retained.out_path_len == 1 && retained.out_path[0] == 0xB1 && m.acl.saves == 1);
assert(m.acl.retained[1] && !m.acl.retained[2]);
assert(memcmp(&other, &m.acl.clients[0], sizeof(other)) == 0);
assert(retained.permissions == PERM_ACL_READ_WRITE && retained.permissions_are_explicit);
assert(result(m, adminClientRequest("admin.route", retained,
routeFields("outpath", "B1", "> B1")), sequence++)["ok"] == true);
assert(m.acl.saves == 1);
const ClientInfo selected = retained;
const auto stale = result(m, adminClientRequest("admin.route", retained,
routeFields("outpath", "C1", "> A2")), sequence++);
assert(stale["error"].is<const char*>() && m.acl.saves == 1);
assert(memcmp(&retained, &selected, sizeof(selected)) == 0);
for (const char* value : {"B1,", "B1,,B2", "B1,1234", "direct;erase", "get prv.key", "path"}) {
const auto invalid = result(m, adminClientRequest("admin.route", retained,
routeFields("outpath", value, "> B1")), sequence++);
assert(invalid["error"].is<const char*>());
assert(memcmp(&retained, &selected, sizeof(selected)) == 0 && m.acl.saves == 1);
}
auto& transient = m.acl.clients[2];
assert(result(m, adminClientRequest("admin.route", transient,
routeFields("altpath", "C1,C2", "> unknown")), sequence++)["ok"] == true);
assert(transient.alt_path_len == 2 && transient.alt_path[0] == 0xC1 && m.acl.saves == 1);
assert(transient.permissions == PERM_ACL_READ_WRITE && !transient.permissions_are_explicit);
m.acl.save_ok = false;
m.dirty_contacts_expiry = 50000; m.contacts_save_failures = 3;
error(m, adminClientRequest("admin.route", retained,
routeFields("altpath", "D1", "> unknown")), sequence++, "save failed");
assert(m.acl.saves == 2 && memcmp(&retained, &selected, sizeof(selected)) == 0);
assert(m.dirty_contacts_expiry == 50000 && m.contacts_save_failures == 3);
m.acl.save_ok = true;
const std::string key = clientKey(retained);
for (const std::string& invalid_key : {key.substr(0, 8), key.substr(0, 12), std::string(64, '0'), std::string(64, 'g')}) {
const auto denied = result(m, request("admin.route", "admin-secret",
",\"boot\":71,\"key\":\"" + invalid_key + "\"" + routeFields("outpath", "D1", "> B1")), sequence++);
assert(denied["error"].is<const char*>() && m.acl.saves == 2);
assert(memcmp(&retained, &selected, sizeof(selected)) == 0);
}
puts("ADMIN_ROUTES: exact identities, stale guards, filtered saves, no-op writes, transient roles and rollback passed");
}
static void unreadPosts(MyMesh& m, const ClientInfo& client) {
m.posts[0].post_timestamp = 150; m.posts[1].post_timestamp = 200;
m.posts[2].post_timestamp = 250; m.posts[3].post_timestamp = 300;
for (unsigned i = 0; i < 4; ++i) memset(m.posts[i].author.pub_key, 0x99, PUB_KEY_SIZE);
m.posts[4].post_timestamp = 1000; m.posts[4].author = client.id;
m.last_room_timestamp = 1000;
}
static void adminCatchUpCases() {
MyMesh m; enable(m); seedClients(m);
auto& client = m.acl.clients[1]; unreadPosts(m, client);
client.extra.room.pending_ack = 0x12345678;
client.extra.room.push_post_timestamp = 150;
client.extra.room.ack_timeout = 9999;
client.extra.room.push_failures = 2;
const ClientInfo other = m.acl.clients[0];
uint32_t sequence = 1;
assert(result(m, adminClientRequest("admin.catchup", client,
",\"mode\":\"keep\",\"count\":2,\"expected_sync\":101"), sequence++)["ok"] == true);
assert(client.extra.room.sync_since == 200 && client.extra.room.pending_ack == 0);
assert(client.extra.room.push_post_timestamp == 0 && client.extra.room.ack_timeout == 0);
assert(client.extra.room.push_failures == 0 && m.acl.saves == 1);
assert(m.retry_cancellations == 1 && m.cancelled_retry_timestamp == 150);
assert(memcmp(&other, &m.acl.clients[0], sizeof(other)) == 0);
const ClientInfo advanced = client;
const auto stale = result(m, adminClientRequest("admin.catchup", client,
",\"mode\":\"keep\",\"count\":0,\"expected_sync\":101"), sequence++);
assert(stale["error"].is<const char*>() && m.acl.saves == 1);
assert(memcmp(&client, &advanced, sizeof(advanced)) == 0);
assert(m.retry_cancellations == 1);
assert(result(m, adminClientRequest("admin.catchup", client,
",\"mode\":\"keep\",\"count\":99,\"expected_sync\":200"), sequence++)["ok"] == true);
assert(memcmp(&client, &advanced, sizeof(advanced)) == 0 && m.acl.saves == 1);
m.acl.save_ok = false;
const auto failed = result(m, adminClientRequest("admin.catchup", client,
",\"mode\":\"keep\",\"count\":0,\"expected_sync\":200"), sequence++);
assert(failed["error"].is<const char*>() && m.acl.saves == 2);
assert(memcmp(&client, &advanced, sizeof(advanced)) == 0);
m.acl.save_ok = true;
assert(result(m, adminClientRequest("admin.catchup", client,
",\"mode\":\"before\",\"before\":4294967295,\"expected_sync\":200"), sequence++)["ok"] == true);
assert(client.extra.room.sync_since == 300 && m.acl.saves == 3);
// A retained own-author post must not create a future catch-up cursor.
assert(client.extra.room.sync_since < m.posts[4].post_timestamp);
auto& transient = m.acl.clients[2]; unreadPosts(m, transient);
transient.extra.room.pending_ack = 0x87654321;
transient.extra.room.pending_topic_revision = 7;
transient.extra.room.push_post_timestamp = 150;
assert(result(m, adminClientRequest("admin.catchup", transient,
",\"mode\":\"keep\",\"count\":0,\"expected_sync\":102"), sequence++)["ok"] == true);
assert(transient.extra.room.sync_since == 300 && transient.extra.room.pending_ack == 0x87654321);
assert(transient.extra.room.pending_topic_revision == 7 && m.acl.saves == 3);
assert(!transient.permissions_are_explicit && transient.permissions == PERM_ACL_READ_WRITE);
puts("ADMIN_CATCHUP: newest counts, stale cursors, future dates, own posts, topic ACKs, rollback and transient policy passed");
}
static void adminListingCases() {
MyMesh m; enable(m); seedClients(m);
auto& selected = m.acl.clients[1]; unreadPosts(m, selected);
selected.extra.room.pending_ack = 1234;
selected.extra.room.push_post_timestamp = 150;
selected.extra.room.ack_timeout = ticks + 3000;
selected.observed_path_len = 0x41;
selected.observed_path[0] = 0x12; selected.observed_path[1] = 0x34;
m.acl.clients[2].last_activity = 0;
m.acl.clients[3].last_activity = m.clock.now + 1;
m.acl.clients[3].extra.room.push_failures = 3;
assert(m.room_access.addBan(m._fs, m.acl.clients[4].id.pub_key) == mesh::RoomAccessPolicy::BanResult::Saved);
uint32_t sequence = 1;
for (unsigned cursor : {0, 2, 4}) {
const std::string output = execute(m, request("admin.users", "admin-secret",
",\"cursor\":" + std::to_string(cursor)), sequence++);
assert(output.size() < 2048);
JsonDocument page; assert(deserializeJson(page, output) == DeserializationError::Ok);
assert(page["total"] == 5 && page["boot"] == 71);
assert(page["active"] == 3 && page["backlog"] == 24);
assert(page["pending"] == 1 && page["failed"] == 1);
const unsigned expected = cursor == 4 ? 1 : 2;
assert(page["users"].is<JsonArray>() && page["users"].size() == expected);
for (unsigned i = 0; i < expected; ++i) {
const auto user = page["users"][i];
assert(user["key"].as<std::string>() == clientKey(m.acl.clients[cursor + i]));
assert(user["shared_secret"].isUnbound() && user["last_timestamp"].isUnbound());
assert(user["outpath"].isUnbound() && user["altpath"].isUnbound());
}
if (cursor < 4) assert(page["next"] == cursor + 2);
else assert(page["next"].isNull());
}
const auto detail = result(m, adminClientRequest("admin.user", selected), sequence++);
assert(detail["boot"] == 71 && detail["user"]["key"].as<std::string>() == clientKey(selected));
assert(detail["user"]["outpath"] == "A2" && detail["user"]["altpath"] == "unknown");
assert(detail["user"]["observed"] == "1234" && detail["user"]["sync_since"] == 101);
assert(detail["user"]["delivery"] == "post" && detail["user"]["ack_wait_ms"] == 3000);
assert(detail["user"]["shared_secret"].isUnbound() && m.acl.saves == 0);
const auto inactive = result(m, adminClientRequest("admin.user", m.acl.clients[2]), sequence++);
assert(inactive["user"]["active"] == false && inactive["user"]["heard_ago"].isNull());
const auto future = result(m, adminClientRequest("admin.user", m.acl.clients[3]), sequence++);
assert(future["user"]["heard_ago"].isNull());
const auto banned = result(m, adminClientRequest("admin.user", m.acl.clients[4]), sequence++);
assert(banned["user"]["banned"] == true && banned["user"]["active"] == false);
selected.out_path_len = selected.alt_path_len = selected.observed_path_len = 0x60;
memset(selected.out_path, 0xA1, MAX_PATH_SIZE);
memset(selected.alt_path, 0xB2, MAX_PATH_SIZE);
memset(selected.observed_path, 0xC3, MAX_PATH_SIZE);
const std::string maximum = execute(m, adminClientRequest("admin.user", selected), sequence++);
JsonDocument complete; assert(deserializeJson(complete, maximum) == DeserializationError::Ok);
assert(complete["user"]["outpath"].as<std::string>().size() == 128 && maximum.size() < 2048);
for (size_t capacity = 0; capacity < 750; ++capacity) {
execute(m, adminClientRequest("admin.user", selected), sequence++, 1, capacity);
}
assert(m.acl.saves == 0);
puts("ADMIN_USERS: two-row pages, delivery detail, full routes, inactive/future/banned states and bounded JSON passed");
}
static void adminAccessAndSettingsCases() {
MyMesh m; enable(m); seedClients(m);
uint32_t sequence = 1;
auto& selected = m.acl.clients[1];
const std::string target_key = clientKey(selected);
const ClientInfo following = m.acl.clients[2];
const auto changed_access = result(m, adminClientRequest("admin.access", selected,
",\"role\":1,\"expected_role\":2"), sequence++);
assert(changed_access["ok"] == true && changed_access["pending_save"] == true);
assert(selected.permissions == PERM_ACL_READ_ONLY && selected.permissions_are_explicit);
assert(m.dirty_contacts_expiry != 0 && m.acl.saves == 0);
const auto stale = result(m, adminClientRequest("admin.access", selected,
",\"role\":3,\"expected_role\":2"), sequence++);
assert(stale["error"].is<const char*>() && selected.permissions == PERM_ACL_READ_ONLY);
assert(result(m, adminClientRequest("admin.access", selected,
",\"role\":0,\"expected_role\":1"), sequence++)["ok"] == true);
assert(m.acl.count == 4 && m.acl.getClient(following.id.pub_key, PUB_KEY_SIZE) != nullptr);
assert(clientKey(m.acl.clients[1]) == clientKey(following));
uint8_t removed[PUB_KEY_SIZE]; memset(removed, 2, sizeof(removed));
assert(m.acl.getClient(removed, PUB_KEY_SIZE) == nullptr);
const unsigned before_ban = m.storage.write_opens;
assert(result(m, request("admin.ban", "admin-secret", ",\"boot\":71,\"key\":\""
+ target_key + "\",\"banned\":true"), sequence++)["ok"] == true);
assert(m.room_access.isBanned(removed) && m.storage.write_opens > before_ban);
const unsigned after_ban = m.storage.write_opens;
assert(result(m, request("admin.ban", "admin-secret", ",\"boot\":71,\"key\":\""
+ target_key + "\",\"banned\":true"), sequence++)["ok"] == true);
assert(m.storage.write_opens == after_ban);
assert(result(m, request("admin.ban", "admin-secret", ",\"boot\":71,\"key\":\""
+ target_key + "\",\"banned\":false"), sequence++)["ok"] == true);
assert(!m.room_access.isBanned(removed));
const unsigned reads_only = m.storage.write_opens;
const auto settings = result(m, request("admin.settings", "admin-secret"), sequence++);
assert(settings["boot"] == 71 && settings["topic"] == m.room_topic);
assert(settings["persistent_history"] == false && settings["post_rate"] == 0 && settings["poll_rate"] == 0);
assert(m.storage.write_opens == reads_only);
const std::string old_topic = m.room_topic;
const auto topic = result(m, request("admin.topic", "admin-secret", ",\"boot\":71,\"value\":\"New topic\",\"expected\":\""
+ old_topic + "\""), sequence++);
assert(topic["ok"] == true && strcmp(m.room_topic, "New topic") == 0);
const uint32_t revision = m.room_topic_revision;
const auto stale_topic = result(m, request("admin.topic", "admin-secret", ",\"boot\":71,\"value\":\"Stale\",\"expected\":\""
+ old_topic + "\""), sequence++);
assert(stale_topic["error"].is<const char*>() && strcmp(m.room_topic, "New topic") == 0 && m.room_topic_revision == revision);
assert(result(m, request("admin.history", "admin-secret", ",\"boot\":71,\"enabled\":true,\"expected\":false"), sequence++)["ok"] == true);
assert(m.room_history_enabled);
const auto stale_history = result(m, request("admin.history", "admin-secret", ",\"boot\":71,\"enabled\":false,\"expected\":false"), sequence++);
assert(stale_history["error"].is<const char*>() && m.room_history_enabled);
assert(result(m, request("admin.rates", "admin-secret", ",\"boot\":71,\"post_rate\":12,\"poll_rate\":60"), sequence++)["ok"] == true);
assert(m.room_access.postRate() == 12 && m.room_access.pollRate() == 60);
const unsigned rate_writes = m.storage.write_opens;
assert(result(m, request("admin.rates", "admin-secret", ",\"boot\":71,\"post_rate\":12,\"poll_rate\":60"), sequence++)["ok"] == true);
assert(m.storage.write_opens == rate_writes);
ticks += 60000;
m.storage.capacity = 0;
const auto failed_topic = result(m, request("admin.topic", "admin-secret", ",\"boot\":71,\"value\":\"Not saved\",\"expected\":\"New topic\""), sequence++);
assert(failed_topic["error"].is<const char*>() && strcmp(m.room_topic, "New topic") == 0);
m.storage.reset(); m.storage.faults.insert("open:w:/room_hist_cfg.tmp");
const auto failed_history = result(m, request("admin.history", "admin-secret", ",\"boot\":71,\"enabled\":false,\"expected\":true"), sequence++);
assert(failed_history["error"].is<const char*>() && m.room_history_enabled);
m.storage.reset(); m.storage.capacity = 0;
const auto failed_rates = result(m, request("admin.rates", "admin-secret", ",\"boot\":71,\"post_rate\":13,\"poll_rate\":61"), sequence++);
assert(failed_rates["error"].is<const char*>() && m.room_access.postRate() == 12 && m.room_access.pollRate() == 60);
puts("ADMIN_SETTINGS: queued ACL semantics, role revocation, bans, topic/history/rates stale guards and storage failure passed");
}
static void adminInvalidAndSessionCases() {
struct Invalid { const char* operation; const char* fields; };
const Invalid invalid[] = {
{"admin.users", ""}, {"admin.users", ",\"cursor\":-1"},
{"admin.users", ",\"cursor\":\"0\""}, {"admin.users", ",\"cursor\":1.5"},
{"admin.users", ",\"cursor\":false"}, {"admin.users", ",\"cursor\":6"},
{"admin.user", ",\"key\":9"}, {"admin.user", ",\"key\":\"02020202\""},
{"admin.user", ",\"key\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\""},
{"admin.route", ",\"which\":\"secret\",\"value\":\"B1\",\"expected\":\"A2\""},
{"admin.route", ",\"which\":false,\"value\":\"B1\",\"expected\":\"A2\""},
{"admin.route", ",\"which\":\"outpath\",\"value\":[],\"expected\":\"A2\""},
{"admin.route", ",\"which\":\"outpath\",\"value\":\"B1\",\"expected\":2"},
{"admin.route", ",\"which\":\"outpath\",\"value\":\"B1\",\"expected\":\"A2\\u0000\""},
{"admin.catchup", ",\"mode\":\"skip\",\"count\":0,\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"keep\",\"count\":-1,\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"keep\",\"count\":65536,\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"keep\",\"count\":\"0\",\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"keep\",\"count\":1.5,\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"keep\",\"count\":0,\"expected_sync\":\"101\""},
{"admin.catchup", ",\"mode\":\"before\",\"before\":-1,\"expected_sync\":101"},
{"admin.catchup", ",\"mode\":\"before\",\"before\":4294967296,\"expected_sync\":101"},
{"admin.access", ",\"role\":4,\"expected_role\":2"},
{"admin.access", ",\"role\":5,\"expected_role\":2"},
{"admin.access", ",\"role\":256,\"expected_role\":2"},
{"admin.access", ",\"role\":-1,\"expected_role\":2"},
{"admin.access", ",\"role\":2.5,\"expected_role\":2"},
{"admin.access", ",\"role\":\"3\",\"expected_role\":2"},
{"admin.access", ",\"role\":3,\"expected_role\":\"2\""},
{"admin.ban", ",\"banned\":\"true\""}, {"admin.ban", ",\"banned\":1"},
{"admin.history", ",\"enabled\":1,\"expected\":false"},
{"admin.history", ",\"enabled\":true,\"expected\":\"false\""},
{"admin.topic", ",\"value\":7,\"expected\":\"unchanged\""},
{"admin.topic", ",\"value\":\"New\\u0000topic\",\"expected\":\"unchanged\""},
{"admin.rates", ",\"post_rate\":-1,\"poll_rate\":60"},
{"admin.rates", ",\"post_rate\":65536,\"poll_rate\":60"},
{"admin.rates", ",\"post_rate\":1.5,\"poll_rate\":60"},
{"admin.rates", ",\"post_rate\":12,\"poll_rate\":\"60\""},
{"admin.cli", ",\"value\":\"erase\""}, {"admin.userdetail", ""},
{"admin.users\\u0000post", ",\"cursor\":0"},
};
for (const auto& entry : invalid) {
MyMesh m; enable(m); seedClients(m); strcpy(m.room_topic, "unchanged");
const std::vector<ClientInfo> clients(m.acl.clients, m.acl.clients + m.acl.count);
const unsigned writes = m.storage.write_opens;
const auto denied = result(m, adminClientRequest(entry.operation, m.acl.clients[1], entry.fields), 1);
assert(denied["error"].is<const char*>() && denied.as<JsonObjectConst>().size() == 1);
assert(m.acl.count == 5 && memcmp(clients.data(), m.acl.clients, clients.size() * sizeof(ClientInfo)) == 0);
assert(m.acl.saves == 0 && m.dirty_contacts_expiry == 0 && m.retry_cancellations == 0);
assert(m.storage.write_opens == writes && m.room_access.banCount() == 0);
assert(!m.room_history_enabled && strcmp(m.room_topic, "unchanged") == 0);
assert(m.room_access.postRate() == 0 && m.room_access.pollRate() == 0);
}
const char* writes[] = {"admin.route", "admin.catchup", "admin.access", "admin.ban",
"admin.topic", "admin.history", "admin.rates", "admin.mail.policy", "admin.mail.delete"};
for (const char* operation : writes) {
MyMesh m; enable(m); seedClients(m);
error(m, adminClientRequest(operation, m.acl.clients[1], "", 70), 99, "restarted");
error(m, request(operation, "admin-secret", ",\"boot\":\"71\""), 99, "restarted");
error(m, request(operation, "admin-secret"), 99, "restarted");
assert(floors(m) == 0 && m.acl.lookups == 0 && m.acl.saves == 0);
}
MyMesh m; enable(m); seedClients(m);
const auto mutation = adminClientRequest("admin.route", m.acl.clients[1], routeFields("outpath", "B1", "A2"));
assert(result(m, mutation, 1)["ok"] == true && m.acl.saves == 1);
error(m, mutation, 1, "completed"); assert(m.acl.saves == 1);
error(m, request("admin.users", "write-secret", ",\"cursor\":0"), 100, "admin");
assert(floors(m) == 1);
assert(result(m, request("admin.users", "admin-secret", ",\"cursor\":0"), 2)["users"].size() == 2);
MyMesh full; enable(full);
for (uint8_t owner = 1; owner <= 8; ++owner) {
assert(result(full, request("admin.settings", "admin-secret"), 1, owner)["boot"] == 71);
}
error(full, request("admin.settings", "admin-secret"), 1, "busy", 9);
assert(floors(full) == 8 && full.acl.lookups == 0);
MyMesh banned; enable(banned);
uint8_t author[PUB_KEY_SIZE]; const auto identity = token();
mesh::Utils::sha256(author, sizeof(author), identity.data(), identity.size(), banned.self_id.pub_key, PUB_KEY_SIZE);
assert(banned.room_access.addBan(banned._fs, author) == mesh::RoomAccessPolicy::BanResult::Saved);
error(banned, request("admin.settings", "admin-secret"), 1, "access denied");
assert(floors(banned) == 0 && banned.acl.lookups == 0 && !banned.room_web_request_authenticated);
printf("ADMIN_INVALID: %zu strict schemas, %zu boot guards, replay/auth floors, bounded sessions and banned browser passed\n", std::size(invalid), std::size(writes) * 3);
}
static std::string browserKey(const MyMesh& m, uint8_t owner) {
const auto identity = token(owner); uint8_t author[32]; char key[65];
mesh::Utils::sha256(author, sizeof(author), identity.data(), identity.size(), m.self_id.pub_key, PUB_KEY_SIZE);
mesh::Utils::toHex(key, author, sizeof(author)); return key;
}
static std::string quoted(const std::string& value) {
JsonDocument json; json.set(value); std::string output; serializeJson(json, output); return output;
}
static std::string mailPolicyFields(const char* mode, uint32_t revision, bool only = false,
const std::vector<std::string>& allowed = {}) {
std::string fields = ",\"boot\":71,\"mode\":" + quoted(mode) + ",\"expected_revision\":" + std::to_string(revision)
+ ",\"mailbox_only\":" + (only ? "true" : "false") + ",\"allowlist\":[";
for (size_t i = 0; i < allowed.size(); ++i) { if (i) fields += ','; fields += quoted(allowed[i]); }
return fields + ']';
}
static std::string mailSendRequest(const std::string& recipient, const std::string& text,
const char* password = "write-secret") {
return request("mail.send", password, ",\"boot\":71,\"recipient\":" + quoted(recipient) + ",\"text\":" + quoted(text));
}
static std::string mailListRequest(uint8_t cursor = 0, uint32_t revision = 0,
const char* password = "") {
return request("mail.list", password, ",\"cursor\":" + std::to_string(cursor) + ",\"revision\":" + std::to_string(revision));
}
static std::string mailOwnerRequest(const char* operation, const std::string& owner,
const std::string& fields = "") {
return request(operation, "admin-secret", ",\"owner\":" + quoted(owner) + fields);
}
static uint32_t mailRevision(MyMesh& m, uint32_t& sequence, uint8_t owner) {
auto settings = result(m, request("mail.settings", ""), sequence++, owner);
assert(!settings["error"] && settings["revision"].is<uint32_t>());
return settings["revision"].as<uint32_t>();
}
static void mailAuthorizationAndPolicyCases() {
MyMesh m; enable(m); uint32_t sequence = 1;
const auto first = result(m, request("mail.settings", ""), sequence++, 1);
const std::string owner = first["owner"].as<std::string>(), sender = browserKey(m, 2);
assert(owner == browserKey(m, 1) && owner != sender);
assert(first["mode"] == "closed" && first["revision"] == 0 && first["total"] == 0 && first["mailbox_only"] == false);
const auto impersonated = result(m, request("mail.settings", "", ",\"owner\":" + quoted(owner)
+ ",\"key\":" + quoted(clientKey(m.acl.radio_admin)) + ",\"permissions\":3"), sequence++, 2);
assert(impersonated["owner"] == sender && m.acl.lookups == 0);
auto policy = result(m, request("mail.policy", "", mailPolicyFields("public", 0, true)), sequence++, 1);
assert(policy["mode"] == "public" && policy["revision"].as<uint32_t>() > 0 && policy["mailbox_only"] == true);
const uint32_t initial_revision = policy["revision"];
error(m, request("mail.policy", "", mailPolicyFields("closed", 0)), sequence++, "changed", 1);
assert(result(m, request("mail.settings", ""), sequence++, 1)["mode"] == "public");
assert(result(m, request("status", ""), sequence++, 1)["mailbox_only"] == true);
assert(result(m, request("status", ""), sequence++, 2)["mailbox_only"] == false);
error(m, mailSendRequest(owner, "reader cannot send", ""), sequence++, "read-only", 2);
auto sent = result(m, mailSendRequest(owner, "public mail"), sequence++, 2);
assert(sent["ok"] == true && sent["id"].as<uint32_t>() > initial_revision);
const uint32_t id = sent["id"], revision = mailRevision(m, sequence, 1);
error(m, request("mail.read", "", ",\"owner\":" + quoted(owner) + ",\"id\":" + std::to_string(id) + ",\"offset\":0"), sequence++, "not found", 2);
error(m, request("mail.ack", "", ",\"boot\":71,\"owner\":" + quoted(owner) + ",\"id\":" + std::to_string(id)), sequence++, "not found", 2);
assert(result(m, request("mail.check", ""), sequence++, 1)["unread"] == 1);
policy = result(m, request("mail.policy", "", mailPolicyFields("private", revision, true, {sender})), sequence++, 1);
assert(policy["allowlist"].size() == 1 && policy["allowlist"][0] == sender);
error(m, mailSendRequest(owner, "unapproved mail"), sequence++, "not accepting", 3);
assert(result(m, mailSendRequest(owner, "approved mail"), sequence++, 2)["ok"] == true);
const uint32_t private_revision = mailRevision(m, sequence, 1);
policy = result(m, request("mail.policy", "", mailPolicyFields("closed", private_revision)), sequence++, 1);
assert(policy["mode"] == "closed");
error(m, mailSendRequest(owner, "closed mail"), sequence++, "not accepting", 2);
assert(result(m, request("mail.check", ""), sequence++, 1)["total"] == 2 && m._num_posted == 0);
assert(result(m, request("status", ""), sequence++, 1)["mailbox_only"] == false);
const auto before = m.storage.get(mesh::ROOM_MAIL_PRIMARY_PATH);
const uint32_t closed_revision = mailRevision(m, sequence, 1);
error(m, request("mail.policy", "", mailPolicyFields("private", closed_revision, false, {sender, sender})), sequence++, "invalid", 1);
std::vector<std::string> too_many(9, sender);
error(m, request("mail.policy", "", mailPolicyFields("private", closed_revision, false, too_many)), sequence++, "eight", 1);
assert(m.storage.get(mesh::ROOM_MAIL_PRIMARY_PATH) == before && m.acl.lookups == 0);
printf("MAIL_AUTH: browser token identity isolation, reader/writer gates, strict stale-zero policy, public/private/closed and sender limits passed\n");
}
static void mailDeliveryAndDurabilityCases() {
MyMesh m; enable(m); const std::string recipient = browserKey(m, 2);
assert(result(m, request("mail.policy", "", mailPolicyFields("public", 0, true)), 1, 2)["mode"] == "public");
std::string text; for (unsigned i = 0; i < 128; ++i) text += "\xF0\x9F\x93\xA1";
assert(text.size() == 512);
const auto submission = mailSendRequest(recipient, text); auto sent = result(m, submission, 200, 1);
assert(sent["ok"] == true); const uint32_t id = sent["id"];
error(m, submission, 200, "completed", 1);
error(m, mailSendRequest(recipient, text + "x"), 201, "1-512", 1);
error(m, request("mail.send", "write-secret", ",\"boot\":71,\"recipient\":" + quoted(recipient) + ",\"text\":\"before\\u0000after\""), 202, "1-512", 1);
error(m, mailSendRequest(recipient.substr(0, 12), "bad recipient"), 203, "complete", 1);
MyMesh restored; restored.storage.files = m.storage.files; enable(restored);
const auto duplicate = result(restored, submission, 200, 1);
assert(duplicate["ok"] == true && duplicate["id"] == id && restored.storage.write_opens == 0);
uint32_t sequence = 1;
auto page = result(restored, mailListRequest(), sequence++, 2);
assert(page["messages"].size() == 1 && page["messages"][0]["id"] == id && page["messages"][0]["sender"] == browserKey(restored, 1));
assert(page["messages"][0]["length"] == 512 && page["messages"][0]["acked"] == false && page["next"].isNull());
std::string assembled;
for (unsigned offset = 0; offset < 512;) {
auto chunk = result(restored, request("mail.read", "", ",\"id\":" + std::to_string(id) + ",\"offset\":" + std::to_string(offset)), sequence++, 2);
uint8_t bytes[128]; size_t length = 0;
assert(chunk["id"] == id && chunk["sender"] == browserKey(restored, 1) && chunk["length"] == 512 && chunk["created"] == 1000);
assert(chunk["offset"] == offset && chunk["acked"] == false && mesh::roomDecodeBase64(chunk["data64"], bytes, sizeof(bytes), length));
assert(length == 128 && chunk["count"] == length && chunk["next"] == offset + length);
assembled.append(reinterpret_cast<const char*>(bytes), length); offset += length;
}
assert(assembled == text);
auto count = result(restored, request("mail.check", ""), sequence++, 2);
assert(count["total"] == 1 && count["unread"] == 1 && count["mailbox_only"] == true);
const auto eof = result(restored, request("mail.read", "", ",\"id\":" + std::to_string(id) + ",\"offset\":512"), sequence++, 2);
assert(eof["count"] == 0 && eof["next"] == 512 && eof["data64"] == "");
assert(result(restored, request("mail.ack", "", ",\"boot\":71,\"id\":" + std::to_string(id)), sequence++, 2)["ok"] == true);
assert(result(restored, request("mail.check", ""), sequence++, 2)["unread"] == 0);
error(restored, request("mail.read", "", ",\"id\":" + std::to_string(id) + ",\"offset\":0"), sequence++, "not found", 2);
MyMesh after_ack; after_ack.storage.files = restored.storage.files; enable(after_ack);
const auto repeated = result(after_ack, submission, 200, 1);
assert(repeated["ok"] == true && repeated["id"] == id);
assert(result(after_ack, request("mail.check", ""), 1, 2)["total"] == 0);
assert(result(after_ack, request("mail.ack", "", ",\"boot\":71,\"id\":" + std::to_string(id)), 2, 2)["ok"] == true);
printf("MAIL_DELIVERY: 512-byte UTF-8, bounded chunks/EOF, read retains unread mail, explicit durable ACK and retry across reboot passed\n");
}
static void mailAdministratorCases() {
MyMesh m; enable(m); uint32_t sequence = 1; const std::string target = browserKey(m, 2), sender = browserKey(m, 1);
auto selected = result(m, mailOwnerRequest("admin.mail.settings", target), sequence++, 9);
assert(selected["owner"] == target && selected["revision"] == 0 && selected["blocked"] == false);
selected = result(m, mailOwnerRequest("admin.mail.policy", target, mailPolicyFields("private", 0, true, {sender})), sequence++, 9);
assert(selected["owner"] == target && selected["allowlist"][0] == sender && selected["mailbox_only"] == true);
const uint32_t revision = selected["revision"];
error(m, mailOwnerRequest("admin.mail.policy", target, mailPolicyFields("closed", 0)), sequence++, "changed", 9);
error(m, mailOwnerRequest("admin.mail.policy", target, mailPolicyFields("closed", revision - 1)), sequence++, "changed", 9);
const auto sent = result(m, mailSendRequest(target, "recipient-only content"), sequence++, 1); assert(sent["ok"] == true);
auto metadata = result(m, request("admin.mail.list", "admin-secret", ",\"cursor\":0,\"revision\":0"), sequence++, 9);
assert(metadata["mailboxes"].size() == 1 && metadata["mailboxes"][0]["owner"] == target && metadata["mailboxes"][0]["total"] == 1);
assert(!metadata["mailboxes"][0]["text"] && !metadata["mailboxes"][0]["data64"] && !metadata["mailboxes"][0]["allowlist"]);
error(m, mailOwnerRequest("admin.mail.read", target, ",\"id\":" + std::to_string(sent["id"].as<uint32_t>()) + ",\"offset\":0"), sequence++, "unknown", 9);
auto own = result(m, request("mail.list", "admin-secret", ",\"owner\":" + quoted(target) + ",\"cursor\":0,\"revision\":0"), sequence++, 9);
assert(own["messages"].size() == 0);
assert(result(m, request("admin.ban", "admin-secret", ",\"boot\":71,\"key\":" + quoted(target) + ",\"banned\":true"), sequence++, 9)["ok"] == true);
selected = result(m, mailOwnerRequest("admin.mail.settings", target), sequence++, 9);
assert(selected["blocked"] == true && selected["mode"] == "private" && selected["allowlist"][0] == sender);
error(m, mailSendRequest(target, "blocked recipient"), sequence++, "blocked", 1);
error(m, request("mail.check", ""), sequence++, "denied", 2);
assert(result(m, mailOwnerRequest("admin.mail.delete", target, ",\"boot\":71,\"id\":0"), sequence++, 9)["ok"] == true);
selected = result(m, mailOwnerRequest("admin.mail.settings", target), sequence++, 9);
assert(selected["total"] == 0 && selected["mode"] == "private" && selected["blocked"] == true);
assert(result(m, request("admin.ban", "admin-secret", ",\"boot\":71,\"key\":" + quoted(target) + ",\"banned\":false"), sequence++, 9)["ok"] == true);
error(m, request("mail.delete", "", ",\"boot\":71,\"id\":0"), sequence++, "identifier", 2);
printf("MAIL_ADMIN: selected full-key policies, metadata-only enumeration, blocked field/gates, owner-isolated admin reads and confirmed purge passed\n");
}
static void mailPagingAndStorageCases() {
MyMesh m; enable(m); uint32_t sequence = 1; const std::string target = browserKey(m, 2);
assert(result(m, request("mail.policy", "", mailPolicyFields("public", 0)), sequence++, 2)["mode"] == "public");
std::vector<uint32_t> ids;
for (unsigned i = 0; i < 3; ++i) { auto sent = result(m, mailSendRequest(target, "queued " + std::to_string(i)), sequence++, 1); assert(sent["ok"] == true); ids.push_back(sent["id"]); }
auto page = result(m, mailListRequest(), sequence++, 2); assert(page["messages"].size() == 2 && page["next"] == 2);
const uint32_t snapshot = page["revision"];
assert(result(m, mailSendRequest(target, "arrival between pages"), sequence++, 1)["ok"] == true);
error(m, mailListRequest(2, snapshot), sequence++, "changed", 2);
page = result(m, mailListRequest(), sequence++, 2);
const uint32_t newer = page["revision"];
assert(result(m, request("mail.delete", "", ",\"boot\":71,\"id\":" + std::to_string(ids[0])), sequence++, 2)["ok"] == true);
error(m, mailListRequest(2, newer), sequence++, "changed", 2);
error(m, mailListRequest(2, 0), sequence++, "revision", 2);
page = result(m, mailListRequest(), sequence++, 2);
auto last = result(m, mailListRequest(2, page["revision"]), sequence++, 2);
assert(last["messages"].size() == 1 && last["next"].isNull());
for (uint8_t owner = 3; owner <= 4; ++owner) assert(result(m, mailOwnerRequest("admin.mail.policy", browserKey(m, owner), mailPolicyFields("public", 0)), sequence++, 9)["mode"] == "public");
auto owners = result(m, request("admin.mail.list", "admin-secret", ",\"cursor\":0,\"revision\":0"), sequence++, 9);
assert(owners["mailboxes"].size() == 2 && owners["next"] == 2); const uint32_t owner_snapshot = owners["revision"];
assert(result(m, mailSendRequest(target, "admin snapshot mutation"), sequence++, 1)["ok"] == true);
error(m, request("admin.mail.list", "admin-secret", ",\"cursor\":2,\"revision\":" + std::to_string(owner_snapshot)), sequence++, "changed", 9);
owners = result(m, request("admin.mail.list", "admin-secret", ",\"cursor\":0,\"revision\":0"), sequence++, 9);
auto owners_last = result(m, request("admin.mail.list", "admin-secret", ",\"cursor\":2,\"revision\":" + std::to_string(owners["revision"].as<uint32_t>())), sequence++, 9);
assert(owners_last["mailboxes"].size() == 1 && owners_last["next"].isNull());
error(m, mailSendRequest(target, "owner queue is full"), sequence++, "full", 1);
const auto primary = m.storage.get(mesh::ROOM_MAIL_PRIMARY_PATH);
m.storage.capacity = 0;
error(m, request("mail.ack", "", ",\"boot\":71,\"id\":" + std::to_string(ids[1])), sequence++, "save failed", 2);
assert(m.storage.get(mesh::ROOM_MAIL_PRIMARY_PATH) == primary); m.storage.reset();
assert(result(m, request("mail.check", ""), sequence++, 2)["total"] == 4);
uint8_t key[32]; assert(mesh::roomMailWebKey(key, sizeof(key), target.c_str()));
mesh::room_mail_detail::Index index; mesh::room_mail_detail::Entry entry; mesh::room_mail_detail::Owner owner; bool found = false;
assert(mesh::room_mail_detail::load(m._fs, key, index, entry, owner, found, false) == mesh::RoomMailResult::Success && found);
char bank[24]; mesh::room_mail_detail::ownerPath(entry, bank);
auto corrupt = m.storage.get(bank); corrupt.back() ^= 1; m.storage.put(bank, corrupt);
auto denied = result(m, request("mail.read", "", ",\"id\":" + std::to_string(ids[1]) + ",\"offset\":0"), sequence++, 2);
assert(denied.as<JsonObjectConst>().size() == 1 && strstr(denied["error"].as<const char*>(), "storage unavailable"));
error(m, mailSendRequest(target, "preserve corrupt evidence"), sequence++, "storage unavailable", 1);
assert(m.storage.get(bank) == corrupt && m.storage.get(mesh::ROOM_MAIL_PRIMARY_PATH) == primary);
assert(result(m, request("status", ""), sequence++, 2)["mailbox_only"] == true);
printf("MAIL_PAGING: explicit revisions reject message arrivals/deletions between pages and changed admin snapshots passed\n");
printf("MAIL_STORAGE: full owner queue, failed ACK retention, corrupt-bank fail-closed reads/status and preserved evidence passed\n");
}
static void mailGlobalQueueCases() {
MyMesh m; enable(m); uint32_t sequence = 1;
for (uint8_t owner = 2; owner <= 6; ++owner) assert(result(m, mailOwnerRequest("admin.mail.policy", browserKey(m, owner), mailPolicyFields("public", 0)), sequence++, 9)["mode"] == "public");
for (uint8_t owner = 2; owner <= 5; ++owner) for (unsigned i = 0; i < 4; ++i) assert(result(m, mailSendRequest(browserKey(m, owner), "bounded global queue"), sequence++, 1)["ok"] == true);
error(m, mailSendRequest(browserKey(m, 6), "seventeenth message"), sequence++, "full", 1);
const auto owners = result(m, request("admin.mail.list", "admin-secret", ",\"cursor\":0,\"revision\":0"), sequence++, 9);
assert(owners["queued"] == 16);
assert(result(m, mailOwnerRequest("admin.mail.delete", browserKey(m, 2), ",\"boot\":71,\"id\":0"), sequence++, 9)["ok"] == true);
assert(result(m, mailSendRequest(browserKey(m, 6), "space released by purge"), sequence++, 1)["ok"] == true);
printf("MAIL_GLOBAL: bounded 16-message server queue rejects without acceptance and recovers capacity after explicit purge passed\n");
}
int main() {
unsigned scenarios = 0;
{
MyMesh m; assert(!m.room_web_enabled);
error(m, request("status"), 1, "disabled"); assert(floors(m) == 0);
char reply[160]; assert(m.handleRoomWebCommand("get room.web", reply) && strstr(reply, "off"));
Preferences::fail_write = true; assert(m.handleRoomWebCommand("set room.web on", reply) && !m.room_web_enabled);
Preferences::fail_write = false; enable(m);
assert(!m.handleRoomWebCommand("get room.webx", reply));
auto status = result(m, request("status"), 1); assert(status["role"] == PERM_ACL_READ_WRITE && status["boot"] == 71);
assert(strcmp(status["topic"], m.room_topic) == 0 && status["persistent_history"] == false);
status = result(m, request("status", "bad-password", ",\"key\":\"radio-admin\",\"permissions\":3"), 2);
assert(status["role"] == PERM_ACL_GUEST && m.acl.lookups == 0);
const std::string web_key = status["web_key"].as<std::string>(); assert(web_key.size() == 64);
error(m, request("post", "bad-password", ",\"boot\":71,\"name\":\"A\",\"text\":\"denied\""), 500, "read-only");
assert(floors(m) == 2 && m.last_room_timestamp == 0 && m._num_posted == 0);
assert(result(m, request("status", "admin-secret"), 3)["role"] == PERM_ACL_ADMIN); ++scenarios;
}
{
MyMesh m; enable(m); m._prefs.allow_read_only = false;
error(m, request("status", "wrong"), 100, "incorrect"); assert(floors(m) == 0 && !m.room_web_request_authenticated);
error(m, request("status"), 100, "invalid browser", 0); assert(floors(m) == 0 && !m.room_web_request_authenticated);
assert(result(m, request("status"), 1)["role"] == PERM_ACL_READ_WRITE);
error(m, request("post", "write-secret", ",\"boot\":70,\"name\":\"A\",\"text\":\"no\""), 200, "restarted");
assert(floors(m) == 1 && m._num_posted == 0);
error(m, saveRequest(1, 0, "Title", "body", "write-secret"), 300, "admin"); assert(floors(m) == 1);
const auto okay = result(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"one\""), 2);
assert(okay["ok"] == true && m.last_room_timestamp == 1000 && m._num_posted == 1);
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"one\""), 2, "completed");
assert(m._num_posted == 1 && m.acl.radio_admin.extra.room.last_post_timestamp == 0);
auto post = result(m, request("posts", "write-secret", ",\"after\":0"), 3);
assert(strcmp(post["post"]["text"], "A: one") == 0 && post["post"]["timestamp"] == 1000);
post = result(m, request("posts", "write-secret", ",\"after\":1000"), 4); assert(post["post"].isNull()); ++scenarios;
}
{
MyMesh m; enable(m); m.room_history_enabled = true;
const std::string text(148, 'x');
assert(result(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"" + text + "\""), 1)["ok"] == true);
assert(strlen(m.posts[0].text) == 151 && m.room_history_state.count == 1);
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"" + text + "x\""), 2, "151");
assert(m._num_posted == 1 && m.last_room_timestamp == 1000);
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A:B\",\"text\":\"x\""), 3, "display name");
m.storage.capacity = 0;
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"retain\""), 4, "not retained");
assert(m._num_posted == 1 && m.last_room_timestamp == 1000 && m.room_history_state.count == 1);
m.storage.reset(); m.clock.now = 2;
assert(result(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"retry with new sequence\""), 5)["timestamp"] == 1001);
assert(m._num_posted == 2 && m.room_history_state.count == 2); ++scenarios;
}
{
MyMesh m; enable(m); assert(m.room_access.setRates(m._fs, 1, 2));
assert(result(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"one\""), 1)["ok"] == true);
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"two\""), 2, "rate limit");
assert(m._num_posted == 1 && m.last_room_timestamp == 1000);
result(m, request("status"), 3); result(m, request("status"), 4); error(m, request("status"), 5, "rate limit");
ticks += 60000; result(m, request("status"), 6);
const auto key = token(); uint8_t author[32]; mesh::Utils::sha256(author, 32, key.data(), 32, m.self_id.pub_key, 32);
assert(m.room_access.addBan(m._fs, author) == mesh::RoomAccessPolicy::BanResult::Saved);
const unsigned floor = floors(m); error(m, request("status"), 1000, "denied"); assert(floors(m) == floor);
assert(m.room_access.removeBan(m._fs, author) == mesh::RoomAccessPolicy::BanResult::Saved);
result(m, request("status"), 7); ++scenarios;
}
{
MyMesh m; enable(m); const std::string title = u8"Café 地図 📡"; const std::string body(2048, 'b');
const auto saved = result(m, saveRequest(1, 0, title, body), 1);
if (saved["ok"] != true) fprintf(stderr, "Full board save error: %s\n", saved["error"].as<const char*>());
assert(saved["ok"] == true);
assert(saved["version"] == 1);
auto page = result(m, request("board.index", "", ",\"revision\":0,\"cursor\":0"), 2);
assert(page["revision"] == 1 && page["articles"].size() == 1 && page["next"] == 255);
assert(page["articles"][0]["length"] == 2048 && strcmp(page["articles"][0]["title"], title.c_str()) == 0);
std::string assembled;
for (unsigned offset = 0, sequence = 3; offset < 2048; ++sequence) {
auto chunk = result(m, request("board.read", "", ",\"id\":1,\"version\":1,\"offset\":" + std::to_string(offset)), sequence);
assert(chunk["version"] == 1 && chunk["offset"] == offset && chunk["total"] == 2048);
uint8_t bytes[128]; size_t length; assert(mesh::roomDecodeBase64(chunk["data64"], bytes, sizeof(bytes), length) && length > 0 && length <= 128);
assembled.append(reinterpret_cast<const char*>(bytes), length); offset += length; assert(chunk["next"] == offset);
}
assert(assembled == body);
const auto updated = result(m, saveRequest(1, 1, "Updated", "new body"), 30);
assert(updated["ok"] == true && updated["version"] == 2);
error(m, request("board.index", "", ",\"revision\":1,\"cursor\":0"), 31, "board changed");
error(m, request("board.read", "", ",\"id\":1,\"version\":1,\"offset\":128"), 32, "article changed");
error(m, saveRequest(1, 1, "Stale", "old"), 33, "article changed");
error(m, request("board.delete", "admin-secret", ",\"boot\":71,\"id\":1,\"version\":0"), 34, "current article version");
assert(result(m, request("board.delete", "admin-secret", ",\"boot\":71,\"id\":1,\"version\":2"), 35)["ok"] == true);
assert(result(m, saveRequest(1, 0, "Reused", "new"), 36)["ok"] == true);
error(m, request("board.read", "", ",\"id\":1,\"version\":2,\"offset\":0"), 37, "article changed"); ++scenarios;
}
{
MyMesh m; enable(m); uint32_t sequence = 1;
for (const std::string& input : {std::string("{"), std::string("[]"), std::string("null"), std::string("{\"op\":{\"a\":{\"b\":{\"c\":{\"d\":1}}}}}"),
std::string("{\"op\":\"status\",\"password\":\"write-secret\\u0000ignored\"}"), std::string("{\"op\":\"status\\u0000ignored\",\"password\":\"write-secret\"}"),
std::string("{\"op\":3,\"password\":[]}")}) {
const auto json = result(m, input, sequence++); assert(json["error"].is<const char*>() && floors(m) == 0);
}
error(m, std::string(4097, ' '), sequence++, "invalid request");
for (const std::string text : {"A===", "====", "Zg=A", "Zg==Zg==", "Zh==", "Zm9=", "*m9v", "Zg", "Zg== "}) {
error(m, request("board.save", "admin-secret", ",\"boot\":71,\"id\":1,\"version\":0,\"title\":\"T\",\"body64\":\"" + text + "\""), sequence++, "encoding");
}
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"before\\u0000after\""), sequence++, "name and message");
error(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\\u0000B\",\"text\":\"body\""), sequence++, "name and message");
error(m, request("board.save", "admin-secret", ",\"boot\":71,\"id\":1,\"version\":0,\"title\":\"T\\u0000X\",\"body64\":\"Zg==\""), sequence++, "article changed");
error(m, request("board.save", "admin-secret", ",\"boot\":71,\"id\":1,\"version\":0,\"title\":\"T\",\"body64\":\"Zg==\\u0000Zg==\""), sequence++, "article changed");
assert(m._num_posted == 0 && m.last_room_timestamp == 0);
error(m, saveRequest(1, 0, std::string(64, 't'), "body"), sequence++, "article changed");
error(m, saveRequest(1, 0, "Title", std::string(2049, 'b')), sequence++, "encoding");
assert(!m.storage.files.count(mesh::ROOM_BOARD_PRIMARY_PATH));
deny_json_allocation = true; error(m, request("status"), sequence++, "invalid JSON"); deny_json_allocation = false;
result(m, request("status"), sequence++);
// Excess unknown array members exhaust the actual bounded allocator.
std::string huge = "{\"op\":\"status\",\"password\":\"write-secret\",\"extra\":[";
for (unsigned i = 0; i < 1000; ++i) { if (i) huge += ','; huge += '0'; } huge += "]}";
error(m, huge, sequence++, "invalid JSON"); assert(json_allocations == 0); ++scenarios;
}
{
MyMesh m; enable(m);
for (uint8_t owner = 1; owner <= 8; ++owner) result(m, request("status"), 1, owner);
error(m, request("status"), 1, "busy", 9); ticks += 1200000;
result(m, request("status"), 1, 9);
// Every small response capacity stays within its provided buffer.
for (size_t capacity = 0; capacity < 200; ++capacity) execute(m, request("status"), uint32_t(capacity + 2), 9, capacity);
for (unsigned i = 0; i < 40; ++i) assert(result(m, request("post", "write-secret", ",\"boot\":71,\"name\":\"A\",\"text\":\"bounded ring\""), 300 + i, 9)["ok"] == true);
assert(m._num_posted == 40 && m.next_post_idx == 8 && m.last_room_timestamp == 1039); ++scenarios;
}
adminAuthorizationCases();
adminRouteCases();
adminCatchUpCases();
adminListingCases();
adminAccessAndSettingsCases();
adminInvalidAndSessionCases();
mailAuthorizationAndPolicyCases();
mailDeliveryAndDurabilityCases();
mailAdministratorCases();
mailPagingAndStorageCases();
mailGlobalQueueCases();
// Deliver concrete backend responses for the browser boundary fixture.
MyMesh boundary; enable(boundary);
printf("BOUNDARY_STATUS %s\n", execute(boundary, request("status"), 1).c_str());
assert(result(boundary, saveRequest(1, 0, "Instructions", "Read only this article"), 2)["ok"] == true);
printf("BOUNDARY_INDEX %s\n", execute(boundary, request("board.index", "write-secret", ",\"revision\":0,\"cursor\":0"), 3).c_str());
printf("BOUNDARY_READ %s\n", execute(boundary, request("board.read", "write-secret", ",\"id\":1,\"version\":1,\"offset\":0"), 4).c_str());
printf("PASS: %u base, 6 admin and 6 mail actual room web service authorization, storage, quota, JSON and boundary groups\n", scenarios);
}