286 lines
8.1 KiB
C++
286 lines
8.1 KiB
C++
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
// SDK
|
|
#include "esp_http_server.h"
|
|
#include "esp_log.h"
|
|
#include "esp_vfs.h"
|
|
|
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
|
#include "esp_littlefs.h"
|
|
#endif
|
|
|
|
// Project
|
|
#include "api/ota/bundle.cpp"
|
|
#include "api/ota/firmware.cpp"
|
|
#include "api/ota/frontend.cpp"
|
|
#include "api/ota/status.cpp"
|
|
#include "api/system/info.cpp"
|
|
#include "api/system/reboot.cpp"
|
|
#include "api/tasks/unity.cpp"
|
|
#include "api/users/unity.cpp"
|
|
|
|
internal const char *TAG = "HTTP_SERVER";
|
|
|
|
constexpr uint8 kGZ_Extension_Length = sizeof(".gz") - 1;
|
|
|
|
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
|
|
#define SCRATCH_BUFSIZE 4096
|
|
|
|
typedef struct
|
|
{
|
|
char scratch[SCRATCH_BUFSIZE];
|
|
} http_server_data_t;
|
|
|
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
|
// Set HTTP response content type according to file extension
|
|
internal esp_err_t set_content_type_from_file(httpd_req_t *req,
|
|
const char *filepath)
|
|
{
|
|
const char *type = "text/plain";
|
|
if (strstr(filepath, ".html"))
|
|
{
|
|
type = "text/html";
|
|
}
|
|
else if (strstr(filepath, ".js"))
|
|
{
|
|
type = "application/javascript";
|
|
}
|
|
else if (strstr(filepath, ".css"))
|
|
{
|
|
type = "text/css";
|
|
}
|
|
else if (strstr(filepath, ".png"))
|
|
{
|
|
type = "image/png";
|
|
}
|
|
else if (strstr(filepath, ".ico"))
|
|
{
|
|
type = "image/x-icon";
|
|
}
|
|
else if (strstr(filepath, ".svg"))
|
|
{
|
|
type = "text/xml";
|
|
}
|
|
return httpd_resp_set_type(req, type);
|
|
}
|
|
|
|
// Handler to serve static files from LittleFS
|
|
internal esp_err_t static_file_handler(httpd_req_t *req)
|
|
{
|
|
char filepath[FILE_PATH_MAX];
|
|
|
|
// Construct real file path
|
|
strlcpy(filepath, "/www", sizeof(filepath));
|
|
if (req->uri[strlen(req->uri) - 1] == '/')
|
|
{
|
|
strlcat(filepath, "/index.html", sizeof(filepath));
|
|
}
|
|
else
|
|
{
|
|
strlcat(filepath, req->uri, sizeof(filepath));
|
|
}
|
|
|
|
// Default to index.html if file doesn't exist (SPA routing support)
|
|
struct stat file_stat;
|
|
if (stat(filepath, &file_stat) == -1)
|
|
{
|
|
// Try gzipped first, then fallback to index.html
|
|
char filepath_gz[FILE_PATH_MAX + kGZ_Extension_Length];
|
|
snprintf(filepath_gz, sizeof(filepath_gz), "%s.gz", filepath);
|
|
if (stat(filepath_gz, &file_stat) == 0)
|
|
{
|
|
strlcpy(filepath, filepath_gz, sizeof(filepath));
|
|
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGW(TAG, "File not found: %s, falling back to index.html", filepath);
|
|
snprintf(filepath, sizeof(filepath), "%s/index.html", "/www");
|
|
if (stat(filepath, &file_stat) == -1)
|
|
{
|
|
// If index.html doesn't exist, try index.html.gz
|
|
snprintf(filepath_gz, sizeof(filepath_gz), "%s/index.html.gz", "/www");
|
|
if (stat(filepath_gz, &file_stat) == 0)
|
|
{
|
|
strlcpy(filepath, filepath_gz, sizeof(filepath));
|
|
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "index.html not found too.");
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Since ESP32 handles .gz transparently if we tell it to via headers
|
|
// If we requested explicitly a .gz file, set the header
|
|
if (strstr(filepath, ".gz"))
|
|
{
|
|
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
|
}
|
|
}
|
|
|
|
int fd = open(filepath, O_RDONLY, 0);
|
|
if (fd == -1)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to open file: %s", filepath);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
|
"Failed to read existing file");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
set_content_type_from_file(req, filepath);
|
|
|
|
http_server_data_t *rest_context = (http_server_data_t *)req->user_ctx;
|
|
char *chunk = rest_context->scratch;
|
|
ssize_t read_bytes;
|
|
|
|
do
|
|
{
|
|
read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
|
|
if (read_bytes == -1)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to read file: %s", filepath);
|
|
}
|
|
else if (read_bytes > 0)
|
|
{
|
|
if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK)
|
|
{
|
|
close(fd);
|
|
ESP_LOGE(TAG, "File sending failed!");
|
|
httpd_resp_sendstr_chunk(req, NULL); // Abort sending
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
} while (read_bytes > 0);
|
|
|
|
close(fd);
|
|
httpd_resp_send_chunk(req, NULL, 0); // End response
|
|
|
|
return ESP_OK;
|
|
}
|
|
#endif
|
|
|
|
// Handler for CORS Preflight OPTIONS requests
|
|
internal esp_err_t cors_options_handler(httpd_req_t *req)
|
|
{
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods",
|
|
"GET, POST, DELETE, OPTIONS");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
|
httpd_resp_set_status(req, "204 No Content");
|
|
httpd_resp_send(req, NULL, 0);
|
|
return ESP_OK;
|
|
}
|
|
|
|
internal httpd_handle_t start_webserver(void)
|
|
{
|
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
|
esp_vfs_littlefs_conf_t conf = {};
|
|
conf.base_path = "/www";
|
|
conf.partition_label = g_Active_WWW_Partition == 0 ? "www_0" : "www_1";
|
|
conf.format_if_mount_failed = false;
|
|
conf.dont_mount = false;
|
|
esp_err_t ret = esp_vfs_littlefs_register(&conf);
|
|
if (ret != ESP_OK)
|
|
{
|
|
if (ret == ESP_FAIL)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to mount or format filesystem");
|
|
}
|
|
else if (ret == ESP_ERR_NOT_FOUND)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to find LittleFS partition");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(TAG, "LittleFS mounted on /www");
|
|
}
|
|
#endif
|
|
http_server_data_t *rest_context =
|
|
(http_server_data_t *)calloc(1, sizeof(http_server_data_t));
|
|
if (rest_context == NULL)
|
|
{
|
|
ESP_LOGE(TAG, "No memory for rest context");
|
|
return NULL;
|
|
}
|
|
|
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
|
config.max_uri_handlers = 20;
|
|
|
|
httpd_handle_t server = NULL;
|
|
ESP_LOGI(TAG, "Starting HTTP Server on port: '%d'", config.server_port);
|
|
|
|
if (httpd_start(&server, &config) == ESP_OK)
|
|
{
|
|
// Register CORS OPTIONS handler for API routes
|
|
httpd_uri_t cors_options_uri = {.uri = "/api/*",
|
|
.method = HTTP_OPTIONS,
|
|
.handler = cors_options_handler,
|
|
.user_ctx = NULL};
|
|
httpd_register_uri_handler(server, &cors_options_uri);
|
|
|
|
// Register system API routes
|
|
httpd_register_uri_handler(server, &api_system_info_uri);
|
|
httpd_register_uri_handler(server, &api_system_reboot_uri);
|
|
httpd_register_uri_handler(server, &api_ota_status_uri);
|
|
httpd_register_uri_handler(server, &api_ota_frontend_uri);
|
|
httpd_register_uri_handler(server, &api_ota_firmware_uri);
|
|
httpd_register_uri_handler(server, &api_ota_bundle_uri);
|
|
|
|
// Register todo list API routes
|
|
httpd_register_uri_handler(server, &api_users_get_uri);
|
|
httpd_register_uri_handler(server, &api_users_post_uri);
|
|
httpd_register_uri_handler(server, &api_users_delete_uri);
|
|
httpd_register_uri_handler(server, &api_tasks_upcoming_uri);
|
|
httpd_register_uri_handler(server, &api_tasks_get_uri);
|
|
httpd_register_uri_handler(server, &api_tasks_post_uri);
|
|
httpd_register_uri_handler(server, &api_tasks_update_uri);
|
|
httpd_register_uri_handler(server, &api_tasks_delete_uri);
|
|
|
|
// Populate dummy data for development
|
|
seed_users();
|
|
seed_tasks();
|
|
|
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
|
// Register static file handler last as a catch-all wildcard if deployed
|
|
httpd_uri_t static_get_uri = {.uri = "/*",
|
|
.method = HTTP_GET,
|
|
.handler = static_file_handler,
|
|
.user_ctx = rest_context};
|
|
httpd_register_uri_handler(server, &static_get_uri);
|
|
#endif
|
|
|
|
return server;
|
|
}
|
|
|
|
ESP_LOGE(TAG, "Error starting server!");
|
|
free(rest_context);
|
|
return NULL;
|
|
}
|
|
|
|
internal void stop_webserver(httpd_handle_t server)
|
|
{
|
|
if (server)
|
|
{
|
|
httpd_stop(server);
|
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
|
esp_vfs_littlefs_unregister(g_Active_WWW_Partition == 0 ? "www_0"
|
|
: "www_1");
|
|
#endif
|
|
}
|
|
}
|