I'm pretty new to ESP-IDF and I'm writing a simple application learning from the official examples. My goal is to serve HTML pages (along with js, css and other resource files) from SPIFFS.
I changed the partition table to add a storage:
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, spiffs, , 0xF0000,
then I put inside my project root a data
directory with my files. Finally I told CMake to make the data partition image:
spiffs_create_partition_image(storage ../data FLASH_IN_PROJECT)
Here my code:
#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include "esp_netif.h"
#include "esp_eth.h"
#include "esp_vfs.h"
#include "esp_spiffs.h"
#include "protocol_examples_common.h"
#include <esp_https_server.h>
#include "esp_tls.h"
#include "sdkconfig.h"
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN)
#define MAX_FILE_SIZE (200*1024) // 200 KB
#define MAX_FILE_SIZE_STR "200KB"
#define SCRATCH_BUFSIZE 8192
struct file_server_data {
char base_path[ESP_VFS_PATH_MAX + 1];
char scratch[SCRATCH_BUFSIZE];
};
static const char *TAG = "example";
static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize)
{
const size_t base_pathlen = strlen(base_path);
size_t pathlen = strlen(uri);
const char *quest = strchr(uri, '?');
if (quest) {
pathlen = MIN(pathlen, quest - uri);
}
const char *hash = strchr(uri, '#');
if (hash) {
pathlen = MIN(pathlen, hash - uri);
}
if (base_pathlen + pathlen + 1 > destsize) {
/* Full path string won't fit into destination buffer */
return NULL;
}
/* Construct full path (base + path) */
strcpy(dest, base_path);
strlcpy(dest + base_pathlen, uri, pathlen + 1);
/* Return pointer to path, skipping the base */
return dest + base_pathlen;
}
#define IS_FILE_EXT(filename, ext) (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename)
{
if (IS_FILE_EXT(filename, ".pdf")) {
return httpd_resp_set_type(req, "application/pdf");
} else if (IS_FILE_EXT(filename, ".html")) {
return httpd_resp_set_type(req, "text/html");
} else if (IS_FILE_EXT(filename, ".jpeg")) {
return httpd_resp_set_type(req, "image/jpeg");
} else if (IS_FILE_EXT(filename, ".ico")) {
return httpd_resp_set_type(req, "image/x-icon");
}
/* This is a limited set only */
/* For any other type always set as plain text */
return httpd_resp_set_type(req, "text/plain");
}
static esp_err_t download_get_handler(httpd_req_t *req)
{
char filepath[FILE_PATH_MAX];
FILE *fd = NULL;
struct stat file_stat;
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path, req->uri, sizeof(filepath));
ESP_LOGI(TAG, "%s", filename);
if (!filename) {
ESP_LOGE(TAG, "Filename is too long");
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
return ESP_FAIL;
}
if (stat(filepath, &file_stat) == -1) {
ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
/* Respond with 404 Not Found */
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
return ESP_FAIL;
}
fd = fopen(filepath, "r");
if (!fd) {
ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
set_content_type_from_file(req, filename);
/* Retrieve the pointer to scratch buffer for temporary storage */
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
size_t chunksize;
do {
/* Read file in chunks into the scratch buffer */
chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
if (chunksize > 0) {
/* Send the buffer contents as HTTP response chunk */
if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
fclose(fd);
ESP_LOGE(TAG, "File sending failed!");
/* Abort sending file */
httpd_resp_sendstr_chunk(req, NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
return ESP_FAIL;
}
}
/* Keep looping till the whole file is sent */
} while (chunksize != 0);
/* Close file after sending complete */
fclose(fd);
ESP_LOGI(TAG, "File sending complete");
/* Respond with an empty chunk to signal HTTP response completion */
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
httpd_resp_set_hdr(req, "Connection", "close");
#endif
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
static httpd_handle_t start_webserver(void)
{
const char* base_path = "/data";
static struct file_server_data *server_data = NULL;
if (server_data) {
ESP_LOGE(TAG, "File server already started");
return ESP_ERR_INVALID_STATE;
}
/* Allocate memory for server data */
server_data = calloc(1, sizeof(struct file_server_data));
if (!server_data) {
ESP_LOGE(TAG, "Failed to allocate memory for server data");
return ESP_ERR_NO_MEM;
}
strlcpy(server_data->base_path, base_path, sizeof(server_data->base_path));
httpd_handle_t server = NULL;
// Start the httpd server
ESP_LOGI(TAG, "Starting server");
httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();
extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start");
extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end");
conf.servercert = servercert_start;
conf.servercert_len = servercert_end - servercert_start;
extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
extern const unsigned char prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
conf.prvtkey_pem = prvtkey_pem_start;
conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;
#if CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK
conf.user_cb = https_server_user_callback;
#endif
esp_err_t ret = httpd_ssl_start(&server, &conf);
if (ESP_OK != ret) {
ESP_LOGI(TAG, "Error starting server!");
return NULL;
}
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_uri_t file_download = {
.uri = "/*", // Match all URIs of type /path/to/file
.method = HTTP_GET,
.handler = download_get_handler,
.user_ctx = server_data // Pass server data as context
};
ret = httpd_register_uri_handler(server, &file_download);
ESP_LOGI(TAG, "%u", ret);
return server;
}
static esp_err_t stop_webserver(httpd_handle_t server)
{
// Stop the httpd server
return httpd_ssl_stop(server);
}
static void disconnect_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
httpd_handle_t* server = (httpd_handle_t*) arg;
if (*server) {
if (stop_webserver(*server) == ESP_OK) {
*server = NULL;
} else {
ESP_LOGE(TAG, "Failed to stop https server");
}
}
}
static void connect_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
httpd_handle_t* server = (httpd_handle_t*) arg;
if (*server == NULL) {
*server = start_webserver();
}
}
void app_main(void)
{
static httpd_handle_t server = NULL;
esp_vfs_spiffs_conf_t conf = {
.base_path = "/data",
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = false
};
esp_err_t ret = esp_vfs_spiffs_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 SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
return;
}
size_t total = 0, used = 0;
ret = esp_spiffs_info(NULL, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
/* Register event handlers to start server when Wi-Fi or Ethernet is connected,
* and stop server when disconnection happens.
*/
#ifdef CONFIG_EXAMPLE_CONNECT_WIFI
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server));
#endif // CONFIG_EXAMPLE_CONNECT_WIFI
#ifdef CONFIG_EXAMPLE_CONNECT_ETHERNET
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &connect_handler, &server));
ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, &disconnect_handler, &server));
#endif // CONFIG_EXAMPLE_CONNECT_ETHERNET
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
* Read "Establishing Wi-Fi or Ethernet Connection" section in
* examples/protocols/README.md for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
}
The issue is about registering the URI handler:
httpd_uri_t file_download = {
.uri = "/*", // Match all URIs of type /path/to/file
.method = HTTP_GET,
.handler = download_get_handler,
.user_ctx = server_data // Pass server data as context
};
Using the "/*"
pattern, no matter the file I request I get the following messages and a 404 is sent to the browser:
W (11845) httpd_uri: httpd_uri: URI '/index.html' not found
W (11845) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI
Please note the warning comes from the httpd_uri
tag, not from my code (which has example
as tag). I mean, it did not enter the download_get_handler
at all.
If I change the pattern to, say, "/index.html"
it finds the URI, executes the download_get_handler
function, retrieves the file in the SPIFFS partition and serves it to the browser.
Of course I need a wildcard, since I want to serve any file that has no other handlers before (like a specific API that is handled dynamically by the code).
What is the right syntax to match all the files below root?
For some reasons the matching of wildcard is disabled by default. You have to explicitly enable it:
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
config.httpd.uri_match_fn = httpd_uri_match_wildcard;
This behavior is described here in the documentation.