#include "WebPanelServer.h"
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
#include
#include
#include
#include
#include
#include "Update.h"
#include "../mqtt/generated/WebPanelCert.h"
namespace {
#ifndef WEB_PANEL_STACK_SIZE
#define WEB_PANEL_STACK_SIZE 6144
#endif
#ifndef WEB_PANEL_IDLE_TIMEOUT_MS
#define WEB_PANEL_IDLE_TIMEOUT_MS (15UL * 60UL * 1000UL)
#endif
constexpr size_t kWebServerStackSize = WEB_PANEL_STACK_SIZE;
constexpr size_t kWebPasswordBufferSize = 80;
constexpr size_t kWebCommandBufferSize = 192;
constexpr size_t kWebReplyBufferSize = 256;
constexpr size_t kWebStatsQueryBufferSize = 96;
constexpr size_t kWebStatsReplyBufferSize = 4608;
constexpr size_t kWebPageChunkSize = MBEDTLS_SSL_OUT_CONTENT_LEN;
constexpr unsigned long kWebIdleTimeoutMs = WEB_PANEL_IDLE_TIMEOUT_MS;
#if defined(MQTT_DEBUG) && MQTT_DEBUG
#define WEB_PANEL_LOG(fmt, ...) Serial.printf("[WEB] " fmt "\n", ##__VA_ARGS__)
#else
#define WEB_PANEL_LOG(...) do { } while (0)
#endif
char* allocScratchBuffer(size_t size) {
void* ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == nullptr) {
ptr = heap_caps_malloc(size, MALLOC_CAP_8BIT);
}
return static_cast(ptr);
}
void freeScratchBuffer(void* ptr) {
if (ptr != nullptr) {
heap_caps_free(ptr);
}
}
void bytesToHexUpper(const uint8_t* src, size_t len, char* dst, size_t dst_size) {
if (dst_size == 0) {
return;
}
size_t di = 0;
for (size_t i = 0; i < len && di + 2 < dst_size; ++i) {
snprintf(&dst[di], dst_size - di, "%02X", src[i]);
di += 2;
}
dst[(di < dst_size) ? di : (dst_size - 1)] = 0;
}
esp_err_t sendChunk(httpd_req_t* req, const char* text) {
return httpd_resp_sendstr_chunk(req, text != nullptr ? text : "");
}
#define sendProgmem(req, mem) sendWhole(req, mem, sizeof(mem) - 1)
esp_err_t sendWhole(httpd_req_t* req, const char* text, const size_t len) {
if (text == nullptr || len == 0) {
return httpd_resp_send_chunk(req, nullptr, 0);
}
const size_t last_size = ((len - 1) % kWebPageChunkSize) + 1;
const char* last = text + (len - last_size);
for (const char* p = text; p < last; p += kWebPageChunkSize) {
if (httpd_resp_send_chunk(req, p, kWebPageChunkSize) != ESP_OK) {
return ESP_FAIL;
}
vTaskDelay(1);
}
if (httpd_resp_send_chunk(req, last, last_size) != ESP_OK) {
return ESP_FAIL;
}
return httpd_resp_send_chunk(req, nullptr, 0);
}
esp_err_t sendJsonEscapedChunk(httpd_req_t* req, const char* src) {
char chunk[48];
size_t offset = 0;
for (size_t i = 0; src != nullptr && src[i] != 0; ++i) {
const char* escape = nullptr;
char c = src[i];
switch (c) {
case '\\':
escape = "\\\\";
break;
case '"':
escape = "\\\"";
break;
case '\n':
escape = "\\n";
break;
case '\r':
escape = "\\r";
break;
case '\t':
escape = "\\t";
break;
default:
break;
}
const char* fragment = escape;
char single[2] = {c, 0};
if (fragment == nullptr) {
fragment = single;
}
size_t frag_len = strlen(fragment);
if (offset + frag_len >= sizeof(chunk) - 1) {
chunk[offset] = 0;
if (sendChunk(req, chunk) != ESP_OK) {
return ESP_FAIL;
}
offset = 0;
}
memcpy(&chunk[offset], fragment, frag_len);
offset += frag_len;
}
if (offset > 0) {
chunk[offset] = 0;
return sendChunk(req, chunk);
}
return ESP_OK;
}
esp_err_t sendJsonFieldChunk(httpd_req_t* req, const char* key, const char* value, bool comma) {
char prefix[40];
int written = snprintf(prefix, sizeof(prefix), "%s\"%s\":\"", comma ? "," : "", key);
if (written < 0 || static_cast(written) >= sizeof(prefix)) {
return ESP_FAIL;
}
if (sendChunk(req, prefix) != ESP_OK) {
return ESP_FAIL;
}
if (sendJsonEscapedChunk(req, value != nullptr ? value : "") != ESP_OK) {
return ESP_FAIL;
}
return sendChunk(req, "\"");
}
bool getQueryValue(httpd_req_t* req, const char* key, char* value, size_t value_size) {
if (req == nullptr || key == nullptr || value == nullptr || value_size == 0) {
return false;
}
const size_t query_len = httpd_req_get_url_query_len(req);
if (query_len == 0 || query_len + 1 > kWebStatsQueryBufferSize) {
return false;
}
char query[kWebStatsQueryBufferSize];
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) {
return false;
}
return httpd_query_key_value(query, key, value, value_size) == ESP_OK;
}
esp_err_t sendLegacyStatsBundle(httpd_req_t* req, WebPanelCommandRunner* runner, char* reply) {
const struct {
const char* key;
const char* command;
} fields[] = {
{"wifi", "get wifi.status"},
{"wifi_powersave", "get wifi.powersaving"},
{"core", "stats-core"},
{"radio", "stats-radio"},
{"packets", "stats-packets"},
{"memory", "memory"},
};
httpd_resp_set_type(req, "application/json; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
if (sendChunk(req, "{") != ESP_OK) {
httpd_resp_sendstr_chunk(req, nullptr);
return ESP_FAIL;
}
for (size_t i = 0; i < (sizeof(fields) / sizeof(fields[0])); ++i) {
memset(reply, 0, kWebReplyBufferSize);
runner->runWebCommand(fields[i].command, reply, kWebReplyBufferSize);
if (sendJsonFieldChunk(req, fields[i].key, reply, i != 0) != ESP_OK) {
httpd_resp_sendstr_chunk(req, nullptr);
return ESP_FAIL;
}
}
esp_err_t rc = sendChunk(req, "}");
if (rc == ESP_OK) {
rc = httpd_resp_sendstr_chunk(req, nullptr);
} else {
httpd_resp_sendstr_chunk(req, nullptr);
}
return rc;
}
const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
Repeater Login
Repeater Config
☾
Use the repeater admin password to unlock the panel.
Unlock
)HTML";
const char kWebPanelStatsDisabledHtml[] PROGMEM = R"HTML(
Stats Disabled
Stats Disabled
The dedicated stats page is currently disabled for this node.
Enable it with `set web.stats on` from the CLI or return to /app .
)HTML";
const char kWebPanelAppHtml[] PROGMEM = R"HTML(
Repeater Config
MQTT IATA needs setting under MQTT Settings.
Repeater Config
Use the repeater admin password to unlock the command console.
Unlock
Actions
App
Stats
Reboot
☾
Logout
Quick "get" Commands
WiFi
wifi.status
wifi.powersaving
MQTT
mqtt.status
mqtt.client_version
mqtt.iata
mqtt.owner
mqtt.email
Run CLI Command
Run
Authenticated sessions can run repeater CLI commands here.
Repeater Settings
Radio Settings
Preset
Loading presets...
↻
Save
Path Hash Mode
1-byte
2-byte
3-byte
↻
Save
Modes
Keeps web and MQTT running, but turns repeat off and disables both local and flood adverts. Turning it back off restores the prior repeat and advert settings if known, else falls back to repeat on, advert interval 60 minutes, and flood advert interval 12 hours.
MQTT Settings
MQTT Servers
Only two brokers can be enabled at the same time.
MQTT IATA is unset. Set it before enabling MeshCoreTel or LetsMesh brokers.
Stats
Open /stats
Current status and historical trends have moved to the dedicated stats page.
Stats
Trends
Loading recent trend guidance...
Neighbours
Loading neighbours...
)HTML";
} // namespace
WebPanelServer::WebPanelServer()
: _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this}, _ota_started(false) {
}
void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) {
_runner = runner;
}
bool WebPanelServer::start() {
if (_server != nullptr || _runner == nullptr) {
return _server != nullptr;
}
noteActivity();
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
config.httpd.max_open_sockets = 2;
config.httpd.max_uri_handlers = 8;
config.httpd.max_resp_headers = 4;
config.httpd.backlog_conn = 0;
config.httpd.recv_wait_timeout = 10;
config.httpd.send_wait_timeout = 10;
config.httpd.lru_purge_enable = true;
config.httpd.stack_size = kWebServerStackSize;
config.httpd.task_priority = tskIDLE_PRIORITY + 2;
config.httpd.core_id = 0;
#if defined(ESP_IDF_VERSION_MAJOR) && ESP_IDF_VERSION_MAJOR >= 5
config.servercert = reinterpret_cast(mqtt_web_panel_cert::kServerCertPem);
config.servercert_len = sizeof(mqtt_web_panel_cert::kServerCertPem);
#else
// IDF 4.x uses the misnamed CA slot for the server certificate.
config.cacert_pem = reinterpret_cast(mqtt_web_panel_cert::kServerCertPem);
config.cacert_len = sizeof(mqtt_web_panel_cert::kServerCertPem);
#endif
config.prvtkey_pem = reinterpret_cast(mqtt_web_panel_cert::kServerKeyPem);
config.prvtkey_len = sizeof(mqtt_web_panel_cert::kServerKeyPem);
esp_err_t rc = httpd_ssl_start(&_server, &config);
if (rc != ESP_OK) {
_server = nullptr;
WEB_PANEL_LOG("server start failed rc=0x%x", static_cast(rc));
return false;
}
httpd_uri_t index_uri = {.uri = "/", .method = HTTP_GET, .handler = &WebPanelServer::handleIndex, .user_ctx = &_route_context};
httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context};
httpd_uri_t stats_page_uri = {.uri = "/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStatsPage, .user_ctx = &_route_context};
httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context};
httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context};
httpd_uri_t update = {.uri = "/update", .method = HTTP_GET, .handler = &WebPanelServer::handleOtaRedirect, .user_ctx = &_route_context};
httpd_uri_t ota_uri = { .uri = "/api/ota", .method = HTTP_POST, .handler = &WebPanelServer::handleOtaUpload, .user_ctx = &_route_context };
httpd_register_uri_handler(_server, &index_uri);
httpd_register_uri_handler(_server, &app_uri);
httpd_register_uri_handler(_server, &stats_page_uri);
httpd_register_uri_handler(_server, &login_uri);
httpd_register_uri_handler(_server, &command_uri);
httpd_register_uri_handler(_server, &stats_uri);
httpd_register_uri_handler(_server, &update);
httpd_register_uri_handler(_server, &ota_uri);
httpd_config_t redirect_config = HTTPD_DEFAULT_CONFIG();
redirect_config.server_port = 80;
// HTTPS already uses the default control port from HTTPD_SSL_CONFIG_DEFAULT().
// The redirect listener needs its own control port or startup will fail.
redirect_config.ctrl_port = 32769;
redirect_config.max_open_sockets = 2;
redirect_config.max_uri_handlers = 1;
redirect_config.max_resp_headers = 4;
redirect_config.backlog_conn = 0;
redirect_config.recv_wait_timeout = 10;
redirect_config.send_wait_timeout = 10;
redirect_config.lru_purge_enable = true;
redirect_config.stack_size = kWebServerStackSize;
redirect_config.task_priority = tskIDLE_PRIORITY + 2;
redirect_config.core_id = 0;
redirect_config.uri_match_fn = httpd_uri_match_wildcard;
rc = httpd_start(&_redirect_server, &redirect_config);
if (rc == ESP_OK) {
httpd_uri_t redirect_uri = {.uri = "/*", .method = HTTP_GET, .handler = &WebPanelServer::handleHttpRedirect, .user_ctx = &_route_context};
httpd_register_uri_handler(_redirect_server, &redirect_uri);
} else {
_redirect_server = nullptr;
WEB_PANEL_LOG("redirect server start failed rc=0x%x", static_cast(rc));
}
WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
return true;
}
void WebPanelServer::stop() {
stopRedirectServer();
if (_server != nullptr) {
httpd_ssl_stop(_server);
_server = nullptr;
WEB_PANEL_LOG("server stopped");
}
_token[0] = 0;
_last_activity_ms = 0;
}
bool WebPanelServer::isRunning() const {
return _server != nullptr;
}
bool WebPanelServer::hasSessionToken() const {
return _token[0] != 0;
}
void WebPanelServer::stopRedirectServer() {
if (_redirect_server != nullptr) {
httpd_stop(_redirect_server);
_redirect_server = nullptr;
WEB_PANEL_LOG("redirect server stopped");
}
}
bool WebPanelServer::shouldAutoLock(unsigned long now_ms) const {
if (_server == nullptr || _token[0] == 0 || kWebIdleTimeoutMs == 0 || _last_activity_ms == 0) {
return false;
}
return now_ms - _last_activity_ms >= kWebIdleTimeoutMs;
}
void WebPanelServer::lockSession() {
_token[0] = 0;
_last_activity_ms = 0;
}
void WebPanelServer::notifyOtaStarted() {
_ota_started = true;
}
esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
ctx->self->noteActivity();
httpd_resp_set_type(req, "text/html; charset=utf-8");
return sendProgmem(req, kWebPanelLoginHtml);
}
esp_err_t WebPanelServer::handleHttpRedirect(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
char location[160];
const char* path = (req->uri != nullptr && req->uri[0] != 0) ? req->uri : "/";
snprintf(location, sizeof(location), "https://%s%s", WiFi.localIP().toString().c_str(), path);
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", location);
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
httpd_resp_set_hdr(req, "Connection", "close");
return httpd_resp_send(req, "", 0);
}
esp_err_t WebPanelServer::handleApp(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
ctx->self->noteActivity();
httpd_resp_set_type(req, "text/html; charset=utf-8");
return sendProgmem(req, kWebPanelAppHtml);
}
esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
ctx->self->noteActivity();
httpd_resp_set_type(req, "text/html; charset=utf-8");
if (ctx->self->_runner != nullptr && !ctx->self->_runner->isWebStatsEnabled()) {
return sendProgmem(req, kWebPanelStatsDisabledHtml);
}
return sendProgmem(req, kWebPanelAppHtml);
}
esp_err_t WebPanelServer::handleOtaRedirect(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->_ota_started) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "OTA not started");
return ESP_OK;
}
char host[64] = {};
if (httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) == ESP_OK && host[0] != '\0') {
char* colon = strchr(host, ':');
if (colon != nullptr) *colon = '\0';
} else {
strncpy(host, WiFi.localIP().toString().c_str(), sizeof(host) - 1);
host[sizeof(host) - 1] = '\0';
}
char location[80];
snprintf(location, sizeof(location), "http://%s/update", host);
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", location);
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
httpd_resp_set_hdr(req, "Connection", "close");
return httpd_resp_send(req, "", 0);
}
esp_err_t WebPanelServer::handleOtaUpload(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->isAuthorized(req)) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
ctx->self->noteActivity();
int remaining = req->content_len;
if (remaining <= 0) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "No content");
}
constexpr size_t kChunkSize = 4096;
char* buf = allocScratchBuffer(kChunkSize);
if (buf == nullptr) {
return httpd_resp_send_500(req);
}
if (!Update.begin(static_cast(remaining), U_FLASH)) {
freeScratchBuffer(buf);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Update begin failed");
}
while (remaining > 0) {
int to_read = (remaining < (int)kChunkSize) ? remaining : (int)kChunkSize;
int n = httpd_req_recv(req, buf, to_read);
if (n <= 0) {
Update.abort();
freeScratchBuffer(buf);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Read error");
}
if (Update.write(reinterpret_cast(buf), n) != (size_t)n) {
Update.abort();
freeScratchBuffer(buf);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Write error");
}
remaining -= n;
}
freeScratchBuffer(buf);
if (!Update.end()) {
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Update end failed");
}
(void)httpd_resp_sendstr(req, "OK");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
}
esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
return httpd_resp_send_500(req);
}
char* password = allocScratchBuffer(kWebPasswordBufferSize);
if (password == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->readRequestBody(req, password, kWebPasswordBufferSize)) {
freeScratchBuffer(password);
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad request");
}
if (strcmp(password, ctx->self->_runner->getWebAdminPassword()) != 0) {
freeScratchBuffer(password);
WEB_PANEL_LOG("login denied");
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Bad password");
}
freeScratchBuffer(password);
ctx->self->refreshToken();
ctx->self->noteActivity();
WEB_PANEL_LOG("login accepted");
httpd_resp_set_type(req, "text/plain; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
return httpd_resp_sendstr(req, ctx->self->_token);
}
esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->isAuthorized(req)) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
char* command = allocScratchBuffer(kWebCommandBufferSize);
char* reply = allocScratchBuffer(kWebReplyBufferSize);
if (command == nullptr || reply == nullptr) {
freeScratchBuffer(command);
freeScratchBuffer(reply);
return httpd_resp_send_500(req);
}
if (!ctx->self->readRequestBody(req, command, kWebCommandBufferSize)) {
freeScratchBuffer(command);
freeScratchBuffer(reply);
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad request");
}
ctx->self->noteActivity();
memset(reply, 0, kWebReplyBufferSize);
ctx->self->_runner->runWebCommand(command, reply, kWebReplyBufferSize);
httpd_resp_set_type(req, "text/plain; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
esp_err_t rc = httpd_resp_send(req, reply[0] ? reply : "OK", HTTPD_RESP_USE_STRLEN);
freeScratchBuffer(command);
freeScratchBuffer(reply);
return rc;
}
esp_err_t WebPanelServer::handleStats(httpd_req_t* req) {
auto* ctx = static_cast(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->isAuthorized(req)) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
ctx->self->noteActivity();
char view[16];
char series[24];
char* reply = allocScratchBuffer(kWebStatsReplyBufferSize);
if (reply == nullptr) {
freeScratchBuffer(reply);
return httpd_resp_send_500(req);
}
if (getQueryValue(req, "view", view, sizeof(view)) && strcmp(view, "legacy") == 0) {
esp_err_t legacy_rc = sendLegacyStatsBundle(req, ctx->self->_runner, reply);
freeScratchBuffer(reply);
return legacy_rc;
}
if (!ctx->self->_runner->isWebStatsEnabled()) {
freeScratchBuffer(reply);
httpd_resp_set_status(req, "503 Service Unavailable");
return httpd_resp_send(req, "Stats disabled", HTTPD_RESP_USE_STRLEN);
}
bool ok = false;
if (getQueryValue(req, "series", series, sizeof(series))) {
ok = ctx->self->_runner->formatWebStatsSeriesJson(series, reply, kWebStatsReplyBufferSize);
} else {
ok = ctx->self->_runner->formatWebStatsSummaryJson(reply, kWebStatsReplyBufferSize);
}
if (!ok || reply[0] == 0) {
freeScratchBuffer(reply);
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "No stats data");
}
httpd_resp_set_type(req, "application/json; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
esp_err_t rc = httpd_resp_send(req, reply, HTTPD_RESP_USE_STRLEN);
freeScratchBuffer(reply);
return rc;
}
bool WebPanelServer::readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const {
if (req == nullptr || buffer == nullptr || buffer_size == 0 || req->content_len <= 0 ||
req->content_len >= static_cast(buffer_size)) {
return false;
}
int remaining = req->content_len;
int offset = 0;
while (remaining > 0) {
int read = httpd_req_recv(req, &buffer[offset], remaining);
if (read <= 0) {
return false;
}
offset += read;
remaining -= read;
}
buffer[offset] = 0;
return true;
}
void WebPanelServer::refreshToken() {
uint8_t token[16];
esp_fill_random(token, sizeof(token));
bytesToHexUpper(token, sizeof(token), _token, sizeof(_token));
}
bool WebPanelServer::isAuthorized(httpd_req_t* req) const {
if (_token[0] == 0) {
return false;
}
char token[40];
if (httpd_req_get_hdr_value_str(req, "X-Auth-Token", token, sizeof(token)) != ESP_OK) {
return false;
}
return strcmp(token, _token) == 0;
}
void WebPanelServer::noteActivity() {
_last_activity_ms = millis();
}
#else
WebPanelServer::WebPanelServer()
: _runner(nullptr) {
}
void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) {
_runner = runner;
}
bool WebPanelServer::start() {
return false;
}
void WebPanelServer::stop() {
}
void WebPanelServer::stopRedirectServer() {
}
bool WebPanelServer::isRunning() const {
return false;
}
bool WebPanelServer::hasSessionToken() const {
return false;
}
bool WebPanelServer::shouldAutoLock(unsigned long) const {
return false;
}
void WebPanelServer::lockSession() {
}
void WebPanelServer::notifyOtaStarted() {
}
#endif