// Real JSON CLI parser, form command builder, drain dispatch and both real // role password setters. Only auth entry and persistence/peripherals are fake. #include #include #include #include #include #include #define MESH_MANAGEMENT_PASSWORD_HOST_TEST #define ESP32_PLATFORM #define ESP_PLATFORM #define WIFI_SSID "" #include #include #include #include namespace password = mesh::esp32::managementPassword; using String = std::string; using WCLock = std::lock_guard; uint32_t millis() { return 1000; } bool save_ok = true; unsigned saves = 0; bool durableSave() { ++saves; return save_ok; } namespace mesh { namespace esp32 { namespace companionAdminPassword { bool save(const char*) { return durableSave(); } }} namespace wifi { constexpr bool kPrimaryEspNowRadio = false; bool parseEspNowChannel(const char*, uint8_t&) { return false; } } struct Debug { template void printf(const char*, T...) {} }; Debug& usbDebugPort() { static Debug value; return value; } } struct StrHelper { static void strncpy(char* out, const char* in, size_t size) { ::strncpy(out, in, size); out[size - 1] = 0; }}; struct CommonCLI { struct Prefs { char password[16] = "original"; } prefs; Prefs* _prefs = &prefs; bool trySavePrefs() { return durableSave(); } void command(char* command, char* reply) { @SETTER@ } }; struct MyMesh { char _management_password[16] = "original"; bool _management_password_ready = true; bool handleLocalControlCommand(const char*, char*, size_t); void command(char* cmd, char* reply) { handleLocalControlCommand(cmd, reply, 160); } }; @COMPANION@ struct Callback { bool companion = false; unsigned starts = 0, ends = 0, calls = 0; CommonCLI infrastructure; MyMesh node; bool supportsCliTerminal() const { return true; } void execCommand(char* cmd, char* reply) { ++calls; if (companion) node.command(cmd, reply); else infrastructure.command(cmd, reply); } void execAdminCommand(char* cmd, char* reply) { execCommand(cmd, reply); } void onConfigBatchStart() { ++starts; } void onConfigBatchEnd() { ++ends; } }; struct AsyncWebServerRequest { int status = 0; std::string body, reply; void* _tempObject = nullptr; size_t contentLength() const { return body.size(); } void send(int code, const char*, const char* text) { status = code; reply = text; } void send(int code, const char*, const String& text) { status = code; reply = text; } void payload(const std::string& text) { body = text; _tempObject = &body[0]; } }; static const char SECRET_SENTINEL[] = "********"; static bool isAllowedSetKey(const char*, bool); static bool isSecretKey(const char*); struct WebConfigServer { static constexpr unsigned MAX_BATCH = 24, MAX_BODY = 6144; struct BatchEntry { char key[33] = {}, cmd[160] = {}, reply[160] = {}; } _batch[MAX_BATCH]; enum Mode { MODE_OFF, MODE_SETUP, MODE_LAN } _mode = MODE_LAN; enum BatchKind { BATCH_CONFIG, BATCH_CLI } _batch_kind = BATCH_CLI; enum State { BATCH_IDLE, BATCH_PENDING, BATCH_DONE } _batch_state = BATCH_IDLE; std::mutex _mux; Callback cb; Callback* _cb = &cb; bool _cli_enabled = true, _initial_setup = false, _admin_pwd_set = false; bool _batch_reboot = false, _batch_reboot_armed = false, _batch_all_ok = true; bool _batch_started = false, _batch_reauth_required = false, _batch_reauth_delivered = false; bool _standalone_wifi = false, _standalone_wifi_dirty = false; int _batch_count = 0, _batch_next = 0; uint32_t _batch_auth_generation = 0, _batch_reauth_until = 0, _batch_last_cmd = 0; uint32_t _session_generation = 0, _session_last_seen = 1; char _batch_owner_token[33] = {}, _session_token[33] = "0123456789abcdef0123456789abcdef"; char _batch_reqid[24] = {}, _wifi_ssid[32] = {}, _wifi_password[65] = {}; uint8_t _wifi_power_save = 0; void* _mqtt_prefs = nullptr; bool checkAuth(AsyncWebServerRequest*) { return true; } bool checkAuthLocked(AsyncWebServerRequest*, uint32_t* generation) { password::Snapshot value; assert(password::snapshot(value)); *generation = value.generation; return true; } bool bluetoothWiFiCoexistenceRequired() const { return false; } void reloadStandaloneWiFi() {} static WebConfigBatch::State toSpecState(State value) { return static_cast(value); } void clearSessionLocked(); void beginBatchAuthorizationLocked(uint32_t); void cancelStaleBatchLocked(uint32_t); void handleCliPost(AsyncWebServerRequest*); void drainBatch(uint32_t); void configForTest(AsyncWebServerRequest* req) { DynamicJsonDocument doc(6144); assert(!deserializeJson(doc, req->body)); JsonObject set = doc["set"]; int count = 0; @CONFIG_LOOP@ _batch_count = count; _batch_next = 0; _batch_kind = BATCH_CONFIG; password::Snapshot value; assert(password::snapshot(value)); beginBatchAuthorizationLocked(value.generation); _batch_state = BATCH_PENDING; req->send(202, "application/json", "{}"); } }; @METHODS@ void reset() { auto& state = password::detail::state(); { password::detail::Lock lock(state); state.current = {}; state.changing = state.uploading = state.rotating = false; } assert(password::initialize("original")); save_ok = true; saves = 0; } std::string cliPayload(const std::string& command, bool prepend = false) { DynamicJsonDocument doc(6144); doc["reqid"] = "0123456789abcdef"; JsonArray cmds = doc.createNestedArray("cmds"); if (prepend) cmds.add("get name"); cmds.add(command); std::string out; serializeJson(doc, out); return out; } int main() { for (bool companion : {false, true}) { for (bool config : {false, true}) { for (bool success : {false, true}) { reset(); WebConfigServer server; server.cb.companion = companion; save_ok = success; const char* value = " NewSecret "; AsyncWebServerRequest req; if (config) { req.payload(std::string("{\"set\":{\"password\":\"") + value + "\"}}"); server.configForTest(&req); } else { req.payload(cliPayload(std::string(" PaSsWoRd ") + value)); server.handleCliPost(&req); } assert(req.status == 202 && server._batch_count == 1); assert(!strcmp(server._batch[0].cmd, "password NewSecret ")); server.drainBatch(1000); password::Snapshot current; assert(password::snapshot(current)); assert(server.cb.calls == 1 && saves == 1); assert(!strcmp(current.password, success ? value : "original")); const char* runtime = companion ? server.cb.node._management_password : server.cb.infrastructure.prefs.password; assert(!strcmp(runtime, success ? value : "original")); assert(server._batch_reauth_required == success); assert(bool(server._session_token[0]) != success); assert(!strcmp(server._batch[0].reply, "OK") == success); assert(strstr(server._batch[0].reply, "NewSecret") == nullptr); } } } for (const char* command : {"PASSWORD bad\rvalue", "password bad\nvalue", "pass\nword rejected"}) { reset(); WebConfigServer server; AsyncWebServerRequest req; req.payload(cliPayload(command, true)); server.handleCliPost(&req); assert(req.status == 400 && server._batch_state == WebConfigServer::BATCH_IDLE); assert(server.cb.calls == 0 && saves == 0); } reset(); WebConfigServer ordinary; AsyncWebServerRequest req; req.payload(cliPayload(" get name\r\n \t")); ordinary.handleCliPost(&req); assert(req.status == 202 && !strcmp(ordinary._batch[0].cmd, "get name")); }