#include "KOReaderSyncClient.h" #include #include #include #include #include #include "KOReaderCredentialStore.h" int KOReaderSyncClient::lastHttpCode = 0; namespace { // Device identifier for CrossPoint reader constexpr char DEVICE_NAME[] = "CrossPoint"; constexpr char DEVICE_ID[] = "crosspoint-reader"; // KOSync's TLS-1.3 servers can't be reached through the precompiled system // mbedTLS (TLS 1.3 is stubbed out), so requests run over wolfSSL via // SecureHttpClient. The handshake still needs working heap; gate on it. wolfSSL's // footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative // floor. Check both total free heap and largest contiguous block so fragmented // heap does not fall through into a failed TLS allocation path. constexpr uint32_t MIN_HEAP_FOR_TLS = 55000; // Apply the shared KOSync auth headers after begin(). x-auth-* is the native // KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility. void applyAuthHeaders(freeink::SecureHttpClient& http) { http.addHeader("Accept", "application/vnd.koreader.v1+json"); http.addHeader("x-auth-user", KOREADER_STORE.getUsername()); http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password()); const std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword(); const String encoded = base64::encode(credentials.c_str()); http.addHeader("Authorization", std::string("Basic ") + encoded.c_str()); } // True when free heap is too low to risk a TLS handshake. bool insufficientHeap() { const uint32_t freeHeap = ESP.getFreeHeap(); const uint32_t maxAllocHeap = ESP.getMaxAllocHeap(); if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) { LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap, maxAllocHeap, MIN_HEAP_FOR_TLS); return true; } return false; } } // namespace KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { lastHttpCode = 0; if (!KOREADER_STORE.hasCredentials()) { LOG_DBG("KOSync", "No credentials configured"); return NO_CREDENTIALS; } const std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); if (insufficientHeap()) return LOW_MEMORY; freeink::SecureHttpClient http; http.setInsecure(); if (!http.begin(url)) { LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } applyAuthHeaders(http); const int httpCode = http.GET(); http.end(); lastHttpCode = httpCode; LOG_DBG("KOSync", "Auth response: %d", httpCode); if (httpCode <= 0) return NETWORK_ERROR; if (httpCode == 200) return OK; if (httpCode == 401) return AUTH_FAILED; return SERVER_ERROR; } KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash, KOReaderProgress& outProgress) { lastHttpCode = 0; if (!KOREADER_STORE.hasCredentials()) { LOG_DBG("KOSync", "No credentials configured"); return NO_CREDENTIALS; } const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); if (insufficientHeap()) return LOW_MEMORY; freeink::SecureHttpClient http; http.setInsecure(); if (!http.begin(url)) { LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } applyAuthHeaders(http); const int httpCode = http.GET(); lastHttpCode = httpCode; LOG_DBG("KOSync", "Get progress response: %d", httpCode); if (httpCode <= 0) { http.end(); return NETWORK_ERROR; } if (httpCode == 200) { JsonDocument doc; const DeserializationError error = deserializeJson(doc, http.getString().c_str()); http.end(); if (error) { LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str()); return JSON_ERROR; } outProgress.document = documentHash; outProgress.progress = doc["progress"].as(); outProgress.percentage = doc["percentage"].as(); outProgress.device = doc["device"].as(); outProgress.deviceId = doc["device_id"].as(); outProgress.timestamp = doc["timestamp"].as(); LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str()); return OK; } http.end(); if (httpCode == 401) return AUTH_FAILED; if (httpCode == 404) return NOT_FOUND; return SERVER_ERROR; } KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) { lastHttpCode = 0; if (!KOREADER_STORE.hasCredentials()) { LOG_DBG("KOSync", "No credentials configured"); return NO_CREDENTIALS; } const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); if (insufficientHeap()) return LOW_MEMORY; // Build JSON body JsonDocument doc; doc["document"] = progress.document; doc["progress"] = progress.progress; doc["percentage"] = progress.percentage; doc["device"] = DEVICE_NAME; doc["device_id"] = DEVICE_ID; std::string body; serializeJson(doc, body); LOG_DBG("KOSync", "Request body: %s", body.c_str()); freeink::SecureHttpClient http; http.setInsecure(); if (!http.begin(url)) { LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } applyAuthHeaders(http); http.addHeader("Content-Type", "application/json"); const int httpCode = http.sendRequest("PUT", body); http.end(); lastHttpCode = httpCode; LOG_DBG("KOSync", "Update progress response: %d", httpCode); if (httpCode <= 0) return NETWORK_ERROR; if (httpCode == 200 || httpCode == 202) return OK; if (httpCode == 401) return AUTH_FAILED; return SERVER_ERROR; } const char* KOReaderSyncClient::errorString(Error error) { switch (error) { case OK: return "Success"; case NO_CREDENTIALS: return "No credentials configured"; case NETWORK_ERROR: return "Network error"; case AUTH_FAILED: return "Authentication failed"; case SERVER_ERROR: return "Server error (try again later)"; case JSON_ERROR: return "JSON parse error"; case NOT_FOUND: return "No progress found"; case LOW_MEMORY: return "Not enough memory for sync — please retry"; default: return "Unknown error"; } }