From 2543d2f48446951c9aab7ab7ea7aae72a7545625 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 26 May 2026 09:15:05 +0200 Subject: [PATCH] remove HTTPClient --- lib_standalone/HTTPClient.h | 24 --- src/ESP32React/MqttSettingsService.cpp | 2 +- src/core/shuntingYard.cpp | 151 +++++++------- src/core/shuntingYard.h | 1 - src/core/system.cpp | 260 ++++++++++++------------- src/web/WebStatusService.cpp | 90 +++++++-- 6 files changed, 273 insertions(+), 255 deletions(-) delete mode 100644 lib_standalone/HTTPClient.h diff --git a/lib_standalone/HTTPClient.h b/lib_standalone/HTTPClient.h deleted file mode 100644 index c52df11c6..000000000 --- a/lib_standalone/HTTPClient.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef HTTPClient_H_ -#define HTTPClient_H_ - -#include "WString.h" - -class HTTPClient { - public: - bool begin(String url) { - return true; - }; - void end(void) {}; - int GET() { - return 200; - }; - int POST(String payload) { - return 200; - }; - void addHeader(const String & name, const String & value, bool first = false, bool replace = true) {}; - String getString(void) { - return "Hello, World!"; - }; -}; - -#endif /* HTTPClient_H_ */ diff --git a/src/ESP32React/MqttSettingsService.cpp b/src/ESP32React/MqttSettingsService.cpp index 550b4343a..244486790 100644 --- a/src/ESP32React/MqttSettingsService.cpp +++ b/src/ESP32React/MqttSettingsService.cpp @@ -166,6 +166,7 @@ bool MqttSettingsService::configureMqtt() { if (_state.rootCA == "insecure") { #if defined(EMSESP_DEBUG) emsesp::EMSESP::logger().debug("Start insecure MQTT"); +#endif static_cast(_mqttClient)->setInsecure(); } else { #if defined(EMSESP_DEBUG) @@ -185,7 +186,6 @@ bool MqttSettingsService::configureMqtt() { static_cast(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain return _mqttClient->connect(); } -#endif static_cast(_mqttClient)->setServer(_state.host.c_str(), _state.port); if (_state.username.length() > 0) { static_cast(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr); diff --git a/src/core/shuntingYard.cpp b/src/core/shuntingYard.cpp index 1db178e4f..4908f91c0 100644 --- a/src/core/shuntingYard.cpp +++ b/src/core/shuntingYard.cpp @@ -687,15 +687,21 @@ std::string calculate(const std::string & expr) { } // perform an HTTP/HTTPS request; returns the HTTP status code (0 on failure or unsupported scheme) -// for HTTPS the response headers are stripped, so `result` always contains only the body +// the response headers are always stripped, so `result` contains only the body +// uses ESP_SSLClient for both schemes (SSL is disabled for plain HTTP), avoiding the HTTPClient dependency int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result) { int httpResult = 0; const bool is_post = value.length() || Helpers::toLower(method) == "post"; const auto lower_url = Helpers::toLower(url.c_str()); - if (lower_url.starts_with("https://")) { - WiFiClient * basic_client = new WiFiClient; - ESP_SSLClient * ssl_client = new ESP_SSLClient; + const bool is_https = lower_url.starts_with("https://"); + if (!is_https && !lower_url.starts_with("http://")) { + return 0; // unsupported scheme + } + + WiFiClient * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = new ESP_SSLClient; + if (is_https) { ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert); // NOTE: 1 KB RX buffer is fine for small JSON-style endpoints used by the scheduler/shunting-yard, // but it is NOT enough for servers that send full-size TLS records (>1 KB), e.g. GitHub release @@ -704,84 +710,67 @@ int http_request(std::string url, const std::string & method, const std::string // payloads, bump the RX buffer to 16384 (see uploadFirmwareURL in core/system.cpp for reference). ssl_client->setBufferSizes(1024, 1024); ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds) - url.replace(0, 8, ""); - std::string host = url; - auto index = url.find_first_of('/'); - if (index != std::string::npos) { - host = url.substr(0, index); - url.replace(0, index, ""); - } - ssl_client->setClient(basic_client); - if (ssl_client->connect(host.c_str(), 443)) { - bool content_set = false; - ssl_client->print(is_post ? "POST " : "GET "); - ssl_client->print(url.c_str()); - ssl_client->println(" HTTP/1.1"); - ssl_client->print("Host: "); - ssl_client->println(host.c_str()); - for (JsonPairConst p : headers) { - content_set |= (Helpers::toLower(p.key().c_str()) == "content-type"); - ssl_client->print(p.key().c_str()); - ssl_client->print(": "); - ssl_client->println(p.value().as().c_str()); - } - if (is_post) { - if (!content_set) { - ssl_client->print("Content-Type: "); - ssl_client->println(value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain); - } - ssl_client->print("Content-Length: "); - ssl_client->println(value.length()); - ssl_client->println("Connection: close"); - ssl_client->print("\r\n"); - ssl_client->print(value.c_str()); - } else { - ssl_client->println("Connection: close"); - } - auto ms = millis(); - while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) { - delay(0); - } - while (ssl_client->available()) { - result += (char)ssl_client->read(); - } - ssl_client->stop(); - index = result.find_first_of(' '); - if (index != std::string::npos) { - httpResult = stoi(result.substr(index + 1, 3)); - } - index = result.find("\r\n\r\n"); - if (index != std::string::npos) { - result.replace(0, index + 4, ""); - } - } else { - EMSESP::logger().warning("HTTPS connection failed"); - } - delete ssl_client; - delete basic_client; - } else if (lower_url.starts_with("http://")) { - HTTPClient * http = new HTTPClient; - if (http->begin(url.c_str())) { - bool content_set = false; - for (JsonPairConst p : headers) { - http->addHeader(p.key().c_str(), p.value().as().c_str()); - content_set |= (Helpers::toLower(p.key().c_str()) == "content-type"); - } - if (is_post) { - if (!content_set) { - http->addHeader(asyncsrv::T_Content_Type, value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain); - } - httpResult = http->POST(value.c_str()); - } else { - httpResult = http->GET(); - } - if (httpResult > 0) { - result = http->getString().c_str(); - } - } - http->end(); - delete http; } + ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP + + url.replace(0, is_https ? 8 : 7, ""); + std::string host = url; + auto index = url.find_first_of('/'); + if (index != std::string::npos) { + host = url.substr(0, index); + url.replace(0, index, ""); + } else { + url = "/"; + } + + const uint16_t port = is_https ? 443 : 80; + if (ssl_client->connect(host.c_str(), port)) { + bool content_set = false; + ssl_client->print(is_post ? "POST " : "GET "); + ssl_client->print(url.c_str()); + ssl_client->println(" HTTP/1.1"); + ssl_client->print("Host: "); + ssl_client->println(host.c_str()); + for (JsonPairConst p : headers) { + content_set |= (Helpers::toLower(p.key().c_str()) == "content-type"); + ssl_client->print(p.key().c_str()); + ssl_client->print(": "); + ssl_client->println(p.value().as().c_str()); + } + if (is_post) { + if (!content_set) { + ssl_client->print("Content-Type: "); + ssl_client->println(value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain); + } + ssl_client->print("Content-Length: "); + ssl_client->println(value.length()); + ssl_client->println("Connection: close"); + ssl_client->print("\r\n"); + ssl_client->print(value.c_str()); + } else { + ssl_client->println("Connection: close"); + } + auto ms = millis(); + while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) { + delay(0); + } + while (ssl_client->available()) { + result += (char)ssl_client->read(); + } + ssl_client->stop(); + index = result.find_first_of(' '); + if (index != std::string::npos) { + httpResult = stoi(result.substr(index + 1, 3)); + } + index = result.find("\r\n\r\n"); + if (index != std::string::npos) { + result.replace(0, index + 4, ""); + } + } else { + EMSESP::logger().warning("%s connection failed", is_https ? "HTTPS" : "HTTP"); + } + delete ssl_client; + delete basic_client; return httpResult; } diff --git a/src/core/shuntingYard.h b/src/core/shuntingYard.h index 712ab2295..23e8486ec 100644 --- a/src/core/shuntingYard.h +++ b/src/core/shuntingYard.h @@ -21,7 +21,6 @@ #ifndef EMSESP_SHUNTING_YARD_H_ #define EMSESP_SHUNTING_YARD_H_ -#include #include #include diff --git a/src/core/system.cpp b/src/core/system.cpp index efa686c25..969cd21fb 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -29,7 +29,6 @@ #include #endif -#include #include #include "firmwareVersion.h" @@ -2957,12 +2956,14 @@ bool System::uploadFirmwareURL(const char * url) { Shell::loop_all(); // flush log buffers so latest messages are shown in console - // detect scheme (case-insensitive) + // detect scheme (case-insensitive). Everything below uses the same code path + // for HTTP and HTTPS - ESP_SSLClient is configured as a plain TCP passthrough + // when SSL is disabled, so we don't need HTTPClient at all. String scheme = saved_url.substring(0, 8); scheme.toLowerCase(); const bool is_https = scheme.startsWith("https://"); + const int scheme_len = is_https ? 8 : 7; // "https://" vs "http://" - HTTPClient http; WiFiClient basic_client; ESP_SSLClient ssl_client; @@ -2981,142 +2982,134 @@ bool System::uploadFirmwareURL(const char * url) { // record the CDN actually sends in practice. ssl_client.setBufferSizes(16384, 1024); ssl_client.setSessionTimeout(120); - basic_client.setTimeout(15000); // socket-level read timeout - ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update - ssl_client.setClient(&basic_client); + } + basic_client.setTimeout(15000); // socket-level read timeout + ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update + ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP - String url_remain = saved_url.substring(8); // strip "https://" - int redirect_count = 0; + const uint16_t port = is_https ? 443 : 80; + String url_remain = saved_url.substring(scheme_len); + int redirect_count = 0; - while (true) { - // split url_remain into host and path - String host; - String path; - int s = url_remain.indexOf('/'); - if (s < 0) { - host = url_remain; - path = "/"; - } else { - host = url_remain.substring(0, s); - path = url_remain.substring(s); - } - - LOG_DEBUG("Connecting to %s", host.c_str()); - if (!ssl_client.connect(host.c_str(), 443)) { - LOG_ERROR("Firmware upload failed - HTTPS connection failed"); - return false; - } - - // send a minimal HTTP/1.0 GET so we don't have to deal with chunked encoding - ssl_client.print("GET "); - ssl_client.print(path); - ssl_client.println(" HTTP/1.0"); - ssl_client.print("Host: "); - ssl_client.println(host); - ssl_client.println("User-Agent: EMS-ESP"); - ssl_client.println("Connection: close"); - ssl_client.print("\r\n"); - - // wait for the first byte (up to 8s, matching the previous HTTP timeout) - uint32_t ms = millis(); - while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 8000) { - delay(1); - } - - // parse status line: "HTTP/1.x CODE TEXT" - String status_line = ssl_client.readStringUntil('\n'); - int sp = status_line.indexOf(' '); - int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0; - - // parse response headers, looking for Content-Length and Location - int content_length = -1; - String location; - while (ssl_client.connected() || ssl_client.available()) { - String line = ssl_client.readStringUntil('\n'); - line.trim(); - if (line.isEmpty()) { - break; // end of headers - } - int colon = line.indexOf(':'); - if (colon < 0) { - continue; - } - String name = line.substring(0, colon); - name.toLowerCase(); - String val = line.substring(colon + 1); - val.trim(); - if (name == "content-length") { - content_length = val.toInt(); - } else if (name == "location") { - location = val; - } - } - - // follow redirects manually (GitHub releases redirect to objects.githubusercontent.com) - if (http_code == 301 || http_code == 302 || http_code == 303 || http_code == 307 || http_code == 308) { - ssl_client.stop(); - if (location.isEmpty() || ++redirect_count > 5) { - LOG_ERROR("Firmware upload failed - too many redirects"); - return false; - } - String lower_loc = location; - lower_loc.toLowerCase(); - if (lower_loc.startsWith("https://")) { - url_remain = location.substring(8); - } else if (location.startsWith("/")) { - url_remain = host + location; // relative redirect, same host - } else { - LOG_ERROR("Firmware upload failed - non-HTTPS redirect to %s", location.c_str()); - return false; - } - LOG_DEBUG("Following redirect to %s", url_remain.c_str()); - continue; - } - - if (http_code != HTTP_CODE_OK) { - ssl_client.stop(); - LOG_ERROR("Firmware upload failed - HTTP code %d", http_code); - return false; - } - - if (content_length <= 0) { - ssl_client.stop(); - LOG_ERROR("Firmware upload failed - missing Content-Length"); - return false; - } - - // wait for the first byte of the body so Update.writeStream's peek() sees real data - // (headers and body may arrive in separate TLS records) - uint32_t body_wait = millis(); - while (ssl_client.connected() && !ssl_client.available() && millis() - body_wait < 8000) { - delay(1); - } - if (!ssl_client.available()) { - ssl_client.stop(); - LOG_ERROR("Firmware upload failed - no body received"); - return false; - } - - stream = &ssl_client; - firmware_size = content_length; - break; + while (true) { + // split url_remain into host and path + String host; + String path; + int s = url_remain.indexOf('/'); + if (s < 0) { + host = url_remain; + path = "/"; + } else { + host = url_remain.substring(0, s); + path = url_remain.substring(s); } - } else { - // HTTP path - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // important for GitHub 302's - http.setTimeout(8000); - http.useHTTP10(true); // use HTTP/1.0 for update since the update handler does not support any transfer Encoding - http.begin(saved_url); - int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) { - LOG_ERROR("Firmware upload failed - HTTP code %d", httpCode); - http.end(); + LOG_DEBUG("Connecting to %s", host.c_str()); + if (!ssl_client.connect(host.c_str(), port)) { + LOG_ERROR("Firmware upload failed - connection failed"); return false; } - firmware_size = http.getSize(); - stream = http.getStreamPtr(); + // send a minimal HTTP/1.0 GET so we don't have to deal with chunked encoding + ssl_client.print("GET "); + ssl_client.print(path); + ssl_client.println(" HTTP/1.0"); + ssl_client.print("Host: "); + ssl_client.println(host); + ssl_client.println("User-Agent: EMS-ESP"); + ssl_client.println("Connection: close"); + ssl_client.print("\r\n"); + + // wait for the first byte (up to 8s, matching the previous HTTP timeout) + uint32_t ms = millis(); + while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 8000) { + delay(1); + } + + // parse status line: "HTTP/1.x CODE TEXT" + String status_line = ssl_client.readStringUntil('\n'); + int sp = status_line.indexOf(' '); + int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0; + + // parse response headers, looking for Content-Length and Location + int content_length = -1; + String location; + while (ssl_client.connected() || ssl_client.available()) { + String line = ssl_client.readStringUntil('\n'); + line.trim(); + if (line.isEmpty()) { + break; // end of headers + } + int colon = line.indexOf(':'); + if (colon < 0) { + continue; + } + String name = line.substring(0, colon); + name.toLowerCase(); + String val = line.substring(colon + 1); + val.trim(); + if (name == "content-length") { + content_length = val.toInt(); + } else if (name == "location") { + location = val; + } + } + + // follow redirects manually (GitHub releases redirect to objects.githubusercontent.com) + if (http_code == 301 || http_code == 302 || http_code == 303 || http_code == 307 || http_code == 308) { + ssl_client.stop(); + if (location.isEmpty() || ++redirect_count > 5) { + LOG_ERROR("Firmware upload failed - too many redirects"); + return false; + } + String lower_loc = location; + lower_loc.toLowerCase(); + if (lower_loc.startsWith("https://") || lower_loc.startsWith("http://")) { + // scheme-changing redirect is not supported - the SSL state is + // baked in at setClient() time and we don't want to re-init mid-flight + const bool new_is_https = lower_loc.startsWith("https://"); + if (new_is_https != is_https) { + LOG_ERROR("Firmware upload failed - cross-scheme redirect to %s", location.c_str()); + return false; + } + url_remain = location.substring(new_is_https ? 8 : 7); + } else if (location.startsWith("/")) { + url_remain = host + location; // relative redirect, same host + } else { + LOG_ERROR("Firmware upload failed - unsupported redirect to %s", location.c_str()); + return false; + } + LOG_DEBUG("Following redirect to %s", url_remain.c_str()); + continue; + } + + if (http_code != 200) { + ssl_client.stop(); + LOG_ERROR("Firmware upload failed - HTTP code %d", http_code); + return false; + } + + if (content_length <= 0) { + ssl_client.stop(); + LOG_ERROR("Firmware upload failed - missing Content-Length"); + return false; + } + + // wait for the first byte of the body so the read loop sees real data + // (headers and body may arrive in separate TLS records) + uint32_t body_wait = millis(); + while (ssl_client.connected() && !ssl_client.available() && millis() - body_wait < 8000) { + delay(1); + } + if (!ssl_client.available()) { + ssl_client.stop(); + LOG_ERROR("Firmware upload failed - no body received"); + return false; + } + + stream = &ssl_client; + firmware_size = content_length; + break; } // check we have a valid size @@ -3150,8 +3143,7 @@ bool System::uploadFirmwareURL(const char * url) { // wait for some data or for the connection to drop uint32_t wait_start = millis(); while (!stream->available()) { - const bool still_connected = is_https ? ssl_client.connected() : http.connected(); - if (!still_connected) { + if (!ssl_client.connected()) { break; } if (millis() - wait_start > READ_TIMEOUT_MS) { diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index 7046931db..fbc8f99e3 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -20,7 +20,8 @@ #ifndef EMSESP_STANDALONE #include -#include +#include +#include #endif namespace emsesp { @@ -414,30 +415,91 @@ bool WebStatusService::refresh_versions_cache() { #ifdef EMSESP_STANDALONE return false; #else - HTTPClient http; - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - http.setTimeout(5000); - http.useHTTP10(true); - - if (!http.begin(EMSESP_VERSIONS_URL)) { + // detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS, + // using ESP_SSLClient as a plain TCP passthrough when SSL is disabled. + String url = EMSESP_VERSIONS_URL; + String lower = url; + lower.toLowerCase(); + const bool is_https = lower.startsWith("https://"); + if (!is_https && !lower.startsWith("http://")) { #if defined(EMSESP_DEBUG) - EMSESP::logger().debug("versions.json: failed to start HTTPS request"); + EMSESP::logger().debug("versions.json: unsupported scheme"); +#endif + return false; + } + const int scheme_len = is_https ? 8 : 7; + + WiFiClient basic_client; + ESP_SSLClient ssl_client; + if (is_https) { + ssl_client.setInsecure(); + ssl_client.setBufferSizes(16384, 1024); // versions.json fits easily but TLS records may be full-size + ssl_client.setSessionTimeout(120); + } + basic_client.setTimeout(5000); + ssl_client.setTimeout(5000); + ssl_client.setClient(&basic_client, is_https); + + // split into host and path + String rest = url.substring(scheme_len); + String host; + String path; + int s = rest.indexOf('/'); + if (s < 0) { + host = rest; + path = "/"; + } else { + host = rest.substring(0, s); + path = rest.substring(s); + } + + if (!ssl_client.connect(host.c_str(), is_https ? 443 : 80)) { +#if defined(EMSESP_DEBUG) + EMSESP::logger().debug("versions.json: connection failed"); #endif return false; } - int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) { + // minimal HTTP/1.0 GET so we don't have to handle chunked encoding + ssl_client.print("GET "); + ssl_client.print(path); + ssl_client.println(" HTTP/1.0"); + ssl_client.print("Host: "); + ssl_client.println(host); + ssl_client.println("User-Agent: EMS-ESP"); + ssl_client.println("Connection: close"); + ssl_client.print("\r\n"); + + // wait for the first byte + uint32_t ms = millis(); + while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 5000) { + delay(1); + } + + // parse status line + String status_line = ssl_client.readStringUntil('\n'); + int sp = status_line.indexOf(' '); + int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0; + if (http_code != 200) { + ssl_client.stop(); #if defined(EMSESP_DEBUG) - EMSESP::logger().debug("versions.json: HTTP error code %d", httpCode); + EMSESP::logger().debug("versions.json: HTTP error code %d", http_code); #endif - http.end(); return false; } + // skip headers + while (ssl_client.connected() || ssl_client.available()) { + String line = ssl_client.readStringUntil('\n'); + line.trim(); + if (line.isEmpty()) { + break; + } + } + JsonDocument doc(PSRAM_DOC); - DeserializationError err = deserializeJson(doc, http.getStream()); - http.end(); + DeserializationError err = deserializeJson(doc, ssl_client); + ssl_client.stop(); if (err) { #if defined(EMSESP_DEBUG) EMSESP::logger().debug("versions.json: parse error");