mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
possible fix for #3201
This commit is contained in:
+37
-10
@@ -35,6 +35,31 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
||||
return 0; // unsupported scheme
|
||||
}
|
||||
|
||||
// split the host from the path before anything is allocated, so a bad URL costs nothing
|
||||
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 = "/";
|
||||
}
|
||||
|
||||
if (host.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ESP_SSLClient keeps the host in a fixed char[64] and strcpy()s into it without a length
|
||||
// check, so a longer name would corrupt whatever follows it and crash somewhere unrelated
|
||||
if (host.length() > MAX_HOSTNAME_LENGTH) {
|
||||
EMSESP::logger().warning("%s hostname is too long (%u characters, max is %u)",
|
||||
is_https ? "HTTPS" : "HTTP",
|
||||
(unsigned)host.length(),
|
||||
(unsigned)MAX_HOSTNAME_LENGTH);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WiFiClient * basic_client = new WiFiClient;
|
||||
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
||||
if (is_https) {
|
||||
@@ -50,16 +75,6 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
||||
ssl_client->setTimeout(5); // seconds, drives BearSSL only - unused on the plain HTTP path
|
||||
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;
|
||||
@@ -97,16 +112,25 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
||||
// the stream goes idle, or the overall budget runs out
|
||||
const uint32_t started = millis();
|
||||
uint32_t last_data = started;
|
||||
bool truncated = false;
|
||||
while (millis() - started < TOTAL_TIMEOUT_MS) {
|
||||
const int avail = ssl_client->available();
|
||||
if (avail > 0) {
|
||||
if (result.length() >= MAX_RESPONSE_BYTES) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
uint8_t buf[128];
|
||||
const size_t want = (avail < (int)sizeof(buf)) ? (size_t)avail : sizeof(buf);
|
||||
const int len = ssl_client->read(buf, want);
|
||||
if (len > 0) {
|
||||
result.append(reinterpret_cast<const char *>(buf), len);
|
||||
last_data = millis();
|
||||
continue;
|
||||
}
|
||||
// available() reported bytes the client won't hand over. Yielding here keeps a
|
||||
// stale count from busy-spinning the whole read budget on this task's core
|
||||
delay(1);
|
||||
continue;
|
||||
}
|
||||
if (!ssl_client->connected()) {
|
||||
@@ -121,6 +145,9 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
||||
ssl_client->stop();
|
||||
|
||||
const auto received = result.length();
|
||||
if (truncated) {
|
||||
EMSESP::logger().warning("%s response from %s truncated at %u bytes", is_https ? "HTTPS" : "HTTP", host.c_str(), (unsigned)received);
|
||||
}
|
||||
|
||||
// parse the status line "HTTP/1.x <code> <reason>". stoi() would abort rather than throw on
|
||||
// a malformed response, since the firmware is built with -fno-exceptions
|
||||
|
||||
@@ -34,6 +34,12 @@ class HttpClient {
|
||||
static constexpr uint32_t FIRST_BYTE_TIMEOUT_MS = 8000; // how long the server may take to start replying
|
||||
static constexpr uint32_t IDLE_TIMEOUT_MS = 500; // gap in the stream that marks the end of a response
|
||||
static constexpr uint32_t TOTAL_TIMEOUT_MS = 10000; // ceiling for the whole read
|
||||
|
||||
// ceiling on the response we'll buffer. Without it a chatty or hijacked endpoint can grow the
|
||||
// result string until the heap runs out, which aborts the firmware since it's built -fno-exceptions
|
||||
static constexpr size_t MAX_RESPONSE_BYTES = 32 * 1024;
|
||||
|
||||
static constexpr size_t MAX_HOSTNAME_LENGTH = 63; // ESP_SSLClient stores the host in a char[64]
|
||||
};
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
@@ -1126,6 +1126,12 @@ void System::show_system(uuid::console::Shell & shell) {
|
||||
shell.printfln(" Internal heap free/largest block: %u KB / %u KB",
|
||||
heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024,
|
||||
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024);
|
||||
// Lowest the loop task's stack has ever been since boot. EMSESP::loop() runs the blocking HTTP
|
||||
// client and all the JSON work on this task, and a TLS handshake alone can want several KB
|
||||
TaskHandle_t loop_task = xTaskGetHandle("loopTask");
|
||||
if (loop_task != nullptr) {
|
||||
shell.printfln(" Loop task stack min free/total: %u / %u bytes", (unsigned)uxTaskGetStackHighWaterMark(loop_task), (unsigned)getArduinoLoopTaskStackSize());
|
||||
}
|
||||
#endif
|
||||
shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree());
|
||||
uint32_t FSused = LittleFS.usedBytes() / 1024;
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define EMSESP_APP_VERSION "3.9.0-dev.9"
|
||||
#define EMSESP_APP_VERSION "3.9.0-dev.10"
|
||||
|
||||
@@ -348,7 +348,18 @@ void WebStatusService::getVersions(JsonObject root) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!versions_cache_valid_) {
|
||||
// take a snapshot, since the loop task can be rewriting the cache while we run on AsyncTCP
|
||||
VersionInfo stable;
|
||||
VersionInfo dev;
|
||||
bool cache_valid;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||
cache_valid = versions_cache_valid_;
|
||||
stable = versions_stable_;
|
||||
dev = versions_dev_;
|
||||
}
|
||||
|
||||
if (!cache_valid) {
|
||||
// no successful fetch yet (no network, fetch pending, or parse error)
|
||||
return;
|
||||
}
|
||||
@@ -364,8 +375,8 @@ void WebStatusService::getVersions(JsonObject root) {
|
||||
out["upgradeable"] = info.upgradeable;
|
||||
};
|
||||
|
||||
add_section("stable", versions_stable_);
|
||||
add_section("dev", versions_dev_);
|
||||
add_section("stable", stable);
|
||||
add_section("dev", dev);
|
||||
#else
|
||||
// standalone/test build: provide deterministic dummy data
|
||||
JsonObject stable_out = root["stable"].to<JsonObject>();
|
||||
@@ -383,7 +394,8 @@ void WebStatusService::getVersions(JsonObject root) {
|
||||
// schedule the next versions.json fetch a few seconds out so the network stack has time to settle
|
||||
// (DHCP completion, default-netif assignment and DNS server propagation through lwip)
|
||||
void WebStatusService::schedule_versions_refresh() {
|
||||
uint32_t next = uuid::get_uptime() + VERSIONS_INITIAL_FETCH_DELAY_MS;
|
||||
versions_failures_ = 0; // a fresh link is a fresh chance, so start again from the short retry delay
|
||||
uint32_t next = uuid::get_uptime() + VERSIONS_INITIAL_FETCH_DELAY_MS;
|
||||
if (next == 0) {
|
||||
next = 1; // 0 is the "idle" sentinel — never let the wrap land there
|
||||
}
|
||||
@@ -409,8 +421,18 @@ void WebStatusService::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = refresh_versions_cache();
|
||||
uint32_t next = uuid::get_uptime() + (ok ? VERSIONS_REFRESH_INTERVAL_MS : VERSIONS_RETRY_INTERVAL_MS);
|
||||
uint32_t interval;
|
||||
if (refresh_versions_cache()) {
|
||||
versions_failures_ = 0;
|
||||
interval = VERSIONS_REFRESH_INTERVAL_MS;
|
||||
} else {
|
||||
if (versions_failures_ < VERSIONS_MAX_BACKOFF_STEPS) {
|
||||
versions_failures_++;
|
||||
}
|
||||
interval = VERSIONS_RETRY_INTERVAL_MS << (versions_failures_ - 1);
|
||||
}
|
||||
|
||||
uint32_t next = uuid::get_uptime() + interval;
|
||||
if (next == 0) {
|
||||
next = 1;
|
||||
}
|
||||
@@ -451,16 +473,21 @@ bool WebStatusService::refresh_versions_cache() {
|
||||
out.upgradeable = !out.version.empty() && FirmwareVersion(out.version) > current_version;
|
||||
};
|
||||
|
||||
read_section("stable", versions_stable_);
|
||||
read_section("dev", versions_dev_);
|
||||
// parse into locals first, then publish in one go so the AsyncTCP task never sees a half-written cache
|
||||
VersionInfo stable;
|
||||
VersionInfo dev;
|
||||
read_section("stable", stable);
|
||||
read_section("dev", dev);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||
versions_stable_ = stable;
|
||||
versions_dev_ = dev;
|
||||
versions_cache_valid_ = true;
|
||||
}
|
||||
|
||||
versions_cache_valid_ = true;
|
||||
#if defined(EMSESP_DEBUG)
|
||||
EMSESP::logger().debug("Fetched from %s: stable=%s, dev=%s, current=%s",
|
||||
VERSIONS_URL,
|
||||
versions_stable_.version.c_str(),
|
||||
versions_dev_.version.c_str(),
|
||||
current_version_s.c_str());
|
||||
EMSESP::logger().debug("Fetched from %s: stable=%s, dev=%s, current=%s", VERSIONS_URL, stable.version.c_str(), dev.version.c_str(), current_version_s.c_str());
|
||||
#endif
|
||||
return true;
|
||||
#endif
|
||||
@@ -468,11 +495,12 @@ bool WebStatusService::refresh_versions_cache() {
|
||||
|
||||
// returns if current dev/stable is upgradeable
|
||||
bool WebStatusService::current_upgradeable() const {
|
||||
FirmwareVersion current_version(current_version_s);
|
||||
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
|
||||
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||
if (!versions_cache_valid_) {
|
||||
return false;
|
||||
}
|
||||
FirmwareVersion current_version(current_version_s);
|
||||
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
|
||||
return is_dev ? versions_dev_.upgradeable : versions_stable_.upgradeable;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "../core/firmwareVersion.h"
|
||||
#include "../emsesp_version.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
class WebStatusService {
|
||||
@@ -67,14 +69,24 @@ class WebStatusService {
|
||||
VersionInfo versions_dev_;
|
||||
bool versions_cache_valid_ = false; // true once we've had at least one successful fetch
|
||||
uint32_t versions_next_fetch_ms_ = 0; // uuid::get_uptime() of the next attempt; 0 = idle
|
||||
uint8_t versions_failures_ = 0; // consecutive failed fetches, drives the retry backoff
|
||||
|
||||
// the cache is written by the loop task in refresh_versions_cache() and read by the AsyncTCP
|
||||
// task in getVersions()/current_upgradeable(), so the std::strings need guarding - without it a
|
||||
// request landing mid-update reads a buffer that has just been freed
|
||||
mutable std::mutex versions_mutex_;
|
||||
|
||||
bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success
|
||||
|
||||
static constexpr const char * VERSIONS_URL = "http://emsesp.org/versions.json";
|
||||
|
||||
static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success
|
||||
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure
|
||||
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after the first failure
|
||||
static constexpr uint32_t VERSIONS_INITIAL_FETCH_DELAY_MS = 5UL * 1000UL; // 5 s after a link comes up
|
||||
|
||||
// each attempt blocks the loop task for as long as the DNS lookup, connect and read timeouts
|
||||
// allow, so double the retry delay per consecutive failure: 5, 10, 20, 40, 80, 160 minutes
|
||||
static constexpr uint8_t VERSIONS_MAX_BACKOFF_STEPS = 6;
|
||||
};
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
Reference in New Issue
Block a user