Preserve admin password input and clarify reconnect guidance
This commit is contained in:
1 parent
ad4124b055
commit
5d741318f5
15 files changed
+2858
-2334
No files matched your search
+166
@@ -0,0 +1,166 @@
|
||||
// Real JSON CLI parser, form command builder, drain dispatch and both real
|
||||
// role password setters. Only auth entry and persistence/peripherals are fake.
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <ArduinoJson.h>
|
||||
#define MESH_MANAGEMENT_PASSWORD_HOST_TEST
|
||||
#define ESP32_PLATFORM
|
||||
#define ESP_PLATFORM
|
||||
#define WIFI_SSID ""
|
||||
#include <helpers/esp32/ManagementPassword.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/WebConfigBatch.h>
|
||||
#include <helpers/WebConfigKeys.h>
|
||||
namespace password = mesh::esp32::managementPassword;
|
||||
using String = std::string;
|
||||
using WCLock = std::lock_guard<std::mutex>;
|
||||
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<typename... T> 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<WebConfigBatch::State>(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"));
|
||||
}
|
||||
Reference in new issue
Block a user