Split heap gate into separate free-memory (50KB) and largest-block (20KB) thresholds based on field measurements. Enable wolfSSL single-precision ECC to use fixed 256-bit arrays instead of heap-allocated fast-math bignums, reducing TLS handshake memory footprint. Reclaim ~7KB by right-sizing ESP timer task stacks and move WiFi code out of IRAM to free ~25-30KB for heap. Add memory audit landmarks for font and EPUB allocations.
223 lines
7.6 KiB
C++
223 lines
7.6 KiB
C++
#include "KOReaderSyncClient.h"
|
|
|
|
#include <ArduinoJson.h>
|
|
#include <Logging.h>
|
|
#include <SecureHttpClient.h>
|
|
#include <base64.h>
|
|
|
|
#include <string>
|
|
|
|
#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.
|
|
// MEMFIX-PORT: TLS heap gate; portable
|
|
// Field data (July 2026): launching sync from a reader session lands at
|
|
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
|
|
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
|
|
// so an optimistic attempt degrades to the same clean "sync failed" as the
|
|
// gate — the gate only needs to keep out states where a doomed handshake
|
|
// would waste tens of seconds, not guarantee success.
|
|
//
|
|
// Free and largest-block have separate requirements: with SP ECC
|
|
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
|
|
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
|
|
// a run of fast-math bignums. A handshake was measured succeeding inside a
|
|
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
|
|
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
|
|
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
|
|
|
|
// 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_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
|
|
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
|
|
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_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<std::string>();
|
|
outProgress.percentage = doc["percentage"].as<float>();
|
|
outProgress.device = doc["device"].as<std::string>();
|
|
outProgress.deviceId = doc["device_id"].as<std::string>();
|
|
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
|
|
|
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;
|
|
if (progress.metadata.has_value()) {
|
|
auto meta = doc["metadata"].to<JsonObject>();
|
|
meta["filename"] = progress.metadata->filename;
|
|
meta["title"] = progress.metadata->title;
|
|
meta["authors"] = progress.metadata->authors;
|
|
}
|
|
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";
|
|
}
|
|
}
|