knxdisplay/main/webserver/WebServer.cpp
2026-01-30 08:51:59 +01:00

313 lines
11 KiB
C++

#include "WebServer.hpp"
#include "../SdCard.hpp"
#include "esp_log.h"
#include <cstring>
#include <sys/stat.h>
#include <dirent.h>
static const char* TAG = "WebServer";
WebServer& WebServer::instance() {
static WebServer instance;
return instance;
}
WebServer::~WebServer() {
stop();
}
void WebServer::start() {
if (server_ != nullptr) {
ESP_LOGW(TAG, "Server already running");
return;
}
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
config.stack_size = 8192;
config.max_uri_handlers = 24;
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
if (httpd_start(&server_, &config) != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server");
return;
}
// Static file routes
httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = rootHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &root);
httpd_uri_t webseite = { .uri = "/webseite/*", .method = HTTP_GET, .handler = staticFileHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &webseite);
httpd_uri_t images = { .uri = "/images/*", .method = HTTP_GET, .handler = imagesHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &images);
// Config routes
httpd_uri_t getConfig = { .uri = "/api/config", .method = HTTP_GET, .handler = getConfigHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &getConfig);
httpd_uri_t postConfig = { .uri = "/api/config", .method = HTTP_POST, .handler = postConfigHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &postConfig);
httpd_uri_t postSave = { .uri = "/api/save", .method = HTTP_POST, .handler = postSaveHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &postSave);
httpd_uri_t postReset = { .uri = "/api/reset", .method = HTTP_POST, .handler = postResetHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &postReset);
// KNX routes
httpd_uri_t getKnxAddresses = { .uri = "/api/knx/addresses", .method = HTTP_GET, .handler = getKnxAddressesHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &getKnxAddresses);
httpd_uri_t getKnxProg = { .uri = "/api/knx/prog", .method = HTTP_GET, .handler = getKnxProgHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &getKnxProg);
httpd_uri_t postKnxProg = { .uri = "/api/knx/prog", .method = HTTP_POST, .handler = postKnxProgHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &postKnxProg);
httpd_uri_t postKnxReset = { .uri = "/api/knx/reset", .method = HTTP_POST, .handler = postKnxResetHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &postKnxReset);
// Status routes
httpd_uri_t getStatus = { .uri = "/api/status", .method = HTTP_GET, .handler = getStatusHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &getStatus);
// File manager routes
httpd_uri_t fileManager = { .uri = "/files", .method = HTTP_GET, .handler = fileManagerHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &fileManager);
httpd_uri_t filesList = { .uri = "/api/files/list", .method = HTTP_GET, .handler = filesListHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesList);
httpd_uri_t filesUpload = { .uri = "/api/files/upload", .method = HTTP_POST, .handler = filesUploadHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesUpload);
httpd_uri_t filesDownload = { .uri = "/api/files/download", .method = HTTP_GET, .handler = filesDownloadHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesDownload);
httpd_uri_t filesDelete = { .uri = "/api/files/delete", .method = HTTP_DELETE, .handler = filesDeleteHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesDelete);
httpd_uri_t filesMkdir = { .uri = "/api/files/mkdir", .method = HTTP_POST, .handler = filesMkdirHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesMkdir);
httpd_uri_t filesRead = { .uri = "/api/files/read", .method = HTTP_GET, .handler = filesReadHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesRead);
httpd_uri_t filesWrite = { .uri = "/api/files/write", .method = HTTP_POST, .handler = filesWriteHandler, .user_ctx = nullptr };
httpd_register_uri_handler(server_, &filesWrite);
ESP_LOGI(TAG, "HTTP server started successfully");
}
void WebServer::stop() {
if (server_ != nullptr) {
httpd_stop(server_);
server_ = nullptr;
ESP_LOGI(TAG, "HTTP server stopped");
}
}
// ============================================================================
// Shared Utility Functions
// ============================================================================
const char* WebServer::getContentType(const char* filepath) {
const char* ext = strrchr(filepath, '.');
if (!ext) return "application/octet-stream";
if (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".htm") == 0) return "text/html; charset=utf-8";
if (strcasecmp(ext, ".css") == 0) return "text/css";
if (strcasecmp(ext, ".js") == 0) return "application/javascript";
if (strcasecmp(ext, ".json") == 0) return "application/json";
if (strcasecmp(ext, ".png") == 0) return "image/png";
if (strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0) return "image/jpeg";
if (strcasecmp(ext, ".gif") == 0) return "image/gif";
if (strcasecmp(ext, ".svg") == 0) return "image/svg+xml";
if (strcasecmp(ext, ".ico") == 0) return "image/x-icon";
return "application/octet-stream";
}
// Case-insensitive path resolution for FAT filesystem
bool resolveCaseInsensitivePath(const char* requestedPath, char* resolvedPath, size_t maxLen) {
strncpy(resolvedPath, SdCard::MOUNT_POINT, maxLen);
resolvedPath[maxLen - 1] = '\0';
const char* relPath = requestedPath;
if (strncmp(requestedPath, SdCard::MOUNT_POINT, strlen(SdCard::MOUNT_POINT)) == 0) {
relPath = requestedPath + strlen(SdCard::MOUNT_POINT);
}
if (!relPath || !*relPath || (relPath[0] == '/' && !relPath[1])) {
return true;
}
char component[256];
const char* start = relPath;
while (*start) {
while (*start == '/') start++;
if (!*start) break;
const char* end = start;
while (*end && *end != '/') end++;
size_t compLen = end - start;
if (compLen >= sizeof(component)) compLen = sizeof(component) - 1;
memcpy(component, start, compLen);
component[compLen] = '\0';
DIR* dir = opendir(resolvedPath);
if (!dir) return false;
bool found = false;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcasecmp(entry->d_name, component) == 0) {
size_t curLen = strlen(resolvedPath);
snprintf(resolvedPath + curLen, maxLen - curLen, "/%s", entry->d_name);
found = true;
break;
}
}
closedir(dir);
if (!found) return false;
start = end;
}
return true;
}
esp_err_t WebServer::sendFile(httpd_req_t* req, const char* filepath) {
char resolvedPath[384];
const char* actualPath = filepath;
struct stat st;
if (stat(filepath, &st) != 0) {
if (resolveCaseInsensitivePath(filepath, resolvedPath, sizeof(resolvedPath))) {
if (stat(resolvedPath, &st) == 0) {
actualPath = resolvedPath;
}
}
}
if (stat(actualPath, &st) != 0) {
ESP_LOGE(TAG, "File not found: %s", filepath);
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
return ESP_FAIL;
}
FILE* f = fopen(actualPath, "r");
if (!f) {
ESP_LOGE(TAG, "Failed to open file: %s", actualPath);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to open file");
return ESP_FAIL;
}
httpd_resp_set_type(req, getContentType(actualPath));
char* chunk = new char[1024];
if (!chunk) {
fclose(f);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory");
return ESP_FAIL;
}
size_t read;
while ((read = fread(chunk, 1, 1024, f)) > 0) {
if (httpd_resp_send_chunk(req, chunk, read) != ESP_OK) {
delete[] chunk;
fclose(f);
return ESP_FAIL;
}
}
delete[] chunk;
fclose(f);
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
}
// URL decode helper (in-place)
void urlDecode(char* str) {
char* dst = str;
char* src = str;
while (*src) {
if (*src == '%' && src[1] && src[2]) {
int hi = src[1];
int lo = src[2];
hi = (hi >= '0' && hi <= '9') ? hi - '0' :
(hi >= 'A' && hi <= 'F') ? hi - 'A' + 10 :
(hi >= 'a' && hi <= 'f') ? hi - 'a' + 10 : -1;
lo = (lo >= '0' && lo <= '9') ? lo - '0' :
(lo >= 'A' && lo <= 'F') ? lo - 'A' + 10 :
(lo >= 'a' && lo <= 'f') ? lo - 'a' + 10 : -1;
if (hi >= 0 && lo >= 0) {
*dst++ = (char)((hi << 4) | lo);
src += 3;
continue;
}
} else if (*src == '+') {
*dst++ = ' ';
src++;
continue;
}
*dst++ = *src++;
}
*dst = '\0';
}
bool WebServer::getQueryParam(httpd_req_t* req, const char* key, char* value, size_t maxLen) {
size_t queryLen = httpd_req_get_url_query_len(req);
if (queryLen == 0) return false;
char* query = new char[queryLen + 1];
if (!query) return false;
if (httpd_req_get_url_query_str(req, query, queryLen + 1) != ESP_OK) {
delete[] query;
return false;
}
esp_err_t ret = httpd_query_key_value(query, key, value, maxLen);
delete[] query;
if (ret == ESP_OK) {
urlDecode(value);
return true;
}
return false;
}
// JSON response helpers
esp_err_t WebServer::sendJsonObject(httpd_req_t* req, cJSON* json) {
char* str = cJSON_PrintUnformatted(json);
if (!str) {
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON print failed");
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, str, strlen(str));
free(str);
cJSON_Delete(json);
return ESP_OK;
}
esp_err_t WebServer::sendJsonError(httpd_req_t* req, const char* error) {
cJSON* json = cJSON_CreateObject();
cJSON_AddBoolToObject(json, "success", false);
cJSON_AddStringToObject(json, "error", error);
return sendJsonObject(req, json);
}
esp_err_t WebServer::sendJsonSuccess(httpd_req_t* req) {
cJSON* json = cJSON_CreateObject();
cJSON_AddBoolToObject(json, "success", true);
return sendJsonObject(req, json);
}