#include "WebServer.hpp" #include "../SdCard.hpp" #include "esp_log.h" #include #include #include 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 postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &postUsbMode); 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); }