Files
tactility/Tactility/Source/settings/WebServerSettings.cpp
T
Ken Van Hoeylandt ecad2248d9 Various improvements (#547)
- Fixed crash when returning to Setup app (from any of the steps)
- Fixed crash when returning to TimeDateSettings app (after locale selection)
- Fixed GuiService inconsistent behaviour: now always perform operations async (e.g. to show/hide apps). This fixes crashes in some onHide() calls where onHide() might be called before onShow() was called.
- Fix for incorrect WebServService path
- Remove CYD-4848S040C SD card functionality, but added a GPIO fix for releasing 3-wire SPI pin sharing.
- Fix for saving/loading settings for various apps
2026-07-04 21:26:43 +02:00

274 lines
10 KiB
C++

#include <Tactility/settings/WebServerSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/file/File.h>
#include <Tactility/Logger.h>
#include <Tactility/Paths.h>
#include <charconv>
#include <map>
#include <string>
#ifdef ESP_PLATFORM
#include <esp_mac.h>
#include <esp_wifi.h>
#include <esp_random.h>
#else
#include <random>
#endif
namespace tt::settings::webserver {
static const auto LOGGER = Logger("WebServerSettings");
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/webserver.properties";
}
// Property keys
constexpr auto* KEY_WIFI_ENABLED = "wifiEnabled";
constexpr auto* KEY_WIFI_MODE = "wifiMode";
constexpr auto* KEY_AP_SSID = "apSsid";
constexpr auto* KEY_AP_PASSWORD = "apPassword";
constexpr auto* KEY_AP_OPEN_NETWORK = "apOpenNetwork";
constexpr auto* KEY_AP_CHANNEL = "apChannel";
constexpr auto* KEY_WEBSERVER_ENABLED = "webServerEnabled";
constexpr auto* KEY_WEBSERVER_PORT = "webServerPort";
constexpr auto* KEY_WEBSERVER_AUTH_ENABLED = "webServerAuthEnabled";
constexpr auto* KEY_WEBSERVER_USERNAME = "webServerUsername";
constexpr auto* KEY_WEBSERVER_PASSWORD = "webServerPassword";
std::string generateDefaultApSsid() {
#ifdef ESP_PLATFORM
uint8_t mac[6];
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
return "Tactility-0000";
}
char ssid[16];
snprintf(ssid, sizeof(ssid), "Tactility-%02X%02X", mac[2] ^ mac[3], mac[4] ^ mac[5]);
return std::string(ssid);
#else
return "Tactility-0000";
#endif
}
/**
* @brief Generate a cryptographically secure random string for credentials
* @param length The desired length of the string
* @return A random alphanumeric string
*/
std::string generateRandomCredential(size_t length) {
static constexpr char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static constexpr size_t charsetSize = sizeof(charset) - 1;
std::string result;
result.reserve(length);
#ifdef ESP_PLATFORM
for (size_t i = 0; i < length; ++i) {
uint32_t randomValue = esp_random();
result += charset[randomValue % charsetSize];
}
#else
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, charsetSize - 1);
for (size_t i = 0; i < length; ++i) {
result += charset[dis(gen)];
}
#endif
return result;
}
/**
* @brief Check if a credential value is empty (needs auto-generation)
* @param value The credential value to check
* @return true if the credential is empty and needs generation
*/
static bool isEmptyCredential(const std::string& value) {
return value.empty();
}
bool load(WebServerSettings& settings) {
auto settings_path = getSettingsFilePath();
if (!file::isFile(settings_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
// Parse all settings from the map
auto wifi_enabled = map.find(KEY_WIFI_ENABLED);
auto wifi_mode = map.find(KEY_WIFI_MODE);
auto ap_ssid = map.find(KEY_AP_SSID);
auto ap_password = map.find(KEY_AP_PASSWORD);
auto ap_open_network = map.find(KEY_AP_OPEN_NETWORK);
auto ap_channel = map.find(KEY_AP_CHANNEL);
auto webserver_enabled = map.find(KEY_WEBSERVER_ENABLED);
auto webserver_port = map.find(KEY_WEBSERVER_PORT);
auto webserver_auth_enabled = map.find(KEY_WEBSERVER_AUTH_ENABLED);
auto webserver_username = map.find(KEY_WEBSERVER_USERNAME);
auto webserver_password = map.find(KEY_WEBSERVER_PASSWORD);
// WiFi settings
settings.wifiEnabled = (wifi_enabled != map.end())
? (wifi_enabled->second == "1" || wifi_enabled->second == "true")
: false; // Default disabled
settings.wifiMode = (wifi_mode != map.end() && wifi_mode->second == "1")
? WiFiMode::AccessPoint
: WiFiMode::Station;
auto parseInt = [](const std::string& value, int min, int max, int fallback) -> int {
int v = 0;
auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), v);
if (ec != std::errc{} || v < min || v > max) {
return fallback;
}
return v;
};
// AP mode settings
settings.apSsid = (ap_ssid != map.end() && !ap_ssid->second.empty())
? ap_ssid->second
: generateDefaultApSsid();
settings.apPassword = (ap_password != map.end()) ? ap_password->second : "";
settings.apOpenNetwork = (ap_open_network != map.end())
? (ap_open_network->second == "1" || ap_open_network->second == "true")
: false;
settings.apChannel = (ap_channel != map.end())
? static_cast<uint8_t>(parseInt(ap_channel->second, 1, 13, 1))
: 1;
// Security: If AP password is empty, generate a strong random password.
// Skip this if user explicitly wants an open network.
// Note: We only auto-generate for EMPTY passwords, not user-set ones.
if (!settings.apOpenNetwork && isEmptyCredential(settings.apPassword)) {
LOGGER.info("AP password is empty - generating secure random password");
// Generate 12-character random password (alphanumeric, ~71 bits of entropy)
// WPA2 requires 8-63 characters, so 12 is well within range
settings.apPassword = generateRandomCredential(12);
// Persist the generated password immediately
map[KEY_AP_PASSWORD] = settings.apPassword;
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOGGER.info("Generated and saved new secure AP password");
} else {
LOGGER.error("Failed to save generated AP password");
}
}
// Web server settings
settings.webServerEnabled = (webserver_enabled != map.end())
? (webserver_enabled->second == "1" || webserver_enabled->second == "true")
: false;
settings.webServerPort = (webserver_port != map.end())
? static_cast<uint16_t>(parseInt(webserver_port->second, 1, 65535, 80))
: 80;
// Web server auth settings
settings.webServerAuthEnabled = (webserver_auth_enabled != map.end())
? (webserver_auth_enabled->second == "1" || webserver_auth_enabled->second == "true")
: false;
// Load credentials from file, defaulting to empty if not present
settings.webServerUsername = (webserver_username != map.end()) ? webserver_username->second : "";
settings.webServerPassword = (webserver_password != map.end()) ? webserver_password->second : "";
// Security: If auth is enabled but credentials are empty,
// generate strong random credentials and persist them immediately.
// Note: We only auto-generate for EMPTY credentials, allowing users to set their own.
if (settings.webServerAuthEnabled &&
(isEmptyCredential(settings.webServerUsername) || isEmptyCredential(settings.webServerPassword))) {
LOGGER.info("Auth enabled with empty credentials - generating secure random credentials");
// Generate 12-character random credentials (alphanumeric, ~71 bits of entropy each)
settings.webServerUsername = generateRandomCredential(12);
settings.webServerPassword = generateRandomCredential(12);
// Persist the generated credentials immediately
// We need to save these to the file so they're consistent across reboots
map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername;
map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword;
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOGGER.info("Generated and saved new secure credentials");
} else {
LOGGER.error("Failed to save generated credentials - auth may be inconsistent across reboots");
}
}
return true;
}
WebServerSettings getDefault() {
return WebServerSettings{
.wifiEnabled = false, // Default WiFi OFF
.wifiMode = WiFiMode::Station,
.apSsid = generateDefaultApSsid(),
.apPassword = "", // Empty - will be auto-generated on first use (unless apOpenNetwork)
.apOpenNetwork = false, // Default to secured network
.apChannel = 1,
.webServerEnabled = false, // Default WebServer OFF for security
.webServerPort = 80,
.webServerAuthEnabled = false, // Auth disabled by default
.webServerUsername = "", // Empty - will be generated if auth is enabled
.webServerPassword = "" // Empty - will be generated if auth is enabled
};
}
WebServerSettings loadOrGetDefault() {
WebServerSettings settings;
bool loadedFromFlash = load(settings);
if (!loadedFromFlash) {
// First boot - use defaults (WiFi OFF, WebServer OFF)
settings = getDefault();
// Save defaults to flash so toggle states persist
if (save(settings)) {
LOGGER.info("First boot - saved default settings (WiFi OFF WebServer OFF)");
} else {
LOGGER.warn("First boot - failed to save default settings to flash");
}
}
return settings;
}
bool save(const WebServerSettings& settings) {
std::map<std::string, std::string> map;
// WiFi settings
map[KEY_WIFI_ENABLED] = settings.wifiEnabled ? "1" : "0";
map[KEY_WIFI_MODE] = (settings.wifiMode == WiFiMode::AccessPoint) ? "1" : "0";
// AP mode settings
map[KEY_AP_SSID] = settings.apSsid;
map[KEY_AP_PASSWORD] = settings.apPassword;
map[KEY_AP_OPEN_NETWORK] = settings.apOpenNetwork ? "1" : "0";
map[KEY_AP_CHANNEL] = std::to_string(settings.apChannel);
// Web server settings
map[KEY_WEBSERVER_ENABLED] = settings.webServerEnabled ? "1" : "0";
map[KEY_WEBSERVER_PORT] = std::to_string(settings.webServerPort);
// Web server auth settings
map[KEY_WEBSERVER_AUTH_ENABLED] = settings.webServerAuthEnabled ? "1" : "0";
map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername;
map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword;
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
LOGGER.error("Failed to create parent dir for {}", settings_path);
return false;
}
return file::savePropertiesFile(settings_path, map);
}
}