#include "OpdsServerStore.h" #include #include #include #include #include #include #include #include #include "CrossPointSettings.h" OpdsServerStore OpdsServerStore::instance; namespace { constexpr char OPDS_FILE_JSON[] = "/.crosspoint/opds.json"; constexpr size_t HW_KEY_LEN = 6; const uint8_t* getHwKey() { static uint8_t key[HW_KEY_LEN] = {}; static bool initialized = false; if (!initialized) { esp_efuse_mac_get_default(key); initialized = true; } return key; } void xorTransform(std::string& data) { const uint8_t* key = getHwKey(); for (size_t i = 0; i < data.size(); i++) { data[i] ^= key[i % HW_KEY_LEN]; } } String obfuscateToBase64(const std::string& plaintext) { if (plaintext.empty()) return ""; std::string temp = plaintext; xorTransform(temp); return base64::encode(reinterpret_cast(temp.data()), temp.size()); } std::string deobfuscateFromBase64(const char* encoded, bool* ok) { if (encoded == nullptr || encoded[0] == '\0') { if (ok) *ok = false; return ""; } if (ok) *ok = true; size_t encodedLen = strlen(encoded); size_t decodedLen = 0; int ret = mbedtls_base64_decode(nullptr, 0, &decodedLen, reinterpret_cast(encoded), encodedLen); if (ret != 0 && ret != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) { LOG_ERR("OPS", "Base64 decode size query failed (ret=%d)", ret); if (ok) *ok = false; return ""; } std::string result(decodedLen, '\0'); ret = mbedtls_base64_decode(reinterpret_cast(&result[0]), decodedLen, &decodedLen, reinterpret_cast(encoded), encodedLen); if (ret != 0) { LOG_ERR("OPS", "Base64 decode failed (ret=%d)", ret); if (ok) *ok = false; return ""; } result.resize(decodedLen); xorTransform(result); return result; } } // namespace bool OpdsServerStore::saveToFile() const { Storage.mkdir("/.crosspoint"); JsonDocument doc; JsonArray arr = doc["servers"].to(); for (const auto& server : servers) { JsonObject obj = arr.add(); obj["name"] = server.name; obj["url"] = server.url; obj["username"] = server.username; obj["password_obf"] = obfuscateToBase64(server.password); obj["download_path"] = server.downloadPath; obj["sort_order"] = server.sortOrder; obj["after_download"] = server.afterDownloadAction; } String json; serializeJson(doc, json); return Storage.writeFile(OPDS_FILE_JSON, json); } bool OpdsServerStore::loadFromFile() { if (Storage.exists(OPDS_FILE_JSON)) { String json = Storage.readFile(OPDS_FILE_JSON); if (!json.isEmpty()) { JsonDocument doc; auto error = deserializeJson(doc, json.c_str()); if (error) { LOG_ERR("OPS", "JSON parse error: %s", error.c_str()); return false; } servers.clear(); bool needsResave = false; JsonArray arr = doc["servers"].as(); for (JsonObject obj : arr) { if (servers.size() >= MAX_SERVERS) break; OpdsServer server; server.name = obj["name"] | std::string(""); server.url = obj["url"] | std::string(""); server.username = obj["username"] | std::string(""); bool ok = false; server.password = deobfuscateFromBase64(obj["password_obf"] | "", &ok); if (!ok || server.password.empty()) { server.password = obj["password"] | std::string(""); if (!server.password.empty()) needsResave = true; } server.downloadPath = obj["download_path"] | std::string("/"); server.sortOrder = obj["sort_order"] | 0; server.afterDownloadAction = obj["after_download"] | 0; if (server.sortOrder == 0) needsResave = true; servers.push_back(std::move(server)); } // Assign sequential sort orders to servers loaded without one bool anyZero = false; for (const auto& s : servers) { if (s.sortOrder == 0) { anyZero = true; break; } } if (anyZero) { for (size_t i = 0; i < servers.size(); i++) { if (servers[i].sortOrder == 0) { servers[i].sortOrder = static_cast(i) + 1; } } } sortServers(); LOG_DBG("OPS", "Loaded %zu OPDS servers from file", servers.size()); if (needsResave) { LOG_DBG("OPS", "Resaving JSON with sort_order / obfuscated passwords"); saveToFile(); } return true; } } // No opds.json found — attempt one-time migration from the legacy single-server // fields in CrossPointSettings (opdsServerUrl/opdsUsername/opdsPassword). if (migrateFromSettings()) { LOG_DBG("OPS", "Migrated legacy OPDS settings"); return true; } return false; } bool OpdsServerStore::migrateFromSettings() { if (strlen(SETTINGS.opdsServerUrl) == 0) { return false; } OpdsServer server; server.name = "OPDS Server"; server.url = SETTINGS.opdsServerUrl; server.username = SETTINGS.opdsUsername; server.password = SETTINGS.opdsPassword; server.sortOrder = 1; servers.push_back(std::move(server)); if (saveToFile()) { SETTINGS.opdsServerUrl[0] = '\0'; SETTINGS.opdsUsername[0] = '\0'; SETTINGS.opdsPassword[0] = '\0'; SETTINGS.saveToFile(); LOG_DBG("OPS", "Migrated single-server OPDS config to opds.json"); return true; } servers.clear(); return false; } bool OpdsServerStore::addServer(const OpdsServer& server) { if (servers.size() >= MAX_SERVERS) { LOG_DBG("OPS", "Cannot add more servers, limit of %zu reached", MAX_SERVERS); return false; } servers.push_back(server); if (servers.back().sortOrder == 0) { int maxOrder = 0; for (size_t i = 0; i + 1 < servers.size(); i++) { maxOrder = std::max(maxOrder, servers[i].sortOrder); } servers.back().sortOrder = maxOrder + 1; } LOG_DBG("OPS", "Added server: %s (order=%d)", server.name.c_str(), servers.back().sortOrder); sortServers(); return saveToFile(); } bool OpdsServerStore::updateServer(size_t index, const OpdsServer& server) { if (index >= servers.size()) { return false; } servers[index] = server; sortServers(); LOG_DBG("OPS", "Updated server: %s (order=%d)", server.name.c_str(), server.sortOrder); return saveToFile(); } bool OpdsServerStore::removeServer(size_t index) { if (index >= servers.size()) { return false; } LOG_DBG("OPS", "Removed server: %s", servers[index].name.c_str()); servers.erase(servers.begin() + static_cast(index)); return saveToFile(); } const OpdsServer* OpdsServerStore::getServer(size_t index) const { if (index >= servers.size()) { return nullptr; } return &servers[index]; } bool OpdsServerStore::moveServer(size_t index, int direction) { if (index >= servers.size()) return false; size_t target; if (direction < 0) { if (index == 0) return false; target = index - 1; } else { if (index + 1 >= servers.size()) return false; target = index + 1; } std::swap(servers[index].sortOrder, servers[target].sortOrder); sortServers(); return saveToFile(); } void OpdsServerStore::sortServers() { std::sort(servers.begin(), servers.end(), [](const OpdsServer& a, const OpdsServer& b) { if (a.sortOrder != b.sortOrder) return a.sortOrder < b.sortOrder; const auto& nameA = a.name.empty() ? a.url : a.name; const auto& nameB = b.name.empty() ? b.url : b.name; return std::lexicographical_compare(nameA.begin(), nameA.end(), nameB.begin(), nameB.end(), [](char ca, char cb) { return tolower(ca) < tolower(cb); }); }); }