File Server Example : Check longer than allowed filenames when converting from URIs to filepaths

This change prevents buffer overflows in case of really long file paths.

Other changes:
* Remove query (?) and fragment (#) component from URI when converting to file path
* /index.html and favicon.ico can be overridden by files with same name and path in SPIFFS
* README.md updated
This commit is contained in:
Anurag Kar
2019-04-26 01:28:33 +05:30
parent df9113e0e1
commit 59afbe4eed
2 changed files with 126 additions and 100 deletions

View File

@@ -42,37 +42,50 @@ struct file_server_data {
static const char *TAG = "file_server";
/* Handler to redirect incoming GET request for /index.html to / */
/* Handler to redirect incoming GET request for /index.html to /
* This can be overridden by uploading file with same name */
static esp_err_t index_html_get_handler(httpd_req_t *req)
{
httpd_resp_set_status(req, "301 Permanent Redirect");
httpd_resp_set_status(req, "307 Temporary Redirect");
httpd_resp_set_hdr(req, "Location", "/");
httpd_resp_send(req, NULL, 0); // Response body can be empty
return ESP_OK;
}
/* Send HTTP response with a run-time generated html consisting of
* a list of all files and folders under the requested path */
static esp_err_t http_resp_dir_html(httpd_req_t *req)
/* Handler to respond with an icon file embedded in flash.
* Browsers expect to GET website icon at URI /favicon.ico.
* This can be overridden by uploading file with same name */
static esp_err_t favicon_get_handler(httpd_req_t *req)
{
char fullpath[FILE_PATH_MAX];
extern const unsigned char favicon_ico_start[] asm("_binary_favicon_ico_start");
extern const unsigned char favicon_ico_end[] asm("_binary_favicon_ico_end");
const size_t favicon_ico_size = (favicon_ico_end - favicon_ico_start);
httpd_resp_set_type(req, "image/x-icon");
httpd_resp_send(req, (const char *)favicon_ico_start, favicon_ico_size);
return ESP_OK;
}
/* Send HTTP response with a run-time generated html consisting of
* a list of all files and folders under the requested path.
* In case of SPIFFS this returns empty list when path is any
* string other than '/', since SPIFFS doesn't support directories */
static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath)
{
char entrypath[FILE_PATH_MAX];
char entrysize[16];
const char *entrytype;
DIR *dir = NULL;
struct dirent *entry;
struct stat entry_stat;
/* Retrieve the base path of file storage to construct the full path */
strcpy(fullpath, ((struct file_server_data *)req->user_ctx)->base_path);
DIR *dir = opendir(dirpath);
const size_t dirpath_len = strlen(dirpath);
/* Concatenate the requested directory path */
strcat(fullpath, req->uri);
dir = opendir(fullpath);
const size_t entrypath_offset = strlen(fullpath);
/* Retrieve the base path of file storage to construct the full path */
strlcpy(entrypath, dirpath, sizeof(entrypath));
if (!dir) {
ESP_LOGE(TAG, "Failed to stat dir : %s", fullpath);
ESP_LOGE(TAG, "Failed to stat dir : %s", dirpath);
/* Respond with 404 Not Found */
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist");
return ESP_FAIL;
@@ -100,8 +113,8 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req)
while ((entry = readdir(dir)) != NULL) {
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
strncpy(fullpath + entrypath_offset, entry->d_name, sizeof(fullpath) - entrypath_offset);
if (stat(fullpath, &entry_stat) == -1) {
strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
if (stat(entrypath, &entry_stat) == -1) {
ESP_LOGE(TAG, "Failed to stat %s : %s", entrytype, entry->d_name);
continue;
}
@@ -145,33 +158,80 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req)
(strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t *req)
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename)
{
if (IS_FILE_EXT(req->uri, ".pdf")) {
if (IS_FILE_EXT(filename, ".pdf")) {
return httpd_resp_set_type(req, "application/pdf");
} else if (IS_FILE_EXT(req->uri, ".html")) {
} else if (IS_FILE_EXT(filename, ".html")) {
return httpd_resp_set_type(req, "text/html");
} else if (IS_FILE_EXT(req->uri, ".jpeg")) {
} 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");
}
/* Send HTTP response with the contents of the requested file */
static esp_err_t http_resp_file(httpd_req_t *req)
/* Copies the full path into destination buffer and returns
* pointer to path (skipping the preceding base path) */
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;
}
/* Handler to download a file kept on the server */
static esp_err_t download_get_handler(httpd_req_t *req)
{
char filepath[FILE_PATH_MAX];
FILE *fd = NULL;
struct stat file_stat;
/* Retrieve the base path of file storage to construct the full path */
strcpy(filepath, ((struct file_server_data *)req->user_ctx)->base_path);
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
req->uri, sizeof(filepath));
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 name has trailing '/', respond with directory contents */
if (filename[strlen(filename) - 1] == '/') {
return http_resp_dir_html(req, filepath);
}
/* Concatenate the requested file path */
strcat(filepath, req->uri);
if (stat(filepath, &file_stat) == -1) {
/* If file not present on SPIFFS check if URI
* corresponds to one of the hardcoded paths */
if (strcmp(filename, "/index.html") == 0) {
return index_html_get_handler(req);
} else if (strcmp(filename, "/favicon.ico") == 0) {
return favicon_get_handler(req);
}
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");
@@ -186,8 +246,8 @@ static esp_err_t http_resp_file(httpd_req_t *req)
return ESP_FAIL;
}
ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filepath, file_stat.st_size);
set_content_type_from_file(req);
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;
@@ -219,20 +279,6 @@ static esp_err_t http_resp_file(httpd_req_t *req)
return ESP_OK;
}
/* Handler to download a file kept on the server */
static esp_err_t download_get_handler(httpd_req_t *req)
{
// Check if the target is a directory
if (req->uri[strlen(req->uri) - 1] == '/') {
// In so, send an html with directory listing
http_resp_dir_html(req);
} else {
// Else send the file
http_resp_file(req);
}
return ESP_OK;
}
/* Handler to upload a file onto the server */
static esp_err_t upload_post_handler(httpd_req_t *req)
{
@@ -242,21 +288,21 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
/* Skip leading "/upload" from URI to get filename */
/* Note sizeof() counts NULL termination hence the -1 */
const char *filename = req->uri + sizeof("/upload") - 1;
/* Filename cannot be empty or have a trailing '/' */
if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') {
ESP_LOGE(TAG, "Invalid file name : %s", filename);
/* Respond with 400 Bad Request */
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name");
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
req->uri + sizeof("/upload") - 1, sizeof(filepath));
if (!filename) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
return ESP_FAIL;
}
/* Retrieve the base path of file storage to construct the full path */
strcpy(filepath, ((struct file_server_data *)req->user_ctx)->base_path);
/* Filename cannot have a trailing '/' */
if (filename[strlen(filename) - 1] == '/') {
ESP_LOGE(TAG, "Invalid filename : %s", filename);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
return ESP_FAIL;
}
/* Concatenate the requested file path */
strcat(filepath, filename);
if (stat(filepath, &file_stat) == 0) {
ESP_LOGE(TAG, "File already exists : %s", filepath);
/* Respond with 400 Bad Request */
@@ -350,23 +396,23 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
char filepath[FILE_PATH_MAX];
struct stat file_stat;
/* Skip leading "/upload" from URI to get filename */
/* Skip leading "/delete" from URI to get filename */
/* Note sizeof() counts NULL termination hence the -1 */
const char *filename = req->uri + sizeof("/upload") - 1;
/* Filename cannot be empty or have a trailing '/' */
if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') {
ESP_LOGE(TAG, "Invalid file name : %s", filename);
/* Respond with 400 Bad Request */
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name");
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
req->uri + sizeof("/delete") - 1, sizeof(filepath));
if (!filename) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
return ESP_FAIL;
}
/* Retrieve the base path of file storage to construct the full path */
strcpy(filepath, ((struct file_server_data *)req->user_ctx)->base_path);
/* Filename cannot have a trailing '/' */
if (filename[strlen(filename) - 1] == '/') {
ESP_LOGE(TAG, "Invalid filename : %s", filename);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
return ESP_FAIL;
}
/* Concatenate the requested file path */
strcat(filepath, filename);
if (stat(filepath, &file_stat) == -1) {
ESP_LOGE(TAG, "File does not exist : %s", filename);
/* Respond with 400 Bad Request */
@@ -385,18 +431,6 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
return ESP_OK;
}
/* Handler to respond with an icon file embedded in flash.
* Browsers expect to GET website icon at URI /favicon.ico */
static esp_err_t favicon_get_handler(httpd_req_t *req)
{
extern const unsigned char favicon_ico_start[] asm("_binary_favicon_ico_start");
extern const unsigned char favicon_ico_end[] asm("_binary_favicon_ico_end");
const size_t favicon_ico_size = (favicon_ico_end - favicon_ico_start);
httpd_resp_set_type(req, "image/x-icon");
httpd_resp_send(req, (const char *)favicon_ico_start, favicon_ico_size);
return ESP_OK;
}
/* Function to start the file server */
esp_err_t start_file_server(const char *base_path)
{
@@ -436,27 +470,9 @@ esp_err_t start_file_server(const char *base_path)
return ESP_FAIL;
}
/* Register handler for index.html which should redirect to / */
httpd_uri_t index_html = {
.uri = "/index.html",
.method = HTTP_GET,
.handler = index_html_get_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &index_html);
/* Handler for URI used by browsers to get website icon */
httpd_uri_t favicon_ico = {
.uri = "/favicon.ico",
.method = HTTP_GET,
.handler = favicon_get_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &favicon_ico);
/* URI handler for getting uploaded files */
httpd_uri_t file_download = {
.uri = "/*", // Match all URIs of type /path/to/file (except index.html)
.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