diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index e0c81f1d..2a9d9a31 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -42,6 +42,21 @@ void beginRequest(const char* operation) { KOReaderSyncClient::lastContigHeapAtFailure = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); } +// Skip a leading UTF-8 BOM (EF BB BF) and ASCII whitespace, returning a pointer +// to the first content character. Used by response-body checks that verify the +// payload starts with '{' (JSON) rather than '<' (HTML captive-portal page). +const char* skipBomAndWhitespace(const char* p) { + // UTF-8 BOM + if (static_cast(p[0]) == 0xEF && static_cast(p[1]) == 0xBB && + static_cast(p[2]) == 0xBF) { + p += 3; + } + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { + p++; + } + return p; +} + // Device identifier for CrossPoint reader constexpr char DEVICE_NAME[] = "CrossPoint"; constexpr char DEVICE_ID[] = "crosspoint-reader"; @@ -377,19 +392,8 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { if (err != ESP_OK) return NETWORK_ERROR; if (httpCode >= 300 && httpCode < 400) return REDIRECT_ERROR; if (httpCode == 200) { - // The kosync auth response is always a JSON object. Guard against a reverse - // proxy returning HTTP 200 + HTML (e.g. a login wall that followed all - // redirects and landed on an auth page instead of the API endpoint). - // Skip leading whitespace before checking for '{' so servers that emit - // a BOM or indent their JSON don't get incorrectly rejected. - if (!activeBuf->data) { - return INVALID_RESPONSE; - } - const char* p = activeBuf->data; - while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { - p++; - } - if (*p != '{') { + // Guard against a reverse proxy or captive portal returning HTTP 200 + HTML. + if (!activeBuf->data || *skipBomAndWhitespace(activeBuf->data) != '{') { return INVALID_RESPONSE; } return OK; @@ -577,14 +581,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr if (err != ESP_OK) return NETWORK_ERROR; if (httpCode >= 300 && httpCode < 400) return REDIRECT_ERROR; if (httpCode == 200 || httpCode == 202) { - // Guard against a reverse proxy or captive portal returning HTTP 200 + HTML - // instead of the real API response, consistent with authenticate()'s check. + // Guard against a reverse proxy or captive portal returning HTTP 200 + HTML. if (activeBuf->data) { - const char* p = activeBuf->data; - while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { - p++; - } - if (*p != '\0' && *p != '{') { + const char c = *skipBomAndWhitespace(activeBuf->data); + if (c != '\0' && c != '{') { return INVALID_RESPONSE; } } @@ -609,13 +609,13 @@ const char* KOReaderSyncClient::lastFailureDetail() { } // Network/TLS case: esp_http_client_perform() failed before getting a status code. if (lastHttpCode == 0 && lastEspError != 0) { - // Detect TLS handshake failures on HTTPS URLs — likely caused by the server - // requiring TLS 1.3 which the ESP32 mbedTLS library does not support. + // HTTPS connect failures can be TLS version issues (ESP32 only supports up + // to TLS 1.2), but also DNS, cert, or plain network problems. Include the + // error name and heap stats so the user/bug-report has enough to triage. if (lastEspError == ESP_ERR_HTTP_CONNECT && KOREADER_STORE.getBaseUrl().rfind("https", 0) == 0) { - snprintf( - g_failureDetailBuf, sizeof(g_failureDetailBuf), - "%s: TLS handshake failed. Server may require TLS 1.3 (unsupported). Try a different server or use HTTP.", - lastOperation); + snprintf(g_failureDetailBuf, sizeof(g_failureDetailBuf), + "%s: connect failed — check network, DNS, certs, or TLS 1.2 compat (heap %u/%u contig)", lastOperation, + lastHeapAtFailure, lastContigHeapAtFailure); } else { snprintf(g_failureDetailBuf, sizeof(g_failureDetailBuf), "%s: %s (heap %u/%u contig)", lastOperation, esp_err_to_name(lastEspError), lastHeapAtFailure, lastContigHeapAtFailure);