diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index b9cbc564..6eb37ac8 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -15,6 +15,9 @@ constexpr uint8_t KOREADER_FILE_VERSION = 1; // KOReader credentials file path constexpr char KOREADER_FILE[] = "/.crosspoint/koreader.bin"; +// Default sync server URL +constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443"; + // Obfuscation key - "KOReader" in ASCII // This is NOT cryptographic security, just prevents casual file reading constexpr uint8_t OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72}; @@ -48,6 +51,9 @@ bool KOReaderCredentialStore::saveToFile() const { obfuscate(obfuscatedPwd); serialization::writeString(file, obfuscatedPwd); + // Write server URL + serialization::writeString(file, serverUrl); + file.close(); Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis()); return true; @@ -70,11 +76,26 @@ bool KOReaderCredentialStore::loadFromFile() { } // Read username - serialization::readString(file, username); + if (file.available()) { + serialization::readString(file, username); + } else { + username.clear(); + } // Read and deobfuscate password - serialization::readString(file, password); - obfuscate(password); // XOR is symmetric, so same function deobfuscates + if (file.available()) { + serialization::readString(file, password); + obfuscate(password); // XOR is symmetric, so same function deobfuscates + } else { + password.clear(); + } + + // Read server URL + if (file.available()) { + serialization::readString(file, serverUrl); + } else { + serverUrl.clear(); + } file.close(); Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str()); @@ -109,3 +130,21 @@ void KOReaderCredentialStore::clearCredentials() { saveToFile(); Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis()); } + +void KOReaderCredentialStore::setServerUrl(const std::string& url) { + serverUrl = url; + Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str()); +} + +std::string KOReaderCredentialStore::getBaseUrl() const { + if (serverUrl.empty()) { + return DEFAULT_SERVER_URL; + } + + // Normalize URL: add https:// if no protocol specified + if (serverUrl.find("://") == std::string::npos) { + return "https://" + serverUrl; + } + + return serverUrl; +} diff --git a/lib/KOReaderSync/KOReaderCredentialStore.h b/lib/KOReaderSync/KOReaderCredentialStore.h index 0d3332fa..488f23b0 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.h +++ b/lib/KOReaderSync/KOReaderCredentialStore.h @@ -11,6 +11,7 @@ class KOReaderCredentialStore { static KOReaderCredentialStore instance; std::string username; std::string password; + std::string serverUrl; // Custom sync server URL (empty = default) // Private constructor for singleton KOReaderCredentialStore() = default; @@ -43,6 +44,13 @@ class KOReaderCredentialStore { // Clear credentials void clearCredentials(); + + // Server URL management + void setServerUrl(const std::string& url); + const std::string& getServerUrl() const { return serverUrl; } + + // Get base URL for API calls (with https:// normalization, falls back to default) + std::string getBaseUrl() const; }; // Helper macro to access credential store diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index 8fa7fc34..91515cd9 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -10,9 +10,6 @@ #include "KOReaderCredentialStore.h" namespace { -// Base URL for KOReader sync server -constexpr char BASE_URL[] = "https://sync.koreader.rocks:443"; - // Device identifier for CrossPoint reader constexpr char DEVICE_NAME[] = "CrossPoint"; constexpr char DEVICE_ID[] = "crosspoint-reader"; @@ -34,7 +31,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { client->setInsecure(); HTTPClient http; - std::string url = std::string(BASE_URL) + "/users/auth"; + std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str()); http.begin(*client, url.c_str()); @@ -66,7 +63,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc client->setInsecure(); HTTPClient http; - std::string url = std::string(BASE_URL) + "/syncs/progress/" + documentHash; + std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str()); http.begin(*client, url.c_str()); @@ -121,7 +118,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr client->setInsecure(); HTTPClient http; - std::string url = std::string(BASE_URL) + "/syncs/progress"; + std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str()); http.begin(*client, url.c_str()); diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 9a6c0f98..4b16e966 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -11,8 +11,8 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 3; -const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Authenticate"}; +constexpr int MENU_ITEMS = 4; +const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Authenticate"}; } // namespace void KOReaderSettingsActivity::taskTrampoline(void* param) { @@ -112,6 +112,23 @@ void KOReaderSettingsActivity::handleSelection() { updateRequired = true; })); } else if (selectedIndex == 2) { + // Sync Server URL + exitActivity(); + enterNewActivity(new KeyboardEntryActivity( + renderer, mappedInput, "Sync Server URL", KOREADER_STORE.getServerUrl(), 10, + 128, // maxLength - URLs can be long + false, // not password + [this](const std::string& url) { + KOREADER_STORE.setServerUrl(url); + KOREADER_STORE.saveToFile(); + exitActivity(); + updateRequired = true; + }, + [this]() { + exitActivity(); + updateRequired = true; + })); + } else if (selectedIndex == 3) { // Authenticate if (!KOREADER_STORE.hasCredentials()) { // Can't authenticate without credentials - just show message briefly @@ -158,13 +175,15 @@ void KOReaderSettingsActivity::render() { renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); - // Draw status for username/password + // Draw status for each item const char* status = ""; if (i == 0) { status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]"; } else if (i == 1) { status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]"; } else if (i == 2) { + status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]"; + } else if (i == 3) { status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"; }