316 lines
11 KiB
C++
316 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 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);
|
|
}
|